News post → town crier + Discord announcement pipeline
Replace the fire-and-forget Discord-only announce on publish with a
retry-safe, two-leg pipeline. When a post transitions into published-news
(false→true publish while in news, or category→news while published), an
announce_jobs row is enqueued with two INDEPENDENT delivery legs:
• town crier — sidecar POST /towncrier via uoLinkClient (stable id
`post-<id>` so a retry replaces rather than duplicates)
• discord — bot POST /internal/announce via botInternalClient
(single source of truth for the #news channel stays in the bot)
An in-process poller (utils/announceWorker) sweeps the table every
ANNOUNCE_POLL_MS and dispatches each due leg with its own exponential
backoff (30s→2h, 6 attempts). A leg is retried on transient failures
(503/504/network) and failed fast on data/config errors (400 over-cap,
401/409). Publishing never blocks on the sidecar or Discord — enqueue is
local DB only. Parent `status` is a done/partial/failed rollup of the two
legs; posts.announced_at is stamped once both deliver.
Admin visibility: GET /admin/posts/:id/announce + a per-leg Retry
(POST .../announce/retry) surfaced in the PostEditor for news posts.
Pure decisions (text build/caps, classification, backoff, rollup) live in
announceJobs.logic and are unit-tested (server/test/announceJobs.test.js,
10 tests). The old manual /admin/uo-link/towncrier form is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
This commit is contained in:
@@ -647,6 +647,37 @@ CREATE TABLE IF NOT EXISTS pages (
|
||||
INDEX idx_pages_nav (show_in_nav, nav_group, nav_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Announcement pipeline. One row per publish event of a news post; the table
|
||||
-- doubles as the job queue (a light in-process poller — utils/announceWorker.js
|
||||
-- — sweeps it for due legs). Two INDEPENDENT delivery legs so a Discord outage
|
||||
-- never blocks or retries the in-game town-crier leg and vice versa. `status` is
|
||||
-- a derived rollup of the two legs (see announceJobs.logic.js): done when both
|
||||
-- legs done, failed when both exhausted, partial in between. Each leg tracks its
|
||||
-- own attempt count, last error, and next-due time for exponential backoff.
|
||||
-- post_id is INT (matches posts.id) and cascades so deleting a post reaps its
|
||||
-- jobs. posts.announce_job_id points back at the latest row for admin lookups.
|
||||
CREATE TABLE IF NOT EXISTS announce_jobs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
post_id INT NOT NULL,
|
||||
status ENUM('pending','partial','done','failed') NOT NULL DEFAULT 'pending',
|
||||
|
||||
towncrier_status ENUM('pending','done','failed') NOT NULL DEFAULT 'pending',
|
||||
towncrier_attempts SMALLINT NOT NULL DEFAULT 0,
|
||||
towncrier_last_error TEXT NULL,
|
||||
towncrier_next_attempt_at DATETIME NULL,
|
||||
|
||||
discord_status ENUM('pending','done','failed') NOT NULL DEFAULT 'pending',
|
||||
discord_attempts SMALLINT NOT NULL DEFAULT 0,
|
||||
discord_last_error TEXT NULL,
|
||||
discord_next_attempt_at DATETIME NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_announce_jobs_post FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
INDEX idx_announce_due (towncrier_status, towncrier_next_attempt_at),
|
||||
INDEX idx_announce_due_discord (discord_status, discord_next_attempt_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migrations for databases created before the wiki upgrade. Each statement uses
|
||||
-- IF NOT EXISTS so re-running on every boot is a harmless no-op. New installs get
|
||||
-- these columns from the CREATE TABLE above; existing installs get them here.
|
||||
@@ -683,3 +714,13 @@ ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published TINYINT(1) NOT NULL DE
|
||||
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published_at DATETIME NULL;
|
||||
ALTER TABLE wiki_pages ADD FULLTEXT INDEX IF NOT EXISTS idx_wiki_search (title, body);
|
||||
|
||||
-- News → town-crier + Discord announcement pipeline. Add the announcement-state
|
||||
-- columns to posts on databases created before the pipeline landed. announced_at
|
||||
-- is stamped once both legs deliver; announce_job_id points at the announce_jobs
|
||||
-- row for the post's admin status panel. Kept as a plain column (not a hard FK)
|
||||
-- so the idempotent boot migration never trips over a re-added constraint — the
|
||||
-- pointer is resolved in application code and the CASCADE on announce_jobs.post_id
|
||||
-- already keeps the two tables consistent.
|
||||
ALTER TABLE posts ADD COLUMN IF NOT EXISTS announced_at DATETIME NULL;
|
||||
ALTER TABLE posts ADD COLUMN IF NOT EXISTS announce_job_id INT NULL;
|
||||
|
||||
Reference in New Issue
Block a user