| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- <?php
- /**
- * Background archive worker.
- *
- * Shared hosting has no dependable cron, so gallery archives are rebuilt by a
- * chain of short requests instead: a page render calls archive_kick(), which
- * fires one request at this script and hangs up; this script does a slice of
- * work and then dispatches its own successor. One guest upload therefore starts
- * a chain that runs to completion with no further visitors.
- *
- * Each request does exactly one of two things, never both, so it cannot
- * approach max_execution_time:
- *
- * - a gallery is due → run one slice (archive.step_seconds), then dispatch
- * - only unsettled → sleep out the rest of the settle window (capped at
- * entries remain 30 s), then dispatch
- *
- * The site-wide lock is held across the whole body, including the sleep. That
- * is what keeps the chain single: a worker that cannot take the lock exits
- * without dispatching, because the worker that holds it will dispatch the next
- * one itself. The chain also stops as soon as the queue is empty, so it can
- * never become a perpetual heartbeat, and archive.max_chain bounds it even if
- * something goes wrong.
- *
- * Authenticated by the key in data/worker-key.json: the caller is this server
- * making an HTTP request to itself, so there is no admin session to check. A
- * wrong or missing key is indistinguishable from the script not existing.
- *
- * If the host offers real cron, calling this URL every few minutes works just as
- * well and needs no code change (see docs/SETUP.md).
- */
- require __DIR__ . '/app/bootstrap.php';
- if (!hash_equals(archive_worker_key(), (string)($_GET['key'] ?? ''))) {
- http_response_code(404);
- exit;
- }
- // The dispatcher hung up after a fraction of a second. Without this, PHP would
- // kill this process the moment it noticed the disconnect.
- ignore_user_abort(true);
- @set_time_limit(0); // honoured on some hosts; the design never relies on it.
- // Nothing is ever read from the response — the caller is not listening.
- http_response_code(204);
- $lock = archive_lock();
- if ($lock === null) {
- exit; // another worker owns the chain and will dispatch its successor
- }
- $chain = json_update(archive_queue_file(), function (array $queue): array {
- $queue['chain'] = (int)($queue['chain'] ?? 0) + 1;
- return $queue;
- });
- if ((int)($chain['chain'] ?? 0) > (int)config('archive.max_chain', 500)) {
- exit; // runaway guard; the next page view starts a fresh chain
- }
- $wait = archive_queue_wait();
- if ($wait === null) {
- // Queue empty: the chain ends here, and its counter resets with it.
- json_update(archive_queue_file(), function (array $queue): array {
- $queue['chain'] = 0;
- return $queue;
- });
- exit;
- }
- if ($wait > 0) {
- // Nothing has settled yet. Waiting here rather than exiting is what lets a
- // gallery nobody is looking at still rebuild on its own.
- //
- // Kept short: this holds both a PHP process and the worker lock, and an
- // admin clicking "Rebuild now" has to wait it out. A settle window is
- // bridged by a chain of these short waits instead of one long one.
- sleep(min($wait, 15));
- } else {
- $slug = archive_next_due();
- if ($slug !== null) {
- archive_run_slice($slug);
- }
- }
- // Release before handing off, so the successor can start immediately.
- flock($lock, LOCK_UN);
- fclose($lock);
- if (archive_queue_wait() !== null) {
- archive_dispatch(2000);
- }
|