KRITISCH: 'database is locked' beim Aktivieren blockierte Online- UND Offline-Aktivierung (v1.2.1)
_process_activate() hielt beim Ersetzen einer bereits aktiven Lizenz fuer denselben Fingerprint (z.B. Trial durch Enterprise, oder schlicht eine Re-Aktivierung nach einem Systemwechsel) eine offene, noch nicht committete Schreibtransaktion auf 'conn', waehrend es innerhalb der Supersede-Schleife log_action_system() aufrief -- diese Funktion oeffnet INTERN eine EIGENE, zweite Verbindung zur selben Datenbank und versucht sofort zu committen. SQLite erlaubt aber nur einen Schreiber gleichzeitig (kein WAL-Modus): die zweite Verbindung wartet auf die erste, die wiederum auf die zweite wartet, bis das Busy-Timeout (Python-Default 5s) mit 'sqlite3.OperationalError: database is locked' aufgibt. Live reproduziert: sowohl /api/activate (Online) als auch /licenses/manual-code (Offline-Code) schlugen dadurch fehl, sobald ein Kunde eine Lizenz aktivierte, die eine fuer denselben Fingerprint bereits aktive ersetzt -- de facto war jede Aktivierung auf einem bereits bekannten System blockiert. Fix: die betroffenen log_action_system()-Aufrufe in der Supersede- Schleife werden nur noch vorgemerkt (reine Lesezugriffe auf die offene Verbindung sind unproblematisch) und erst NACH conn.commit()/ conn.close() nachgeholt. Zusaetzlich get_db_connection() auf timeout=15 (statt Python-Default 5s) angehoben als generelle Verteidigungslinie gegen kurze, legitime Ueberschneidungen -- ersetzt nicht die eigentliche Regel (niemals eine zweite Verbindung waehrend einer noch offenen Schreibtransaktion committen), ist aber zusaetzliche Sicherheit. Alle anderen log_action()/log_action_system()-Aufrufstellen in dieser Session (customers(), ticket_new/-edit/-delete, license_delete, customer_portal, _issue_license_from_ticket, _revoke_license_row, _process_deactivate, _process_heartbeat) wurden systematisch geprueft -- committen bereits korrekt VOR dem Logging, betroffen war ausschliesslich diese eine Stelle. Verifiziert: direkter Reproduktionstest gegen eine echte Kopie der Live-Datenbank (POETESTs echte aktive Lizenz wird durch eine zweite ersetzt, exakt das gemeldete Szenario) -- vorher deadlock/Timeout, nachher 0.04s, korrekt superseded + Hostname + Audit-Log-Eintraege. Als Notfall-Hotfix bereits direkt auf dem Live-Master eingespielt (Dienst neu gestartet, seither keine weiteren 'database is locked'- Fehler); dieser Commit bringt Repo/Release auf denselben Stand, damit 'latest' nicht wieder auf die kaputte v1.2.0 zurueckfaellt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
|||||||
1.2.0
|
1.2.1
|
||||||
|
|||||||
+25
-4
@@ -580,7 +580,14 @@ class User(UserMixin):
|
|||||||
|
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
conn = sqlite3.connect(DB_PATH)
|
# timeout=15 statt Python-Default 5s: reine Verteidigungslinie gegen
|
||||||
|
# kurze, legitime Ueberschneidungen zweier Schreibzugriffe (SQLite
|
||||||
|
# erlaubt nur EINEN Schreiber gleichzeitig) -- ersetzt NICHT die
|
||||||
|
# eigentliche Regel, niemals eine zweite Verbindung waehrend einer noch
|
||||||
|
# offenen Schreibtransaktion auf derselben zu committen (siehe
|
||||||
|
# _process_activate für ein Beispiel, wo genau das den Fehler
|
||||||
|
# "database is locked" ausgeloest hat).
|
||||||
|
conn = sqlite3.connect(DB_PATH, timeout=15)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
@@ -2202,19 +2209,33 @@ def _process_activate(data):
|
|||||||
"SELECT license_id, customer_id FROM licenses WHERE fingerprint=? AND status='active' AND license_id!=?",
|
"SELECT license_id, customer_id FROM licenses WHERE fingerprint=? AND status='active' AND license_id!=?",
|
||||||
(fingerprint, license_id),
|
(fingerprint, license_id),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
# log_action_system() oeffnet INTERN eine EIGENE, zweite Verbindung und
|
||||||
|
# committet sofort -- solange `conn` hier noch eine offene Schreib-
|
||||||
|
# transaktion haelt (erstes UPDATE oben, noch nicht committet), wuerde
|
||||||
|
# dieser zweite Commit-Versuch auf sich selbst warten (SQLite erlaubt
|
||||||
|
# nur EINEN Schreiber gleichzeitig) und nach Ablauf des Busy-Timeouts
|
||||||
|
# mit "database is locked" fehlschlagen -- live reproduziert beim
|
||||||
|
# Aktivieren einer Lizenz, die eine andere fuer denselben Fingerprint
|
||||||
|
# ersetzt. Deshalb werden die Log-Eintraege hier nur VORGEMERKT (reine
|
||||||
|
# Lesezugriffe auf `conn` sind unproblematisch, nur der schreibende
|
||||||
|
# log_action_system()-Aufruf selbst muss warten) und dann als allerletzter
|
||||||
|
# Schritt nach `conn.commit()`/`conn.close()` unten nachgeholt.
|
||||||
|
superseded_logs = []
|
||||||
for old in superseded:
|
for old in superseded:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE licenses SET status='deactivated', deactivated_at=? WHERE license_id=?",
|
"UPDATE licenses SET status='deactivated', deactivated_at=? WHERE license_id=?",
|
||||||
(now, old["license_id"]),
|
(now, old["license_id"]),
|
||||||
)
|
)
|
||||||
old_customer = _customer_row(conn, old["customer_id"])
|
old_customer = _customer_row(conn, old["customer_id"])
|
||||||
log_action_system(
|
superseded_logs.append((
|
||||||
"license.superseded", old_customer["name"] if old_customer else old["license_id"],
|
old_customer["name"] if old_customer else old["license_id"],
|
||||||
f"Automatisch deaktiviert -- Fingerprint {fingerprint} hat jetzt Lizenz {license_id} aktiviert.",
|
f"Automatisch deaktiviert -- Fingerprint {fingerprint} hat jetzt Lizenz {license_id} aktiviert.",
|
||||||
)
|
))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
customer = _customer_row(conn, row["customer_id"])
|
customer = _customer_row(conn, row["customer_id"])
|
||||||
conn.close()
|
conn.close()
|
||||||
|
for target, details in superseded_logs:
|
||||||
|
log_action_system("license.superseded", target, details)
|
||||||
log_action_system("license.activated_remote", customer["name"] if customer else license_id, f"Fingerprint {fingerprint}")
|
log_action_system("license.activated_remote", customer["name"] if customer else license_id, f"Fingerprint {fingerprint}")
|
||||||
return licensing.build_master_response("activated", license_id, fingerprint, MASTER_PRIVATE_KEY, status="ok"), 200
|
return licensing.build_master_response("activated", license_id, fingerprint, MASTER_PRIVATE_KEY, status="ok"), 200
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user