SQLAlchemy + pyTelegramBotAPI: объекты SQLite, созданные в потоке, могут использоваться только в этом же потоке.

У меня настоящая головная боль от попыток понять причину следующей проблемы. Мы используем комбинацию следующих библиотек:

SQLAlchemy сначала использовал NullPool, а теперь настроен на использование QueuePool. Я также использую следующую идиому, чтобы запустить новый сеанс БД для каждого потока (насколько я понимаю)

Session = sessionmaker(bind=create_engine(classes.db_url, poolclass=QueuePool))

@contextmanager
def session_scope():
   session = Session()
   try:
      yield session
      session.commit()
   except:
      session.rollback()
      raise
   finally:
      session.close()

@bot.message_handler(content_types=['document'])
def method_handler:
   with session_scope() as session:
      do_database_stuff_here(session)

Тем не менее, я все еще получаю это раздражающее исключение: (sqlite3.ProgrammingError) SQLite objects created in a thread can only be used in that same thread

Любые идеи? ;) В частности, я не понимаю, как другой протектор может оказаться где-то между операциями БД... это, вероятно, причина надоедливого исключения

обновление 1: если я изменю poolclass на SingletonThreadPool, то ошибок больше не будет. Однако в документации SQLAlchemy говорится, что это не массовое производство.


person d56    schedule 19.10.2016    source источник
comment
Вы имеете в виду NullPool, когда говорите NullThreadPool? Почему вам нужно использовать QueuePool?   -  person xli    schedule 22.10.2016
comment
Из документации SQLAlchemy: Когда указана файловая база данных, диалект будет использовать NullPool в качестве источника соединений. Этот пул закрывает и отбрасывает соединения, которые немедленно возвращаются в пул. Соединения на основе файлов SQLite имеют чрезвычайно низкие накладные расходы, поэтому объединение в пул не требуется. Эта схема также предотвращает повторное использование соединения в другом потоке и лучше всего работает с грубой блокировкой файлов SQLite.   -  person xli    schedule 22.10.2016
comment
Как говорит xli, почему вы пытаетесь использовать QueuePool? В документах совершенно ясно, что вы можете используйте NullPool, SingletonThreadPool или StaticPool.   -  person Peter Brittain    schedule 23.10.2016
comment
@xli, да, я имею в виду NullPool;) спасибо за исправление. Идея использования QueuePool заключалась в том, чтобы одновременно иметь несколько потоков (чтения). Почему это было бы неправильно? @xli, @Peter Brittain, реализация SqlAlchemy по умолчанию использует NullPool, но при ее использовании я получаю исключения (как указано выше). Я понятия не имею, почему я их получаю, так как с помощью @contextmanager должно быть гарантировано, что я открываю одно соединение для каждого потока...   -  person d56    schedule 23.10.2016
comment
Я получил ошибку, которую вы показываете с несколькими потоками, выполняющими операции с базой данных с QueuePool, но она исчезла с NullPool. Может быть, у вас есть несколько потоков в вашем блоке with, или вы могли бы описать, что вы там делаете?   -  person xli    schedule 23.10.2016
comment
@xli, к методу method_handler() обращаются несколько потоков, но внутри do_database_stuff_here() нет вызовов новых потоков. Вот почему я не понимаю, почему это не работает. Но почему вы получаете ошибку с QueuePool? согласно моим рассуждениям, хотя потоки используются повторно, все же есть один поток, обслуживающий method_handler(), не так ли?   -  person d56    schedule 24.10.2016
comment
Одно и то же соединение SQLite не может использоваться более чем в одном потоке. QueuePool будет повторно использовать соединения и передавать то же соединение другому потоку. Но NullPool следует избегать этой проблемы, потому что он каждый раз устанавливает новое соединение, а использование NullPool привело к тому, что ошибка исчезла, когда я ее воспроизвел.   -  person xli    schedule 24.10.2016
comment
я проверяю это сейчас с NullPool. Хотя один вопрос в сторону. Означает ли использование with session_scope() as session, что у меня здесь есть блокирующий сегмент кода, пока NullPool используется для соединения с каким-то другим потоком? Я читал, что сегодня Sqlite использует WAL для одновременного чтения. Есть ли способ активировать его в SQLAlchemy   -  person d56    schedule 26.10.2016
comment
@xli, причина в том, что блок with ... session является довольно продолжительным сегментом, в котором происходит больше, чем просто соединение с БД (которое я не могу отделить)   -  person d56    schedule 26.10.2016
comment
Нет - не блокирует. Он основан на блокировке sqlite, которая, как вы можете видеть здесь, будет по-прежнему разрешать одновременный доступ для чтения. Однако вы можете обнаружить, что доступ для записи заблокирован, если вы оставите транзакцию открытой для этих длительных процессов. Если это неизбежно, лучшим вариантом может быть режим WAL.   -  person Peter Brittain    schedule 26.10.2016
comment
@xli, еще раз спасибо. Насколько я вижу из логов, ошибка больше не появляется. Установка класса пула на NullPool сделала свое дело!   -  person d56    schedule 14.11.2016


Ответы (2)


Как вы можете видеть в исходнике, sqlite вызовет это исключение внутри pysqlite_check_thread, если объект подключения повторно используется в любых потоках.

Используя QueuePool, вы сообщаете SQLAchemy, что безопасно повторно использовать соединения в нескольких потоках. Поэтому он просто выберет соединение из пула для любого сеанса, независимо от того, в каком потоке он находится. Вот почему вы нажимаете на ошибку. При первом создании и использовании соединения все будет в порядке; однако следующее использование, вероятно, будет в другом потоке, поэтому проверка не пройдет.

Вот почему SQLAlchemy предписывает использование других пулов такие как SingletonThreadPool и NullPool.

Предполагая, что вы используете базу данных на основе файлов, вы должны использовать файл NullPool. Это даст вам хороший параллелизм при чтении. Параллелизм доступа для записи всегда будет проблемой для sqlite; если вам это нужно, вам, вероятно, нужна база данных diffenet.

person Peter Brittain    schedule 24.10.2016

Что-то, что, возможно, стоит попробовать: используйте scoped_session вместо contextmanager; scoped_session неявно создает локальный сеанс потока, когда к нему обращаются из другого потока. Не забудьте также использовать NullPool.

from sqlalchemy.orm import scoped_session
sessionmaker(bind=create_engine(classes.db_url, poolclass=NullPool))
session = scoped_session()

Обратите внимание, что вы можете использовать этот session с областью действия напрямую, как если бы это был обычный session, даже несмотря на то, что он фактически создает локальные сеансы потока за кулисами, когда он используется.

Для scoped_session следует вызывать session.remove() for после того, как вы закончите (т. е. после каждого method_handler) и явно вызывать session.commit() по мере необходимости.

Теоретически ваш менеджер контекста должен предоставлять каждому потоку свой собственный сеанс, но из-за отсутствия лучшего объяснения мне интересно, есть ли несколько потоков, обращающихся к этому сеансу в контексте.

person xli    schedule 27.10.2016