Как узнать, какой процесс отвечает за «OperationalError: база данных заблокирована»? - PullRequest
0 голосов
/ 13 ноября 2018

Я иногда случайно встречаю:

OperationalError: база данных заблокирована

в процессе, который обновляет базу данных SQLite, но мне трудно воспроизвести ошибку:

  • другой процесс не вставляет / удаляет строки одновременно
  • только один процесс может выполнять некоторые запросы только для чтения (SELECT и т. Д.) Здесь и там, но без фиксации

Я уже прочитал OperationalError: база данных заблокирована

Вопрос: существует ли способ, когда эта ошибка происходит, регистрировать, какой другой идентификатор процесса отвечает за блокировку?

В общем, как отлаживать OperationalError: database is locked?

Ответы [ 2 ]

0 голосов
/ 25 ноября 2018

Решение: Всегда закрывайте cursor для (даже для чтения) запросов!

Во-первых, вот способ воспроизвести проблему:

  1. Сначала запустите этот код, один раз:

    import sqlite3
    conn = sqlite3.connect('anothertest.db')
    conn.execute("CREATE TABLE IF NOT EXISTS mytable (id int, description text)")
    for i in range(100):
        conn.execute("INSERT INTO mytable VALUES(%i, 'hello')" % i)
    conn.commit()
    

    , чтобы инициализировать тест.

  2. Затем начните только для чтения запрос:

    import sqlite3, time
    conn = sqlite3.connect('anothertest.db')
    c = conn.cursor()
    c.execute('SELECT * FROM mytable')
    item = c.fetchone()
    print(item)
    print('Sleeping 60 seconds but the cursor is not closed...')
    time.sleep(60)
    

    и оставьте этот скрипт работающим при выполнении следующего шага :

  3. Затем попробуйте удалить некоторый контент и зафиксировать:

    import sqlite3
    conn = sqlite3.connect('anothertest.db')
    conn.execute("DELETE FROM mytable WHERE id > 90")
    conn.commit()
    

    Это действительно вызовет эту ошибку:

    sqlite3.OperationalError: база данных заблокирована

Почему?Поскольку невозможно удалить данные, к которым в данный момент обращается запрос на чтение: если курсор все еще открыт, это означает, что данные все еще можно получить с помощью fetchone или fetchall.

Вот какЧтобы устранить ошибку: на шаге 2 просто добавьте:

item = c.fetchone()
print(item)
c.close()
time.sleep(60)

Затем, пока он еще работает, запустите скрипт # 3, вы увидите, что ошибки больше нет.

0 голосов
/ 13 ноября 2018

Есть ли способ, когда эта ошибка происходит, регистрировать, какой другой идентификатор процесса отвечает за блокировку?

Нет, эта информация не записывается при возникновении исключения. Исключение OperationalError: database is locked обычно возникает после тайм-аута (по умолчанию 5 минут) при попытке получить мьютекс и блокировку файла во внутренних органах SQLite, после чего SQLite возвращает SQLITE_BUSY, но SQLITE_BUSY также может быть сообщается в других точках. Коды ошибок SQLite не несут никакого дальнейшего контекста, такого как PID другого процесса, который удерживал блокировку, и вполне возможно, что блокировка была передана между двумя другими процессами, прежде чем текущий процесс прекратил попытки получить ее!

В лучшем случае вы можете перечислить, какие процессы в данный момент обращаются к файлу, используя lsof <filename of database>, но это не приблизит вас к выяснению, какой из них на самом деле занимает слишком много времени для фиксации.

Вместо этого я бы снабдил ваш код явными транзакциями и подробной регистрацией того, когда вы начинаете и делаете транзакции. Затем, когда вы столкнетесь с OperationalError исключениями, вы можете проверить журналы на предмет того, что происходило в этом временном окне.

Менеджер контекста Python, который можно использовать для этого:

import logging
import sys
import time
import threading
from contextlib import contextmanager
from uuid import uuid4

logger = logging.getLogger(__name__)


@contextmanager
def logged_transaction(con, stack_info=False, level=logging.DEBUG):
    """Manage a transaction and log start and end times.

    Logged messages include a UUID transaction ID for ease of analysis.

    If trace is set to True, also log all statements executed.
    If stack_info is set to True, a stack trace is included to record
    where the transaction was started (the last two lines will point to this
    context manager).

    """
    transaction_id = uuid4()
    thread_id = threading.get_ident()

    def _trace_callback(statement):
        logger.log(level, '(txid %s) executing %s', transaction_id, statement)
    if trace:
        con.set_trace_callback(_trace_callback)

    logger.log(level, '(txid %s) starting transaction', transaction_id, stack_info=stack_info)

    start = time.time()
    try:
        with con:
            yield con
    finally:
        # record exception information, if an exception is active
        exc_info = sys.exc_info()
        if exc_info[0] is None:
            exc_info = None
        if trace:
            con.set_trace_callback(None)
        logger.log(level, '(txid %s) transaction closed after %.6f seconds', transaction_id, time.time() - start, exc_info=exc_info)

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

Я бы использовал его для любого кода, использующего соединение, поэтому вы также можете выбирать время:

with logged_transaction(connection):
    cursor = connection.cursor()
    # ...

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

Вы также можете использовать меньшее значение timeout в вызовах sqlite3.connect(), чтобы ускорить процесс; Вам, возможно, не придется ждать целых 5 минут, чтобы обнаружить ситуацию.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...