python sqlite3表无故消失

1u4esq0p  于 2023-03-30  发布在  SQLite
关注(0)|答案(1)|浏览(368)

我有一个python sqlite3 db,其中有一个表没有明显的原因“消失”-我猜它被删除了。数据库文件仍然存在,并且似乎没有“损坏”:我可以通过CLI连接到它并在上面执行基本操作:

sqlite> SELECT name FROM sqlite_schema WHERE type='table' ORDER BY name;

到目前为止,它只发生在dev中,并且只在重启后几天发生。dev服务器上的流量很少,主要是我测试应用程序。

我欢迎任何关于如何调试此问题的想法。在日志中没有指向表删除的 * 原因 * 的错误。我已经添加了一些详细的日志记录,但我看不到任何导致表删除的情况。只是,在某个时候,它消失了,我开始收到no such table错误:

# Everything looking fine....
2023-03-07 17:14:43,039 139982591223616 - application INFO - Updating hash value for PID: 12933
2023-03-07 17:14:43,039 139982591223616 - application INFO - Connecting to: /some_path/mydatabase.db
2023-03-07 17:14:43,047 139982591223616 - application INFO - Connecting to: /some_path/mydatabase.db
2023-03-07 17:14:43,063 139982591223616 - application INFO - Got 7 cache items. This is 7 more than the previous set.
2023-03-07 17:14:43,064 139982591223616 - application INFO - Connecting to: /some_path/mydatabase.db
2023-03-07 17:14:43,072 139982591223616 - application INFO -
MONOMERS STATUS
(pid=12932)  cache_data_hash=d49e11e052832ed7de03f38fa61c09cabdae66473991ae3e9d02041f019983ae
(pid=12933)  cache_data_hash=d49e11e052832ed7de03f38fa61c09cabdae66473991ae3e9d02041f019983ae *****

2023-03-07 17:14:43,072 139982591223616 - uvicorn.error INFO - Application startup complete.
2023-03-07 17:14:43,072 139982591223616 - uvicorn.error INFO - Uvicorn running on socket ('127.0.0.1', 8200) (Press CTRL+C to quit)

# Then, the next day, suddenly...
2023-03-08 15:46:10,190 140122358679360 - application INFO - Connecting to: /some_path/mydatabase.db
2023-03-08 15:46:10,733 140122358679360 - application ERROR - Traceback (most recent call last):
  File "/some_path/my_app/web_service.py", line 74, in whitelist_authentication_and_log_all_exceptions
    if should_update_cache_data():
  File "/some_path/my_app/operations/update_cache_data.py", line 80, in should_update_cache_data
    return _should_update_cache_data(os.getpid())
  File "/some_path/my_app/operations/update_cache_data.py", line 84, in _should_update_cache_data
    cache_data_hash: Union[str, None] = _get_hash_for_pid(pid)
  File "/some_path/my_app/operations/update_cache_data.py", line 94, in _get_hash_for_pid
    cursor.execute(f"select * from {TABLE_NAME} where pid=?", (pid,))
sqlite3.OperationalError: no such table: MY_CACHE_TABLE

我最好的猜测是,我的数据库访问代码中的某些东西,可能与并发问题(多进程)结合在一起,不知何故破坏了某些东西-未关闭的游标/连接,竞争条件......我已经尽我所能仔细检查了代码。
数据库用于跨多个uvicorn进程的内存缓存同步。PID和缓存数据的散列。当我收到缓存更新通知时,我更新当前进程的缓存,然后通过将PID的散列设置为空字符串来使所有其他进程的缓存无效。每个进程轮询表,如果它发现其PID为空字符串,则更新缓存并设置新的散列。

PID     HASH
2385    d49e11e052832ed7de03f38fa61c09cabdae66473991ae3e9d02041f019983ae
9823    d49e11e052832ed7de03f38fa61c09cabdae66473991ae3e9d02041f019983ae

我在部署期间每次都重新创建数据库(我从不在运行时调用create_db()create_table()),并在启动时更新该高速缓存和表--不需要在部署/重新启动期间保存任何数据。

def create_db():
    create_table()

def get_database_connection() -> sqlite3.Connection:
    try:
        connection = sqlite3.connect(settings.DB_FILE_NAME)
    except sqlite3.Error as exc:
        raise RuntimeError(f"Cannot connect to {os.path.abspath(settings.DB_FILE_NAME)}") from exc

    return connection

def create_table() -> None:
    """ Establishing a connection creates the db if it doesn't exist. """
    database_connection: sqlite3.Connection = get_database_connection()
    cursor: sqlite3.Cursor

    try:
        cursor = database_connection.cursor()
        cursor.execute(f"DROP TABLE IF EXISTS {TABLE_NAME}")
        cursor.execute(
            f"CREATE TABLE {TABLE_NAME} (PID INTEGER, CACHE_HASH TEXT)")
    except sqlite3.Error as err:
        raise RuntimeError("Error creating database.") from err
    finally:
        cursor.close()
        database_connection.commit()
        database_connection.close()

if __name__ == "__main__":
    create_db()

这里是所有涉及数据库的其他代码。基本上,我尽可能使用上下文管理器(with),否则尝试确保我正确提交和关闭所有内容:

PID = 0
CACHE_DATA_HASH = 1

async def update_cache_data_and_invalidate_cache():
    result_message: dict[str, str] = await update_cache_data()
    invalidate_cache_for_all_other_processes()
    _log_cache_data_info()
    return result_message

async def update_cache_data() -> dict[str, str]:

    cache_data = await _get_cache_data()
    thin_cache_data: CacheData = CacheData(cache_data)
    cache_data_refresh_diff = len(thin_cache_data.items) - len(settings.cache_data.items)
    if len(thin_cache_data.items) > 0:
        settings.cache_data = thin_cache_data
        set_cache_data_up_to_date()
        return {"message": f"Successfully received {len(thin_cache_data.items)} items."}

    error_message: str = ("ERROR: items refresh request did not error, but we received 0 items.")
    logger.error(error_message)
    _log_cache_data_info()
    raise RuntimeError(error_message)

async def _get_cache_data() -> list[dict]:
    async with httpx.AsyncClient(verify=False) as client:
        response = await client.get(cache_data_url, timeout=15.0)
        cache_data: list[dict] = response.json()
        return cache_data

def should_update_cache_data() -> bool:
    return _should_update_cache_data(os.getpid())

def _should_update_cache_data(pid: int) -> bool:
    cache_data_hash: Union[str, None] = _get_hash_for_pid(pid, include_connection_logging=False)
    if cache_data_hash is None or cache_data_hash == '':
        return True
    return False

def _get_hash_for_pid(pid: int, include_connection_logging=True) -> Union[str, None]:
    cache_data_hash: Union[str, None] = None
    with get_database_connection(include_logging=include_connection_logging) as conn:
        cursor: sqlite3.Cursor = conn.cursor()
        cursor.execute(f"select * from {TABLE_NAME} where pid=?", (pid,))
        result: Union[tuple, None] = cursor.fetchone()
        cursor.close()

        if result is not None:
            cache_data_hash = result[CACHE_DATA_HASH]
    return cache_data_hash

def set_cache_data_up_to_date() -> None:
    current_pid: int = os.getpid()
    _set_cache_data_up_to_date(current_pid)

def _set_cache_data_up_to_date(pid: int) -> None:
    cache_data_hash: Union[str, None] = _get_hash_for_pid(pid)
    with get_database_connection() as conn:
        if cache_data_hash is None:
            conn.execute(f"insert into {TABLE_NAME} values (?, ?)", (pid, settings.cache_data.hash))
        else:
            conn.execute(
                f"update {TABLE_NAME} set cache_data_hash = ? where pid = ?", (settings.cache_data.hash, pid))

def invalidate_cache_for_all_other_processes() -> None:
    with get_database_connection() as conn:
        process_ids = []
        for row in conn.execute(f"select * from {TABLE_NAME}"):
            process_ids.append(row[PID])

        # Invalidate the cache for all other processes by setting the hash to an empty string
        this_process_pid: int = os.getpid()
        for pid in process_ids:
            if pid != this_process_pid:
                conn.execute(f"update {TABLE_NAME} set cache_data_hash = ? where pid = ?", ('', pid))

def _generate_cache_data_info() -> str:
    cache_data_info: str = "\nCACHE STATUS\n"
    got_rows: bool = False
    with get_database_connection() as conn:
        for row in conn.execute(f"select * from {TABLE_NAME}"):
            got_rows = True
            if row[PID] == os.getpid():
                cache_data_info += f"(pid={row[PID]})  cache_data_hash={row[CACHE_DATA_HASH]} *****\n"
            else:
                cache_data_info += f"(pid={row[PID]})  cache_data_hash={row[CACHE_DATA_HASH]}\n"
    if not got_rows:
        cache_data_info += f"{TABLE_NAME} is empty."
    return cache_data_info

def _log_cache_data_info() -> None:
    logger.info(_generate_cache_data_info())
lawou6xi

lawou6xi1#

这原来是因为SQLite db文件在NFS文件系统上。我不确定db文件到底发生了什么,但它因为并发访问而“损坏”:
SQLite使用读取器/写入器锁来控制对数据库的访问....但请注意:如果数据库文件保存在NFS文件系统上,则此锁定机制可能无法正常工作。
https://www.sqlite.org/faq.html#q5

相关问题