Cross-server schema cache consistency without broadcast storms #3205
Replies: 1 comment
Open questions and proposed implementation planUpdated 2026-09-18. See the discussion timeline for merged fixes and the separate #3157 watch-recovery PR. The phases below are proposals, not additional scope for #3157 or an already-approved release gate. Open questionsNumbered so replies can reference them directly ("Q3: 5s"). Q8, Q9 and Q11 block the start of implementation; Q1 gates only the optional HBase phase; Q3 needs an answer before the release; the rest calibrate claims or clean up.
Implementation planPR-sized phases, each independently landable. The proposal recommends Phases 0 and 1 plus integration validation for the k8s release; maintainers have not yet accepted this as a release gate; Phase 2 is optional and severable. Phase 0, standalone preparatory fixes (tiny, land first):
Phase 1, proposed core machinery on the hstore+PD path (release scope pending agreement):
Phase 2, optional HBase mode (only if Q1 says yes):
Phase 3, validation and release:
Phase 4, deferred: graph-data cache events reusing the same machinery, orphan-key janitor, RPC notifier deprecation or repair, legacy-key retirement per Q5. Next stepNo implementation until Q8, Q9 and Q11 have answers and the overall direction has a maintainer nod. Alternative designs that reach the same bounds with less machinery are welcome; the storm numbers in the post are the bar any alternative has to clear. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Scope and current status
Updated 2026-09-18. This discussion tracks cross-server schema-cache convergence, including missed notifications and excessive invalidation traffic. Restoring a watch connection is necessary, but does not by itself make schema caches consistent.
A schema change on Server A can remain invisible on Server B because the publisher emits no notification, the watch stops, an event is lost during disconnection, or the consumer fails to invalidate its cache. These are different failure points in the same propagation path.
flowchart LR A["Server A: schema mutation"] --> B["Publish invalidation"] B --> C["PD watch"] C --> D["Server B: invalidate cache"] V["Proposed durable schema version"] -. "periodic reconciliation" .-> D A -. "proposed version update" .-> VTimeline and related work
Dates below use UTC; merged dates are distinguished from issue reports and review updates.
eeb7041e8preserves ordinary KV/lock timeout budgets and unpublishes SchemaDriver before cleanup. Five independent review lanes; 41 KvClient + 5 SchemaDriver tests passed.eeb7041e8; 24 CI checks passed. One legacy streaming override compatibility discussion remains. No event replay or reconciliation was added.Recommended order: land the bounded watch-recovery fix in #3157 independently; verify and address the small publisher/consumer defects below; then agree and implement the convergence contract in this discussion. Do not expand #3157 into the full schema synchronization redesign, and do not close this discussion merely because #3157 merges.
Background
The remaining gap exists even with a healthy watch.
CachedSchemaTransactionV2.updateSchema()updates its local cache but suppresses cross-server notification to avoid broadcast storms from background status changes. Removal notification is gated bytask.sync_deletion. Reconnecting cannot deliver a notification that was never emitted, nor recover an event from before the fresh subscription.Multiple Server pods behind one Kubernetes Service expose this as inconsistent schema visibility depending on the responding pod. The proposal below aims to bound stale caches without turning every mutation into a full cluster-wide reload.
Why the quick fix does not work
Re-enabling the suppressed notification looks like a one-line fix. The numbers say otherwise. An index rebuild flips status 2 to 3 times per label (
IndexLabelRebuildJob.java:106,137,143). Take M = 1,000 status flips over 60 s on N = 10 servers with 5,000 schema elements per graph:clearSchemaCache,CachedSchemaTransactionV2.java:131-152), so 10,000 full clears;:414-420), so concurrent readers pile up: roughly 40,000 prefix scans for the duration of the job, with latency spikes on every server.Cluster-wide work scales as O(M x N). Any acceptable design has to make cluster-wide work independent of M. The same math applies to today's shipped add path: bulk-creating 500 schema elements already produces 500 uncoalesced events and 500 full clears per remote server.
What propagates today
task.sync_deletion=trueTwo details make the multi-server rows worse than they look. The
task.sync_deletiongate (maybeNotifySchemaCacheClear,CachedSchemaTransactionV2.java:360-367) assumes the caller invalidates other nodes synchronously, butsyncWaitonly waits for the local task, so a removed label can survive in remote caches indefinitely. And the PD watch client re-subscribes after a leader change or error with no resume point (KvClient.java:~168-190), so any design whose correctness depends on event delivery is wrong by construction.Proposed design: three rules
The historical tension exists because the event is the only carrier of truth: losing one loses the information forever, so nothing dared coalesce or drop them. The proposal separates truth from wake-up.
Rule 1: a version register, written synchronously on every mutation. Each graph gets a small version value stored next to the schema itself: a PD KV key for hstore (
HUGEGRAPH/{cluster}/SCHEMA_VERSION/{graphSpace}/{graph}, an opaque token compared only for equality), a counter row on the existing counter table for HBase (no DDL, no migration). Every schema mutation, including updates and status flips, writes it on the DDL thread right after the schema write. One cheap put with zero fan-out; the storm was fan-out times full reloads, never the publisher's own puts. If the register put fails after the schema commit, the DDL call still succeeds, and a background retry with metrics (register_write_failures,register_dirty) takes over until it lands.Rule 2: events become coalesced, lossy hints. A debounced flusher (500 ms window) publishes per-id invalidation hints on a new
EVENT/GRAPH/SCHEMA/DELTAkey, at most about 2 events per second per graph no matter how fast mutations come. Hints only invalidate cache entries; they can never mark a node as up to date. That makes dropping, merging and reordering them harmless, which is exactly what allows unlimited coalescing. For compatibility, bursts containing add/remove/clear also put one event per window on the legacy CLEAR key, byte-identical to today's payload, so old servers and store nodes see what they see today, only fewer of them.Rule 3: a node advances its applied state only through full resync. A reconciler polls the register (default every 10 s per graph, one prefix scan per server per tick) and, on mismatch, clears the graph's caches and adopts the value it read before clearing. Nothing else advances the applied state, not even the hints. The proposed target is convergence within two successful reconcile intervals (20 s with a 10 s interval), after the version update is durably visible and under bounded read/reload latency. This is not an unconditional deadline during PD unavailability. A schema commit followed by a failed version write and publisher crash can lose an in-memory retry; durable recovery or an atomic publication contract must be established before claiming a bound after every successful DDL. Typical hint-path latency also needs measurement.
For the storm scenario above, this comes out to at most 121 events on the wire and about 40 full clears cluster-wide instead of 10,000, a roughly 250x reduction, and doubling M changes nothing on the receiver side. Schema read hit paths gain zero instructions; the register is only touched on writes and by the reconciler.
Proposed convergence targets, per deployment
An emergency switch (
schema.sync.enabled=false) restores the legacy event behavior. Rolling-upgrade compatibility is a design goal: keep legacy payloads readable, then validate mixed-version ordering, coalescing delays and recovery before claiming safety in any pod order.Candidate preparatory fixes to verify independently
The original analysis identified the following publisher/consumer defects or optimization candidates. Recheck each on the target head before changing code; #3157 does not claim to fix them. The JSON payload item concerns Store-side SchemaDriver, not proof that a Server watch failed:
SchemaDriver.schemaCacheClearHandler(SchemaDriver.java:137-147) still splits the payload on-, but the server has emitted JSON since fix(server): sync hstore schema cache clears #3011, so store-side schema caches silently never clear.SchemaTransactionV2.saveSchemapublishesnotifyGraphVertexCacheClear/notifyGraphEdgeCacheClearPD events on every VertexLabel/EdgeLabel save, and nothing anywhere subscribes to them: pure write amplification inside the schema write path.task.sync_deletiongate described above, which also suppresses STORE_CLEAR/TRUNCATE propagation.PdMetaDriver.extractValuesFromResponsereturns null for a watch batch containing any non-Put event, swallowing the whole batch for every meta listener in the process.Who this pings, and why
task.sync_deletiongate, so review from that side matters most.SchemaDriverin hugegraph-struct, where bug 1 lives.SchemaTransactionV2/MetaManagerand may know history that invalidates parts of this.Nothing here is settled. If a simpler mechanism reaches the same bounds, or the bounds themselves are wrong for the k8s release, that is exactly the feedback this thread is for. The full analysis, per-mode matrix, failure semantics and test plan exist as a longer document; the open questions and the implementation plan are in the first comment below.
Implementation decisions and the proposed phased plan are tracked in the first comment. Existing diagrams illustrate the proposal; timing/count annotations are model estimates, not measured production guarantees.
All reactions