From fd209e705e5c4c1cb5b7be01e430f49bf1e70675 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 02:58:13 +0200 Subject: [PATCH 01/10] feat(plugin): add POST /cdcf/v1/maintenance endpoint Sets/clears cdcf:maintenance:until in Redis with a TTL clamped to [60, 600] seconds. The cdcf-queue-worker will check this key at the top of each poll cycle to pause processing during deploys (#62). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cdcf-redis-translations.php | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php b/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php index 6036da3..5ce4658 100644 --- a/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php +++ b/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php @@ -40,4 +40,69 @@ 'batch_size' => ['required' => false, 'type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint'], ], ]); + + register_rest_route('cdcf/v1', '/maintenance', [ + 'methods' => 'POST', + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + 'callback' => function (WP_REST_Request $request) { + $action = $request['action'] ?? ''; + if ($action !== 'begin' && $action !== 'end') { + return new WP_Error( + 'invalid_action', + "action must be 'begin' or 'end'", + ['status' => 400] + ); + } + + if (!class_exists('Redis')) { + return new WP_Error( + 'redis_unavailable', + 'PHP Redis extension not installed', + ['status' => 500] + ); + } + + try { + $redis = new Redis(); + if (!$redis->connect('127.0.0.1', 6379, 1.0)) { + return new WP_Error( + 'redis_unavailable', + 'Could not connect to Redis at 127.0.0.1:6379', + ['status' => 500] + ); + } + } catch (\Throwable $e) { + return new WP_Error('redis_unavailable', $e->getMessage(), ['status' => 500]); + } + + if ($action === 'end') { + $redis->del('cdcf:maintenance:until'); + return new WP_REST_Response(['ok' => true], 200); + } + + // action === 'begin' + $duration = intval($request['duration_seconds'] ?? 300); + $duration = max(60, min(600, $duration)); + $redis->setex('cdcf:maintenance:until', $duration, '1'); + return new WP_REST_Response([ + 'ok' => true, + 'until' => time() + $duration, + 'duration' => $duration, + ], 200); + }, + 'args' => [ + 'action' => [ + 'required' => true, + 'type' => 'string', + ], + 'duration_seconds' => [ + 'required' => false, + 'type' => 'integer', + 'default' => 300, + 'sanitize_callback' => 'absint', + ], + ], + ]); }); From 170299da0253d9c14459006f9877721f66f84cc4 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 03:03:17 +0200 Subject: [PATCH 02/10] feat(worker): pause processing while cdcf:maintenance:until is set Adds an in_maintenance() helper that checks Redis at the top of each poll cycle. Logs a single transition line on entry and exit, not per cycle. Treats redis-cli failures as "not in maintenance" so a Redis outage doesn't stall the worker (#62). Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/cdcf_queue_worker.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/cdcf_queue_worker.sh b/scripts/cdcf_queue_worker.sh index bd0bfef..2d5f996 100755 --- a/scripts/cdcf_queue_worker.sh +++ b/scripts/cdcf_queue_worker.sh @@ -133,6 +133,16 @@ except: fi } +# ─── Maintenance flag check ────────────────────────────────────────── +# Returns 0 (true) if the maintenance flag is set in Redis, 1 (false) +# otherwise. Redis-unreachable counts as "not in maintenance" so a +# Redis outage does not stall the worker indefinitely. +in_maintenance() { + local result + result=$(redis-cli -h 127.0.0.1 -p 6379 -n 0 EXISTS cdcf:maintenance:until 2>/dev/null) || return 1 + [ "$result" = "1" ] +} + # ─── Queue processing ──────────────────────────────────────────────── # process_one fires a single REST call and logs the result. @@ -187,7 +197,22 @@ except: fi } +IN_MAINTENANCE=0 while true; do + if in_maintenance; then + if [ "$IN_MAINTENANCE" = "0" ]; then + echo "$(date -Iseconds) Entering maintenance mode (worker paused)" + IN_MAINTENANCE=1 + fi + sleep "${POLL_INTERVAL}" + continue + fi + + if [ "$IN_MAINTENANCE" = "1" ]; then + echo "$(date -Iseconds) Exiting maintenance mode (worker resumed)" + IN_MAINTENANCE=0 + fi + run_daily_tasks if [ "$CONCURRENCY" -le 1 ]; then From ee44f6617995c80b3012467bb857d4a3a02ab1fd Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 03:12:45 +0200 Subject: [PATCH 03/10] ci(deploy): pause queue worker around production WP-touching steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls POST /cdcf/v1/maintenance{begin,300} before tarball extraction and {end} after plugin activation (with if: always()). If begin fails, abort the deploy — the whole point is to mitigate FPM stress, so deploying without the pause defeats the purpose. End failures emit a warning; the Redis TTL self-heals within 600s (#62). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/deploy.yml | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60a13bf..5584728 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -295,6 +295,35 @@ jobs: echo "All SSH attempts failed" exit 1 + - name: Pause queue worker for deploy + if: env.ENVIRONMENT == 'production' + # When staging gets its own WP backend, also gate this step on + # env.ENVIRONMENT == 'staging' and route to the staging WP REST URL. + # Today staging shares the production backend, so only production + # deploys touch FPM hard enough to need the pause. + env: + WP_REST_URL: ${{ vars.WP_REST_URL }} + WP_APP_USERNAME: ${{ secrets.WP_APP_USERNAME }} + WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }} + run: | + AUTH=$(echo -n "$WP_APP_USERNAME:$WP_APP_PASSWORD" | base64) + for attempt in 1 2 3; do + STATUS=$(curl -sS -o /tmp/maint-begin.json -w '%{http_code}' \ + --connect-timeout 10 --max-time 30 \ + -X POST "$WP_REST_URL/cdcf/v1/maintenance" \ + -H "Authorization: Basic $AUTH" \ + -H "Content-Type: application/json" \ + -d '{"action":"begin","duration_seconds":300}') + if [ "$STATUS" = "200" ]; then + echo "Worker paused: $(cat /tmp/maint-begin.json)" + exit 0 + fi + echo "Maintenance begin attempt $attempt failed (HTTP $STATUS)" + [ "$attempt" -lt 3 ] && sleep 5 + done + echo "::error::Failed to pause queue worker; aborting deploy." + exit 1 + - name: Extract WP theme and plugin bundles if: env.ENVIRONMENT == 'production' env: @@ -381,3 +410,29 @@ jobs: echo "One or more plugin activations failed; failing the deploy." exit 1 fi + + - name: Resume queue worker after deploy + if: always() && env.ENVIRONMENT == 'production' + env: + WP_REST_URL: ${{ vars.WP_REST_URL }} + WP_APP_USERNAME: ${{ secrets.WP_APP_USERNAME }} + WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }} + run: | + AUTH=$(echo -n "$WP_APP_USERNAME:$WP_APP_PASSWORD" | base64) + for attempt in 1 2 3; do + STATUS=$(curl -sS -o /dev/null -w '%{http_code}' \ + --connect-timeout 10 --max-time 30 \ + -X POST "$WP_REST_URL/cdcf/v1/maintenance" \ + -H "Authorization: Basic $AUTH" \ + -H "Content-Type: application/json" \ + -d '{"action":"end"}') + if [ "$STATUS" = "200" ]; then + echo "Worker resumed." + exit 0 + fi + echo "Maintenance end attempt $attempt failed (HTTP $STATUS)" + [ "$attempt" -lt 3 ] && sleep 5 + done + # Don't exit 1 here — the TTL self-heals within 600s and failing + # this step would mask the real outcome of the deploy. + echo "::warning::Failed to resume queue worker; will self-heal via TTL within 600s." From ecfb1f48e4fecac722e47a8b3444bb7b6e1eed55 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 03:21:32 +0200 Subject: [PATCH 04/10] feat(cli): add maintenance subcommand to cdcf_api.py Wraps POST /cdcf/v1/maintenance for ergonomic admin use: scripts/.venv/bin/python scripts/cdcf_api.py maintenance --action begin --duration 300 scripts/.venv/bin/python scripts/cdcf_api.py maintenance --action end Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/cdcf_api.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/scripts/cdcf_api.py b/scripts/cdcf_api.py index 156e01e..c69cc96 100644 --- a/scripts/cdcf_api.py +++ b/scripts/cdcf_api.py @@ -355,6 +355,22 @@ def revalidate(self, path: str | None = None, tags: list[str] | None = None) -> resp.raise_for_status() return resp.json() + def maintenance(self, action: str, duration_seconds: int = 300) -> dict: + """Set or clear the deploy-time maintenance flag. + + action: 'begin' (sets cdcf:maintenance:until in Redis with a + clamped TTL) or 'end' (deletes the key). + duration_seconds: only used for 'begin'. Server clamps to [60, 600]. + + Returns the endpoint response dict. + """ + if action not in ("begin", "end"): + raise ValueError(f"action must be 'begin' or 'end', got {action!r}") + payload: dict = {"action": action} + if action == "begin": + payload["duration_seconds"] = int(duration_seconds) + return self._wp_post("cdcf/v1/maintenance", payload) + # --------------------------------------------------------------------------- # CLI @@ -472,6 +488,14 @@ def _build_parser() -> argparse.ArgumentParser: p.add_argument("--path", help="Path to revalidate") p.add_argument("--tags", nargs="*", help="Cache tags to revalidate") + # maintenance + p = sub.add_parser("maintenance", + help="Pause/resume the cdcf-queue-worker via Redis flag") + p.add_argument("--action", required=True, choices=["begin", "end"]) + p.add_argument("--duration", type=int, default=300, + help="Seconds to pause for (clamped server-side to 60-600). " + "Only used with --action begin. Default: 300") + # -- Post Meta / ACF Fields -- # get-post @@ -628,6 +652,9 @@ def _run_cli(args: argparse.Namespace, client: CdcfClient) -> dict: if cmd == "revalidate": return client.revalidate(path=args.path, tags=args.tags) + if cmd == "maintenance": + return client.maintenance(args.action, args.duration) + if cmd == "get-post": return client.get_post(args.post_id, args.post_type) From cb19147c70357165e421db0b95d66eb9df399441 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 03:25:43 +0200 Subject: [PATCH 05/10] chore(cli): clarify maintenance docstring and add section comment Addresses code-review feedback: surface that argparse handles action validation at the CLI layer (so the ValueError is for programmatic callers), and add the missing # -- Maintenance Flag -- section comment that every other method group in CdcfClient has. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/cdcf_api.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/cdcf_api.py b/scripts/cdcf_api.py index c69cc96..5f4a05f 100644 --- a/scripts/cdcf_api.py +++ b/scripts/cdcf_api.py @@ -355,12 +355,16 @@ def revalidate(self, path: str | None = None, tags: list[str] | None = None) -> resp.raise_for_status() return resp.json() + # -- Maintenance Flag -- + def maintenance(self, action: str, duration_seconds: int = 300) -> dict: - """Set or clear the deploy-time maintenance flag. + """POST /cdcf/v1/maintenance — set or clear the deploy-time maintenance flag. - action: 'begin' (sets cdcf:maintenance:until in Redis with a - clamped TTL) or 'end' (deletes the key). - duration_seconds: only used for 'begin'. Server clamps to [60, 600]. + action: 'begin' (sets cdcf:maintenance:until in Redis with a clamped + TTL) or 'end' (deletes the key). Validated by argparse at the + CLI layer; ValueError raised for invalid values when called + programmatically. + duration_seconds: only sent for 'begin'. Server clamps to [60, 600]. Returns the endpoint response dict. """ From 608b33d7de710d5133e5a3e47c2ac363c0ccbfbc Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 03:30:04 +0200 Subject: [PATCH 06/10] docs: maintenance mode for the queue worker Documents the new POST /cdcf/v1/maintenance endpoint, its redis-cli fallback, the Python CLI subcommand, and the expected worker log output during a deploy (#62). Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 5 ++++ docs/redis-queue-worker.md | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ed3076e..af83246 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,7 @@ All endpoints require Application Password authentication (`edit_posts` capabili | `POST` | `/team-member` | Create a team member with auto-translation and About page linking (see below) | | `POST` | `/community-channel` | Create a community channel with auto-translation and Community page linking (see below) | | `POST` | `/local-group` | Create a local group with auto-translation and Community page linking (see below) | +| `POST` | `/maintenance` | Pause or resume the cdcf-queue-worker by setting/clearing a Redis flag (`{action: "begin"|"end", duration_seconds?: 60-600}`) | | `POST` | `/academic-collaboration` | Create an academic collaboration with auto-translation and Community page linking (see below) | ### `POST /team-member` @@ -169,6 +170,10 @@ scripts/.venv/bin/python scripts/cdcf_api.py rest-get wp/v2/posts --params '{"pe # Cache revalidation scripts/.venv/bin/python scripts/cdcf_api.py revalidate --path /about + +# Pause/resume the queue worker (used by the deploy workflow) +scripts/.venv/bin/python scripts/cdcf_api.py maintenance --action begin --duration 300 +scripts/.venv/bin/python scripts/cdcf_api.py maintenance --action end ``` See `docs/python-api-client.md` for full documentation of all commands and library usage. diff --git a/docs/redis-queue-worker.md b/docs/redis-queue-worker.md index f4cef5f..93f5d82 100644 --- a/docs/redis-queue-worker.md +++ b/docs/redis-queue-worker.md @@ -147,6 +147,63 @@ sudo chmod +x /usr/local/bin/cdcf_queue_worker.sh sudo systemctl restart cdcf-queue-worker ``` +## Maintenance mode + +The worker can be paused via a Redis flag. While the flag is set, the worker skips both `process_one` and `run_daily_tasks` and just sleeps `POLL_INTERVAL` seconds per cycle. This is used by the production deploy workflow to prevent the worker's parallel POSTs from competing with deploy-time WP traffic for FPM workers. + +### Setting and clearing the flag + +Via the WP REST API (the way the deploy workflow does it): + +```bash +# Pause for 300 seconds +curl -u "$WP_APP_USERNAME:$WP_APP_PASSWORD" \ + -X POST "$WP_REST_URL/cdcf/v1/maintenance" \ + -H "Content-Type: application/json" \ + -d '{"action":"begin","duration_seconds":300}' + +# Resume +curl -u "$WP_APP_USERNAME:$WP_APP_PASSWORD" \ + -X POST "$WP_REST_URL/cdcf/v1/maintenance" \ + -H "Content-Type: application/json" \ + -d '{"action":"end"}' +``` + +Or via the Python CLI: + +```bash +scripts/.venv/bin/python scripts/cdcf_api.py maintenance --action begin --duration 300 +scripts/.venv/bin/python scripts/cdcf_api.py maintenance --action end +``` + +Or directly via `redis-cli` on the VPS (operator-only): + +```bash +redis-cli SETEX cdcf:maintenance:until 300 1 +redis-cli DEL cdcf:maintenance:until +``` + +The TTL is server-clamped to `[60, 600]` seconds. If `end` is never called, the flag self-expires within ≤600 seconds. + +### Expected log output + +The worker logs exactly one line per transition, never per cycle: + +``` +2026-05-02T12:00:00+00:00 Entering maintenance mode (worker paused) +2026-05-02T12:02:30+00:00 Exiting maintenance mode (worker resumed) +2026-05-02T12:02:45+00:00 Processed 3 job(s) +``` + +### Verifying during a deploy + +```bash +journalctl -u cdcf-queue-worker -f +# Expect one "Entering" then one "Exiting" line bracketing the deploy. +# Compare 504 counts in the WP access log before/after the deploy day +# to baseline (~10/day; bad days hit 200+). +``` + ## Troubleshooting ### Worker logs show "WARNING: unexpected response" From 432deff26c62eb902ee4d7e323f16dfba884384d Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 03:39:18 +0200 Subject: [PATCH 07/10] docs(rest-table): clarify maintenance row capability and duration Addresses doc-review feedback on the new /maintenance row: - Note that this endpoint requires manage_options (administrator), not the table preamble's default edit_posts. An editor's Application Password would get 403 with no obvious explanation. - Clarify that the 60-600 range is server-side clamping, not a client-side validation requirement. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index af83246..3cb10cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ All endpoints require Application Password authentication (`edit_posts` capabili | `POST` | `/team-member` | Create a team member with auto-translation and About page linking (see below) | | `POST` | `/community-channel` | Create a community channel with auto-translation and Community page linking (see below) | | `POST` | `/local-group` | Create a local group with auto-translation and Community page linking (see below) | -| `POST` | `/maintenance` | Pause or resume the cdcf-queue-worker by setting/clearing a Redis flag (`{action: "begin"|"end", duration_seconds?: 60-600}`) | +| `POST` | `/maintenance` | Pause or resume the cdcf-queue-worker by setting/clearing a Redis flag (`{action: "begin"|"end", duration_seconds?: int}` — server-clamps duration to 60–600). Requires administrator (`manage_options`) capability. | | `POST` | `/academic-collaboration` | Create an academic collaboration with auto-translation and Community page linking (see below) | ### `POST /team-member` From cc6052277894b28ab4e1e3429075c2ba20e49541 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 04:16:33 +0200 Subject: [PATCH 08/10] chore(plugin): satisfy Codacy WPCS rules in /maintenance endpoint Codacy flagged three style issues on the new endpoint: - Replace `!class_exists(...)` with `=== false` comparison - Replace `!$redis->connect(...)` with `=== false` comparison - Replace `intval(...)` with `(int)` cast The pre-existing `/process-queue` route still uses the older patterns; those weren't in this PR's diff so Codacy didn't flag them. Cleanup of that route is left for a separate PR to keep this diff minimal. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cdcf-redis-translations/cdcf-redis-translations.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php b/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php index 5ce4658..0cc2c70 100644 --- a/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php +++ b/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php @@ -56,7 +56,7 @@ ); } - if (!class_exists('Redis')) { + if (class_exists('Redis') === false) { return new WP_Error( 'redis_unavailable', 'PHP Redis extension not installed', @@ -66,7 +66,7 @@ try { $redis = new Redis(); - if (!$redis->connect('127.0.0.1', 6379, 1.0)) { + if ($redis->connect('127.0.0.1', 6379, 1.0) === false) { return new WP_Error( 'redis_unavailable', 'Could not connect to Redis at 127.0.0.1:6379', @@ -83,7 +83,7 @@ } // action === 'begin' - $duration = intval($request['duration_seconds'] ?? 300); + $duration = (int) ($request['duration_seconds'] ?? 300); $duration = max(60, min(600, $duration)); $redis->setex('cdcf:maintenance:until', $duration, '1'); return new WP_REST_Response([ From 4fc9a197b779c4ba89a0f94b93e7aad4fb4db381 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 05:36:45 +0200 Subject: [PATCH 09/10] chore: address CodeRabbit review on PR 66 Three findings, fixed: - AGENTS.md: the cdcf/v1 preamble said all endpoints require edit_posts, but /process-queue and /maintenance both require manage_options. The /maintenance row already noted its capability inline, but the preamble was misleading. Update the preamble to flag the exception. - docs/redis-queue-worker.md: the worker now calls redis-cli from in_maintenance() at every poll cycle, so add it to the prerequisites list with a hint about the package name on Debian/Ubuntu and RHEL. - cdcf-redis-translations.php: $redis->setex() can return false on a Redis write failure even when connect() succeeded; without checking it, the deploy would proceed thinking the worker is paused when the flag wasn't actually set. Return 500 with redis_write_failed if the setex returns false; the deploy step's existing 3-attempt retry will surface the failure and abort the deploy. One finding skipped: CodeRabbit claimed the pipe inside backticks in the /maintenance row description splits the cell into four columns. GFM correctly preserves pipes inside inline code spans, so the row renders as a single cell. No change. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 2 +- docs/redis-queue-worker.md | 2 +- .../cdcf-redis-translations/cdcf-redis-translations.php | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3cb10cd..8e377ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,7 @@ Uses `@import 'tailwindcss'` (not `@tailwind` directives). Custom utilities via ## REST API Endpoints (`cdcf/v1`) -All endpoints require Application Password authentication (`edit_posts` capability). +All endpoints require Application Password authentication. Most endpoints require `edit_posts` capability; `/process-queue` and `/maintenance` require `manage_options` (administrator) — see the row notes where capability differs. | Method | Route | Description | |--------|-------|-------------| diff --git a/docs/redis-queue-worker.md b/docs/redis-queue-worker.md index 93f5d82..069d326 100644 --- a/docs/redis-queue-worker.md +++ b/docs/redis-queue-worker.md @@ -33,7 +33,7 @@ Queue Worker (systemd service) - Redis server running on the production host - The `redis-queue` and `cdcf-redis-translations` WordPress plugins activated - A WordPress user with `manage_options` capability and an Application Password -- `curl` and `python3` available on the server +- `curl`, `python3`, and `redis-cli` available on the server (`redis-cli` is typically provided by the `redis-tools` package on Debian/Ubuntu or `redis` on RHEL-derived distros; the worker uses it to poll the maintenance flag — see "Maintenance mode" below) ## 1. Install the worker script diff --git a/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php b/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php index 0cc2c70..6ca2c05 100644 --- a/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php +++ b/wordpress/plugins/cdcf-redis-translations/cdcf-redis-translations.php @@ -85,7 +85,13 @@ // action === 'begin' $duration = (int) ($request['duration_seconds'] ?? 300); $duration = max(60, min(600, $duration)); - $redis->setex('cdcf:maintenance:until', $duration, '1'); + if ($redis->setex('cdcf:maintenance:until', $duration, '1') === false) { + return new WP_Error( + 'redis_write_failed', + 'Failed to set maintenance flag in Redis', + ['status' => 500] + ); + } return new WP_REST_Response([ 'ok' => true, 'until' => time() + $duration, From ad51d3234e6c1eb3fa165a3b84df486d57e5298b Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Sat, 2 May 2026 05:46:20 +0200 Subject: [PATCH 10/10] chore: address CodeRabbit follow-up on PR 66 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new actionable findings; the other two CodeRabbit re-flagged (redis-cli prereq in worker docs and setex return check in PHP) were already addressed by 4fc9a19 and are stale review state. - worker: fail fast at startup if redis-cli is missing from PATH. Without this guard, in_maintenance()'s 2>/dev/null swallows the shell's "command not found" stderr and || return 1 makes the worker always think it's not in maintenance — so the deploy pause is silently ignored and FPM stress proceeds as before. - AGENTS.md: rewrite the /maintenance row description to avoid the raw pipe in `"begin"|"end"`. GFM does preserve pipes inside backticks, but CodeRabbit's parser disagrees and the prose form ('"begin" or "end"') is unambiguous regardless of which renderer reads it. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 2 +- scripts/cdcf_queue_worker.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 8e377ce..cdbd753 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ All endpoints require Application Password authentication. Most endpoints requir | `POST` | `/team-member` | Create a team member with auto-translation and About page linking (see below) | | `POST` | `/community-channel` | Create a community channel with auto-translation and Community page linking (see below) | | `POST` | `/local-group` | Create a local group with auto-translation and Community page linking (see below) | -| `POST` | `/maintenance` | Pause or resume the cdcf-queue-worker by setting/clearing a Redis flag (`{action: "begin"|"end", duration_seconds?: int}` — server-clamps duration to 60–600). Requires administrator (`manage_options`) capability. | +| `POST` | `/maintenance` | Pause or resume the cdcf-queue-worker by setting/clearing a Redis flag. Body: `action` is `"begin"` or `"end"`; optional `duration_seconds` is clamped server-side to 60–600. Requires administrator (`manage_options`) capability. | | `POST` | `/academic-collaboration` | Create an academic collaboration with auto-translation and Community page linking (see below) | ### `POST /team-member` diff --git a/scripts/cdcf_queue_worker.sh b/scripts/cdcf_queue_worker.sh index 2d5f996..1076ecc 100755 --- a/scripts/cdcf_queue_worker.sh +++ b/scripts/cdcf_queue_worker.sh @@ -75,6 +75,14 @@ if [ -z "$WP_REST_URL" ] || [ -z "$WP_APP_USERNAME" ] || [ -z "$WP_APP_PASSWORD" exit 1 fi +# Required for in_maintenance() — without this the worker silently +# ignores the deploy-time pause flag and keeps hitting FPM during +# deploys (the very condition this whole feature is meant to prevent). +if ! command -v redis-cli >/dev/null 2>&1; then + echo "ERROR: redis-cli not found in PATH. Install it (Debian/Ubuntu: 'sudo apt install redis-tools'; RHEL: 'sudo dnf install redis')." + exit 1 +fi + ENDPOINT="${WP_REST_URL}/cdcf/v1/process-queue" AUTH=$(echo -n "${WP_APP_USERNAME}:${WP_APP_PASSWORD}" | base64)