Skip to content

feat(cache/jobs): fragment cache + tag-invalidation worker + block render cache + scheduled publisher - #489

Merged
tayebmokni merged 4 commits into
mainfrom
feat/cache-fragment-scheduler
May 26, 2026
Merged

feat(cache/jobs): fragment cache + tag-invalidation worker + block render cache + scheduled publisher#489
tayebmokni merged 4 commits into
mainfrom
feat/cache-fragment-scheduler

Conversation

@tayebmokni

Copy link
Copy Markdown
Contributor

Closes #91, #94, #108, #143.

@tayebmokni
tayebmokni enabled auto-merge (squash) May 26, 2026 22:00
@github-actions

Copy link
Copy Markdown

Heads up — this PR touches strings that often signal a security disclosure (vulnerab, CVE-, exploit, bypass, or auth bypass).

If this PR fixes or describes a real vulnerability that has not yet been publicly disclosed, please stop and use the private path:

  1. Open a private security advisory, or
  2. Email security@gonext.io with subject [SECURITY] GoNext - <summary>.

See /SECURITY.md for the full disclosure flow and /docs/16-bug-bounty.md for bounty terms.

If this is a false positive (test fixture, doc update, release notes, etc.) please ignore this comment — the check is advisory only and does not block the PR.

Matched commit messages:

  • b01d7fc feat(blocks/render): per-block, version-keyed render cache
    Matched files:

  • packages/go/blocks/render/cache.go

  • packages/go/blocks/render/cache_test.go

  • packages/go/cache/fragment/fragment.go

tib0o0o added 4 commits May 27, 2026 00:15
Adds packages/go/cache/fragment with Get/Set/Purge over a Redis
backend. Tag invalidation is decoupled from the cache itself: Purge
appends rows into the cache_invalidations outbox shipped by 000030,
and the invalidator worker fans the INCRs out to per-tag version
counters via pub/sub. Reads validate the captured-vs-live version
vector to detect stale entries.

Layered as fragment (cache surface, no Postgres dep) + PgOutboxWriter
(pgxpool insert) so unit tests don't need a database.

Closes #91.

Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…tions

Adds Subscriber to packages/go/cache/invalidator: PSUBSCRIBEs to the
gonext:cache:invalidate channel published by the existing Worker,
parses '<slug>:<tag>' payloads, and dispatches each invalidation to
one or more registered HandlerFuncs. Handler errors are logged but
do not stop the loop — the at-least-once delivery model assumes
handlers are idempotent.

Wires the Worker into apps/worker so the outbox actually drains in
production, plus the shared pgx pool and dedicated Redis client the
scheduler/GC jobs (#143) will reuse.

Closes #94.

Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Adds CachedWalker — a Walker that consults a CacheBackend keyed by
(version, block.type, sha256 of canonical encoding) before invoking
each subtree's renderer. Production wires render.CacheBackend to
cache/fragment.Cache; the fragment cache itself routes invalidation
through the cache_invalidations outbox (#91/#94).

Blocks that declare UsesContext or ProvidesContext bypass the cache
because their output depends on ancestor state we cannot fold into
the key. The remaining "leaf" blocks — paragraph, heading, image,
list, etc. — dominate real-post content, which is what makes the
80%+ hit rate target on repeated renders achievable. Tests pin the
hit rate at 83% after six identical-tree renders.

The canonical encoder sorts attribute keys (Go's randomized map
iteration would otherwise produce a different hash on each render
of the same block).

Closes #108.

Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Adds packages/go/jobs/scheduler with two cron-driven tasks:

  * content.publisher.minute — fires every 1m, transitions posts
    whose scheduled_for has elapsed from status='scheduled' to
    status='published' via a single UPDATE … RETURNING. published_at
    is preserved across re-publishes (COALESCE) so canonical
    date-URLs stay stable.

  * content.gc.daily — fires at 03:30 UTC, hard-deletes posts that
    have been status='trash' for more than 30 days (configurable).
    Trailing 30 minutes behind revisions.purge.daily so the two
    heavy queries don't pile up at 03:00.

Both use the SELECT … FOR UPDATE SKIP LOCKED + CTE pattern so a
mis-fire from a non-leader replica is safely idempotent. Both have
LIMIT clauses (500 for publisher, 1000 for GC) to bound the lock
window — excess rows roll over to the next fire.

SeedDefaults wires both into the worker's taskspec + cron
registries; the worker's main.go calls it once the pgx pool is up.

Closes #143.

Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
@tayebmokni
tayebmokni force-pushed the feat/cache-fragment-scheduler branch from 329edcd to 8bc7693 Compare May 26, 2026 22:15
@tayebmokni
tayebmokni merged commit 3f9e5a2 into main May 26, 2026
13 of 18 checks passed
@tayebmokni
tayebmokni deleted the feat/cache-fragment-scheduler branch May 26, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redis fragment cache with tag-based invalidation

2 participants