feat(multi_layer): cross-node layer coherence via pg-tracked coordinators - #49
Merged
Merged
Conversation
…tors Node-local fast layers (Cache.ETS) go stale on every node except the writer: put writes all layers on the calling node only, and delete is the only cross-layer remover — there was no way to invalidate another node's L1 without also deleting the shared slower layer. Each MultiLayer cache now runs a per-node Coordinator (replacing the placeholder Agent in child_spec) that joins a :pg group named after the cache — group membership doubles as the registry of which nodes hold the cache. With broadcast_mode set, successful put/delete notifies every other member, which applies the change to its own broadcast_layers: - :invalidate — remote nodes drop the key from local layers and lazily re-read through the shared layer (key-sized messages; the default choice for large values) - :replicate — remote nodes write the new value immediately (full value copy per member; for small values only) Delivery is best-effort (send to pg members, no acks) — backfill_ttl and layer TTLs remain the correctness floor for members that miss a message, and the docs say so. The pg scope is started unlinked (:pg.start/1) — start_link would tie the scope's life to whichever coordinator started it first. Each coordinator monitors the scope and re-joins on :DOWN, since a restarted scope comes back with empty membership.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #49 +/- ##
==========================================
+ Coverage 83.81% 84.23% +0.41%
==========================================
Files 23 24 +1
Lines 655 685 +30
==========================================
+ Hits 549 577 +28
- Misses 106 108 +2 ☔ View full report in Codecov by Harness. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
MultiLayer's node-local fast layers (e.g.
Cache.ETS) go stale on every node except the writer:putwrites all layers on the calling node only, anddeleteremoves from all layers including the shared one — so there was no way to invalidate another node's L1 without destroying the shared L2 entry. Any multi-node deployment reading through an ETS L1 serves stale data for up tobackfill_ttlafter every remote write, including the pathological case where an update-triggered consumer re-reads immediately and re-caches the previous value.Design
Each MultiLayer cache now runs a per-node
Cache.MultiLayer.Coordinator(replacing the placeholder Agent inchild_spec) that joins a:pggroup named after the cache — group membership doubles as the registry of which nodes hold the cache. Two new (optional, default-off) cache opts:broadcast_mode: :invalidate— after a successfulput/delete, other members drop the key from theirbroadcast_layersand lazily re-read through the shared layer. Key-sized messages; the right default for large values.broadcast_mode: :replicate— members write the new value into their local layers immediately. Full value copy per member; for small values.broadcast_layersnames the node-local layer modules the remote change applies to (never the shared layer).Guarantees (stated in docs)
Best-effort delivery —
send/2to pg members, no acks.backfill_ttl/layer TTLs remain the correctness floor for members that miss a message. The pg scope is started unlinked (:pg.start/1, notstart_link— linking would tie the scope to whichever coordinator started it first); every coordinator monitors the scope and re-joins on:DOWNbecause a restarted scope has empty membership.Tests
8 new tests: pg membership, invalidate/replicate message handling scoped to the listed layers only, broadcast excluding the writing node's own coordinator (by pid, so it's testable single-node), and put/delete integration for both modes. Existing MultiLayer suite untouched and green (20/20 combined); full suite green except the pre-existing RedisJSON env failures (module not installed in local Redis).
mix credo --strictclean.Motivating use case
CheddarFlow's OpenInterestCache (325KB surfaces, Redis-only today) moves to
{Cache.MultiLayer, [ETS, Redis]}withbroadcast_mode: :invalidate— the OI update broadcast currently races the update itself, and remote L1s would otherwise re-cache the previous surface for hours.