further enhanced error handling + added retries in case of parallel writes

This commit is contained in:
2026-06-03 17:27:51 +02:00
parent d0daa7e937
commit fc84d55bd6
21 changed files with 184 additions and 383 deletions

View File

@ -140,18 +140,37 @@ function qdb_apply_migrations(PDO $pdo, string $schemaSql): bool {
return $changed;
}
function qdb_acquire_lock() {
/**
* Exclusive lock with short retries (non-blocking attempts). Avoids hung PHP workers
* when two writes overlap (app upload + website edit).
*/
function qdb_flock_exclusive_with_retry($lockFp, int $maxAttempts = 12, int $sleepMicros = 75000): void
{
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
if (flock($lockFp, LOCK_EX | LOCK_NB)) {
return;
}
if ($attempt < $maxAttempts - 1) {
usleep($sleepMicros);
}
}
throw new Exception('Could not acquire lock');
}
function qdb_open_writable_lock()
{
$lockFp = fopen(QDB_LOCK, 'c');
if ($lockFp === false) {
throw new Exception('Could not open lock file');
}
if (!flock($lockFp, LOCK_EX)) {
fclose($lockFp);
throw new Exception('Could not acquire lock');
}
qdb_flock_exclusive_with_retry($lockFp);
return $lockFp;
}
function qdb_acquire_lock() {
return qdb_open_writable_lock();
}
/**
* Decrypt the master DB file into a temp SQLite file, or create a fresh one
* from schema.sql if no DB exists yet.
@ -171,12 +190,7 @@ function qdb_open(bool $writable = false): array {
$lockFp = null;
if ($writable) {
$lockFp = fopen(QDB_LOCK, 'c');
if ($lockFp === false) throw new Exception("Could not open lock file");
if (!flock($lockFp, LOCK_EX)) {
fclose($lockFp);
throw new Exception("Could not acquire lock");
}
$lockFp = qdb_open_writable_lock();
}
$tmpDb = tempnam(sys_get_temp_dir(), 'qdb_');
@ -289,33 +303,26 @@ function qdb_discard(string $tmpDb, $lockFp): void {
*
* @return array{0: PDO, 1: string, 2: resource|null}|null
*/
function qdb_open_read_or_fail(): ?array {
/**
* @return array{0: PDO, 1: string, 2: resource|null}
*/
function qdb_open_read_or_fail(): array {
try {
return qdb_open(false);
} catch (Throwable $e) {
if (function_exists('qdb_api_log_note_exception')) {
qdb_api_log_note_exception($e);
}
require_once __DIR__ . '/lib/errors.php';
error_log('qdb_open_read: ' . $e->getMessage());
json_error('SERVER_ERROR', qdb_public_error_message($e, 'Database read'), 500);
qdb_emit_api_error($e, 'Database read');
}
}
/**
* Open a writable DB connection; on failure logs and returns a JSON error response.
*
* @return array{0: PDO, 1: string, 2: resource|null}|null
* @return array{0: PDO, 1: string, 2: resource|null}
*/
function qdb_open_write_or_fail(): ?array {
function qdb_open_write_or_fail(): array {
try {
return qdb_open(true);
} catch (Throwable $e) {
if (function_exists('qdb_api_log_note_exception')) {
qdb_api_log_note_exception($e);
}
require_once __DIR__ . '/lib/errors.php';
error_log('qdb_open_write: ' . $e->getMessage());
json_error('SERVER_ERROR', qdb_public_error_message($e, 'Database write'), 500);
qdb_emit_api_error($e, 'Database write');
}
}