無料Streamlitアプリが寝てしまう

「オープンデータをSTREAMLITでビジュアルに表示してみよう」や「路線バスデータをマッピングしてみよう」で紹介した”Streamlit”はPythonでとても簡単にWebアプリケーションを作成できるオープンソースのライブラリですが、無料プランの利用だと、しばらくアクセスが途絶えるとスリープモードに入って使えなくなってしまいます。「get this app back up!」を押して、数分待つと復帰するのですが誰かに公開しているページだと使えない状態を作り出してしまうのは避けたいですよね。(頻繁にアクセスしてもらえないのが、スリープに入る原因だけれども。。) ここでは、無料プランのStreamlitアプリを寝させないための取り組みを紹介します。

寝てしまう
毎日のようにアクセスしていないと寝てしまいます。そして起こすのに数分かかります。

自動でアクセスしてStreamlitアプリが寝るのを防ぎたい
毎日パソコンを起動していて、忘れずにStreamlitアプリのページを開けばアクセスがしばらく途絶えることは無いのですが、パソコンを毎日起動するとは限らないし、Streamlitアプリのことを忘れるでしょう。パソコンを起動しっぱ無しという運用をしていれば、OSのイベント/タスクスケジューラを使えばなんとかできそうですが、パソコンを起動しっぱ無しになんかしていません。そこで、次の2つについてトライしました。

  • 定期的にHTTPリクエストを送ることができるサービスサイト(UptimeRobotやCron-job.org等)を利用する
  • Streamlitアプリのページにアクセスするスクリプトを作成し、ずっと起動しっぱなしのRaspberry piに定期実行させる。

UptimeRobot
以下の手順です。
UptimeRobotに登録してログインします。
「新しいモニターの追加」をクリックします。
モニタータイプを「HTTP(s)」に設定します。
名前を入力して、URL に自動アクセスしたい Streamlit アプリの URL を入力します。
監視間隔を設定します(例:5分ごと)。
「モニターの作成」をクリックします。

結論を言うと、5分起きにStreamlitアプリのページ(URL)にHTTPリクエストを送る設定にしましたが、Streamlitアプリが寝てしまうのを防ぐことはできませんでした。

Pythonコードの定期的実行
行う作業としては次の二つになります。

  • StreamlitアプリのページにHTTPリクエストを送るPythonコードの作成
  • CRONに上記Pythoコードを定期実行するのための記述とCRON再起動

ここで注意すべきはパーミッション設定です。実行させるPythonコードやPythonコードの中で扱うログファイルについて、cronが扱えるように必要な権限設定を行っておきましょう。

pythonコードの例

import requests
import time
import logging
from datetime import datetime

# ログの設定
logging.basicConfig(filename='app_ping.log', 
                    level=logging.INFO, 
                    format='%(asctime)s - %(levelname)s - %(message)s')

def ping_app():
    url = "https://bigdogcerberus-eu-covid19-plot-covid19-7hcwa4.streamlit.app/"
    try:
        response = requests.get(url)
        if response.status_code == 200:
            logging.info("App is up and running!")
        else:
            logging.warning(f"Received unexpected status code {response.status_code}")
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    ping_app()

実行する権限があることは確認した後、cronを再起動して結果を確認しました。このpythonコードはcronにより定期(15分起き)実行され、ログファイルの記録を見る限りは指定したURLにアクセスしてくれたようなのですが、Streamlitアプリが眠るのを防ぐ効果はありませんでした。

続いて、ブラウザでアクセスさせてみるのですが、単にHTTPリクエストを出すのとブラウザでアクセスするので差があるのか気になりますね。調べてみると違いがあるようです。

HTTPリクエストとSeleniumのアクセス(GUIあり、無し(ヘッドレスモード)には、以下の違いがあります。

HTTPリクエストだけ
HTTPリクエストを送信する場合、以下のことが行われます:
サーバーはGETリクエストを受け取り、クライアントにHTMLコンテンツを返します。
リクエストは単純なもの(ヘッダー、ボディが付随しない場合が多い)です。
多くのホスティングサービスやアプリケーションサーバーは、一定期間HTTPリクエストのみではアプリをスリープから復帰させない設定になっています。これは、負荷を軽減し、リソースを節約するためです。

Seleniumによるアクセス(GUIあり、無し(ヘッドレスモード))
Seleniumを使用してブラウザを自動操作する場合、以下のことが行われます:
実際のブラウザが起動し、指定されたURLにアクセスします。
JavaScriptや他のリソースもブラウザ内で実行されます。
これにより、アプリケーションは実際にユーザーがアクセスしていると見なされます。
ブラウザがページを完全にレンダリングし、JavaScriptを実行するため、サーバーはアプリケーションがアクティブに使用されていると認識しやすくなります。

違いの要点
HTTPリクエストは単純なリクエストであり、ページの完全なレンダリングやJavaScriptの実行を伴いません。多くの場合、これだけではアプリがアクティブだと認識されません。
Seleniumによるブラウザアクセスは、ページの完全なレンダリングとJavaScriptの実行を伴うため、アプリケーションサーバーは実際のユーザーがアクセスしていると認識しやすくなります。
というわけで、ブラウザでアクセスすると目的を達成できそうな期待が高まります。

ブラウザでアクセスさせよう
環境はこのとおりです。
Python 3.7.3とSelenium 4.2
Selenium の準備ができていない場合はインストールしましょう。

Pythonのコード例は次のとおりです。2つのStreamlitアプリのページを続けて開く・閉じるを行うようにしました。IDEで実行するときとcronで実行するときで処理を分けています。cronで実行するときはブラウザを開かないようにする必要があるためです。ただ、動作を確認するにあたってはブラウザを開きたいのでIDEで実行するときはブラウザをGUIで確認するようにします。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep
import time
import logging
from datetime import datetime

# ログの設定
logging.basicConfig(filename='app_webopen.log', 
                    level=logging.DEBUG, 
                    format='%(asctime)s - %(levelname)s - %(message)s')

def web_open(url,debug):
    try:
        if debug == 1:
            # ブラウザを起動 for debug
            driver = webdriver.Chrome()   
        else :    
            #ブラウザを起動(ヘッドレスモード) : for cron
            options = Options()
            options.add_argument('--headless')
            driver = webdriver.Chrome(options=options)

        # URLのWEBサイトを開く
        driver.get(url)

        logging.info("Web Opened!")
        
        driver.implicitly_wait(120)    

        # If you want to do something, write the command here.

        sleep(5)

        # ブラウザを閉じる
        driver.quit()
    except Exception as e:
        logging.exception(f"Error cccuered! : {e}")

def main(debug):
    url_1 = "https://bigdogcerberus-eu-covid19-plot-covid19-7hcwa4.streamlit.app/"
    url_2 = "https://bigdogcerberus-gt-gtfs-jp-gtfs-jp-ohao7s.streamlit.app/"
    url_list = [url_1, url_2]

    for url in url_list:
        web_open(url, debug)

if __name__ == '__main__':
    debug = 0
    main(debug)

IDEから実行
上記コードの下の方で、debug=1に書き換えてIDEから実行すると、このようにブラウザが開きます。そしてしばらくすると閉じます。これでIDEからは意図どおりに動作することの確認ができました。

続いて、これをcronを使って定期実行させます。15分起きに実行させるようにしました。

結果はどうか
Raspberry pi の /var/log/cron.log を見ると、cronで設定したとおり定期的に該当するPythonのコードは実行され、特にエラーが見つかりません。



Jun 19 09:30:01 raspberrypi CRON[17681]: (username) CMD (/usr/bin/python3 /home/bigdog/Python/web_open.py --headless >> /home/bigdog/cron_output.log 2>&1)
Jun 19 10:00:01 raspberrypi CRON[18375]: (username) CMD (/usr/bin/python3 /home/bigdog/Python/web_open.py --headless >> /home/bigdog/cron_output.log 2>&1)

しかし、翌日に手動でターゲットのStreamlitアプリのページを開くと寝ているのです。ブラウザを開きStreamlitアプリが準備完了になる前にselenium.webdriverの動作が邪魔しているのかもしれません。少なくとも手動操作とは異なる事が起きていると考えられます。

改良版
driver.get(url)でブラウザを開く前に、driver.implicitly_wait(120) で、selenium.webdriverに待たせます。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
# from selenium.webdriver.common.by import By
# from selenium.webdriver.support.wait import WebDriverWait
# from selenium.webdriver.support import expected_conditions as EC

from time import sleep
import time
import logging
from datetime import datetime
import os
os.environ['DISPLAY'] = ':0.0'

# ログの設定
logging.basicConfig(filename='app_webopen.log', 
                    level=logging.DEBUG, 
                    format='%(asctime)s - %(levelname)s - %(message)s')

def web_open(url,debug):
    try:
        driver_service = Service('/usr/bin/chromedriver')
        if debug == 1:
            # ブラウザを起動 for debug
            driver = webdriver.Chrome(service=driver_service)   
        else :    
            #ブラウザを起動(ヘッドレスモード) : for cron
            options = Options()
            options.add_argument('--headless')
            driver = webdriver.Chrome(service=driver_service,options=options)
        
        # 暗黙的な待機時間の設定
        driver.implicitly_wait(120)    

        # URLのWEBサイトを開く
        driver.get(url)

        logging.info("Web Opened!")
        sleep(30)
       
       
        # ブラウザを閉じる

        driver.quit()
    except Exception as e:
        logging.exception(f"Error cccuered! : {e}")

def main(debug):
    url_1 = "https://bigdogcerberus-eu-covid19-plot-covid19-7hcwa4.streamlit.app/"
    url_2 = "https://bigdogcerberus-gt-gtfs-jp-gtfs-jp-ohao7s.streamlit.app/"
    url_list = [url_1, url_2]

    for url in url_list:
        web_open(url,debug)

if __name__ == '__main__':
    debug = 0
    main(debug)

成功
これまで一日手動でアクセスしなければ寝ていたのですが、一週間放置しても起きてます。うまくいったかもしれません。しばらく様子を見ます。これで駄目なら、Streamlitの使い方に手を入れることにします。