Phase 17: run the migration, and fix the guard that locked out its own backup
Claire's writing — 8 documents, 33 snapshots, 103 suggestions, 3 vocabulary words and an image — now belongs to her account rather than to the pre-auth 'local' user, and the VPS is canonical. millenia was left running and untouched as a frozen fallback; it diverges the moment either side is written to, so it wants retiring rather than syncing. The plan's stated prerequisite, that she log in once so her subject exists, turned out to be false. Authentik's hashed_user_id sub is the user's uid, derived from her id and the instance secret, so it can be read in advance — which means the data moves first and she signs in to find her writing already there, instead of to an empty Petal that fills in later. The fix here is to the liveness guard, and it is the second attempt at it. PRAGMA locking_mode = EXCLUSIVE goes on holding its lock after being set back to NORMAL — SQLite only lets go on that connection's next database access — so against a real WAL database the script locked itself out of its own VACUUM INTO backup. It passed locally because the test database had come out of VACUUM INTO and so was never in WAL mode: the fixture didn't look like production, the same way the stub identity provider's slashless issuer didn't. The probe now runs on its own connection and closes it. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
@@ -83,7 +83,7 @@ def counts(conn: sqlite3.Connection, user_id: str) -> dict[str, int]:
|
||||
return out
|
||||
|
||||
|
||||
def require_app_stopped(conn: sqlite3.Connection, path: str) -> None:
|
||||
def require_app_stopped(path: str) -> None:
|
||||
"""Fail unless nothing else has the database open.
|
||||
|
||||
`BEGIN EXCLUSIVE` is not enough, and quietly so: in WAL mode it only
|
||||
@@ -94,11 +94,18 @@ def require_app_stopped(conn: sqlite3.Connection, path: str) -> None:
|
||||
|
||||
Checking for a `-wal` file would be no use either: WAL mode leaves one
|
||||
behind whether or not anything is running.
|
||||
|
||||
This runs on its own connection, which is then closed. Setting locking_mode
|
||||
back to NORMAL does not release the lock straight away — SQLite drops it on
|
||||
the connection's next database access, which on a WAL database can leave the
|
||||
file locked against everything else this script is about to do. Closing is
|
||||
the only unambiguous way to let go of it.
|
||||
"""
|
||||
probe = sqlite3.connect(path, isolation_level=None)
|
||||
try:
|
||||
conn.execute("PRAGMA locking_mode = EXCLUSIVE")
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
conn.execute("COMMIT")
|
||||
probe.execute("PRAGMA locking_mode = EXCLUSIVE")
|
||||
probe.execute("BEGIN IMMEDIATE")
|
||||
probe.execute("COMMIT")
|
||||
except sqlite3.OperationalError as err:
|
||||
die(
|
||||
f"{path} is in use ({err}). Stop Petal first:\n"
|
||||
@@ -106,9 +113,7 @@ def require_app_stopped(conn: sqlite3.Connection, path: str) -> None:
|
||||
" systemctl --user stop petal # millenia"
|
||||
)
|
||||
finally:
|
||||
# Back to normal locking so the rest of the run behaves like any other
|
||||
# client, and so a dry run leaves the file exactly as it found it.
|
||||
conn.execute("PRAGMA locking_mode = NORMAL")
|
||||
probe.close()
|
||||
|
||||
|
||||
def backup(path: str) -> str:
|
||||
@@ -140,8 +145,8 @@ def main() -> None:
|
||||
if args.to == args.source:
|
||||
die("source and destination are the same account")
|
||||
|
||||
require_app_stopped(args.database)
|
||||
conn = open_db(args.database)
|
||||
require_app_stopped(conn, args.database)
|
||||
|
||||
src_counts = counts(conn, args.source)
|
||||
if not any(src_counts.values()):
|
||||
|
||||
Reference in New Issue
Block a user