SQLite3 マルチプロセス

FastAPI や Flask などで SQLite3 を利用するとき、非同期処理などを行う際に、ほぼ同じ時刻に複数のプロセスが実行され SQLite3 からデータをとる(SELECT)ことが頻繁に発生する。この際に Python プログラムから "Recursive use of cursors not allowed" のようなエラーが出力され、プロセスが中断される。

  File "/Users/username/.pyenv/versions/3.11/envs/microapp/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 867, in run
    result = context.run(func, *args)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/Library/CloudStorage/OneDrive-Personal/projects/AppLab/app-jcddb/app.py", line 137, in get_stats
    db.execute(sql)
  File "/Users/username/Library/CloudStorage/OneDrive-Personal/projects/AppLab/app-jcddb/app.py", line 39, in execute
    self.cur.execute(sql)
sqlite3.ProgrammingError: Recursive use of cursors not allowed.

SQLite3 がマルチスレッドまたはプロセスに対応していないために、ほぼ同じ時刻に複数のアクセスが入ると、処理が追いつかなくなります。

このエラーを回避する対策として、複数の処理を一つずつ実行するように制御すればよい。例えば、クエリーが実行されたときに、フラグを確認して True であれば、それを False に変更してクエリーを実行します。実行後にそのフラグをまた True に戻す。

このとき、もし少しだけ遅れた処理が入ってきたとき、その時点でフラグが False であるならば、そのクエリーを実行せずに、0.1 秒を待ってから、再度フラグの値を確認して実行をトライするように制御する。

プログラムでは例えば次のように実装できる。

class DB():
    def __init__(self, host):
        self.host = host
        self.con = sqlite3.connect(host,
                                   check_same_thread=False,
                                   uri=True,
                                   isolation_level='DEFERRED')
        self.cur = self.con.cursor()
        self.cur.execute('PRAGMA foreign_keys = ON')
        self.is_unlock = True

    def select(self, sql):
        while True:
            if self.is_unlock:
                self.is_unlock = False
                self.cur.execute(sql)
                output = self.cur.fetchall()
                self.is_unlock = True
                return output
            time.sleep(0.1)

あとはこれを呼び出して実行すれば、ほとんどの場合、Recursive use of cursors not allowed. というエラーがでなくなる。これでも出る場合は、time.sleep の値を調整するとよいかもしれない。

db = DB('path/to/db.sqlite3')
x = db.select('SELECT * FROM locations')
print(x)