Skip to content

fix(server): keep schema caches current across HStore Servers - #3477

Open
bitflicker64 wants to merge 3 commits into
apache:masterfrom
bitflicker64:fix/schema-cache-events-3235
Open

bitflicker64 wants to merge 3 commits into
apache:masterfrom
bitflicker64:fix/schema-cache-events-3235

Conversation

@bitflicker64

@bitflicker64 bitflicker64 commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Purpose of the PR

With several Servers on HStore, a schema change made through one Server does not reliably reach the others, and a Server that keeps an old copy can write it back over newer schema. #3235 has the failures measured on master 8828d8238.

This PR completes the existing schema-cache-clear event. It does not add a new sync mechanism. There is no change to PD raft, the PD store or kv.proto; the only PD code touched is the client library (KvClient).

It is a new change written from master. It is not built on #3237 (polling, closed) and contains nothing from #3376, which stays separate follow-up work.

Scope at a glance

In this PR Not in this PR, separate follow-up
Publish a clear event after every schema add, update, remove and store clear or truncate, also when the write fails partway ACK or barrier between Servers before an index rebuild starts
Clear the V2 schema caches when the PD watch starts or restarts Schema write and event as one atomic PD commit
Retry a publish that failed Protection for two Servers changing the same element at once (#3376)
Generation guard so an old read cannot refill a cleared cache Refusing requests while cut off from PD, and a keepalive on the watch

The right column is not solved here and this PR does not claim it. Each item is explained under "What this PR does not implement".

Main Changes

What this PR fixes

All changes are in CachedSchemaTransactionV2 unless noted.

  1. Schema update and removal propagation. updateSchema() published nothing, so a property append, user_data, the index list of a base label and every status change stayed on the writer. removeSchema() and store clear or truncate published only with task.sync_deletion=false. Now every committed add, update, remove and store clear or truncate publishes a clear event after its write. Every event clears at once; events are not delayed or merged.
    A schema write is two or three PD puts. If it fails after the first one, PD holds part of the change. That case publishes too, and the writer drops its own copy of the element; the caller still gets the error.
  2. Cache invalidation on PD watch start and recovery. Events are delivered at most once, and KvClient subscribes again one second after a disconnect without telling the listener. KvClient.listen() has a new overload with a callback that runs each time a watch goes live (first frame of a new stream, including after a leader change or a stream error). The Server clears all its V2 schema caches when it runs, so nothing missed while the watch was down survives.
  3. Failed-publication retry. A graph whose publish failed goes into an in-memory pending set and is published again after 1 s, doubling up to 30 s, and at once when the watch is live again. Each Server also publishes once when it opens a graph, which repairs a publish lost to a restart of the writer.
  4. Generation guard. A generation counter and a read-write lock on the shared cache. Every clear, removal and local write advances the generation as the last step of its change. A read by id, a read by name, a full listing or the writer's own write-through that started before a clear cannot cache its value after the clear. A name miss now triggers one reload per graph at a time instead of one per waiting thread.

Plumbing for the callback: MetaDriver.listen(key, consumer, onStarted) as a default method that falls back to the plain listen, overridden in PdMetaDriver, and passed through MetaManager and GraphMetaManager. The etcd meta driver uses the fallback and gets no watch-start clear.

Changed behavior of schema calls

Once the schema write has committed, a failed event publication no longer fails the schema call. The call returns success and the publication is retried in the background (item 3).

Before this PR the add path threw after the commit when the publish failed, so the caller saw an error for a change that was already stored. Inside an index rebuild job the same throw would fail the job after its status write had committed. A publish failure is logged at WARN with the graph name.

What a caller can now observe: the call succeeds while other Servers still hold the old copy until the retry goes through or their watch restarts.

How this relates to the review of #3237

That review asked for a notification design in place of polling and listed points to settle first. This is where each one stands here. Nothing from #3237 itself is revived.

Point from that review In this PR
Notify, do not poll Done. Nothing runs while the schema is idle. A retry timer exists only while a publish is pending.
Restore update and status notifications Done, item 1. This reverses the choice in #3011 to keep status changes local. The cost is measured under "Cost of an event".
Retry without blocking DDL Done, item 3.
A reliable link between a schema commit and its notification Partly. A failed publish is retried, a restarted writer publishes again, and a write that fails partway publishes too. The write and the event are still separate PD puts: see the limits.
A Server that was away reloads before it serves Partly. It drops all its cached schema when its watch is live again. It is not kept from serving while it is cut off: see the limits.
Batch redundant invalidations within one operation Not done. One index creation publishes four events. Merging them delays the clear of the base label, which widens the index window described in the limits.

Cost of an event

One event makes every other Server drop the schema cache of that graph. Its next read by name then loads the whole schema again (four prefix scans in PD).

Measured on 1 PD + 1 Store + 3 Servers on one host (12 CPUs), fresh cluster with 300 property keys, 2026-11-13: 200 updates in a row through S0 while S1 and S2 each serve schema reads by name on two threads. Two runs per build.

master 8828d8238 this PR
One update on S0, mean 7.8 and 8.0 ms 11.8 and 11.8 ms
Reads per second on S1 and S2 while idle 3,900 and 4,140 3,850 and 4,310
Reads per second during the burst 3,610 and 3,760 320 and 340
Median read during the burst 1.0 and 1.1 ms 10.6 and 10.8 ms
Reads per second right after the burst 4,780 and 4,890 4,950 and 4,970
Failed reads 0 0
S1 and S2 show the last update 3 s after the burst no yes

So for as long as schema changes arrive back to back, schema reads on the other Servers were about 11 times slower in this setup, and they recovered when the burst ended. The cost grows with the schema: the same burst on a graph with about 950 property keys gave 180 to 190 reads per second. Ten index creations in a row cost about the same on both builds (140 to 149 ms per creation and 2,450 to 2,580 reads per second on the other Servers). I did not measure the effect on graph reads and writes, only on schema reads. Lowering this cost means sending which element changed, or merging events, and is proposed as follow-up.

What this PR does not implement

These are known limits. They are listed in docs/server-runtime.md as limits, and none of them is implied to be solved by this PR. Follow-up issues so far: the existing #3376, and #3478 for a watch that stops for good.

Not implemented What can still go wrong Why it is excluded
ACK or barrier before an index rebuild A write on another Server that is prepared from the old base label and committed after the rebuild scan has passed it is missing from the new index. GraphTransaction.prepareCommit() takes the index list from the cached label, and IndexLabelRebuildJob scans the label once and starts as soon as the task is scheduled. The rebuild would have to wait until every Server has confirmed the new base label. That needs Server membership, confirmations and a rule for a Server that never answers. It is a protocol of its own and belongs in a separate design.
Atomic schema commit plus event publication A writer that stops for good between its schema write and its publish leaves the other Servers on the old copy until the next event for that graph or their next watch restart. If the writer restarts, its publish on graph open repairs this. The write and the event are two PD puts. Making them one needs a PD-side change (one raft entry for both, or an outbox), which this PR avoids on purpose.
Concurrent same-element schema changes An update still writes the whole element from the writer's copy. Two Servers changing the same element at the same moment can lose one change. It needs a conditional write in PD. That is #3376, which is +3,711 lines on its own.
Offline-serving gate A Server that cannot reach PD keeps serving its cached schema until it reconnects. Refusing requests in that state trades availability for freshness. That is a product decision, not part of a cache fix.
Restart of a watch that stopped for good A PD watch that ends with a gRPC status the PD client does not retry (UNAUTHENTICATED, PERMISSION_DENIED, UNIMPLEMENTED and six more) is removed and not started again. The watch-start clear then never runs, and that Server keeps its cached schema until it is restarted; its log has one ERROR line for the stopped watch. PD does not answer a watch with these codes itself. They can come from a proxy between Server and PD: gRPC maps HTTP 401, 403 and 404 to three of them. This is how the PD client on master behaves for every Server meta listener, not only this one. Starting the watch again needs a stop callback through the meta driver and a limit on how often a refused watch is retried. Tracked in #3478.
Watch keepalive A watch stream that dies without an error is noticed only when another PD call from that Server fails. It needs keepalive settings on the PD client channels, which affects every user of the PD client and needs its own tests.

Two more points:

  • A reload right after a PD leader change also needs fix(pd): wait for a ReadIndex before serving get and scanPrefix #3383, so the read cannot be served by a leader that has not applied its log yet.
  • The guarantees start when the last Server runs this version. A Server on the old version still publishes nothing on update. Index entries skipped before the upgrade stay missing until that index is rebuilt.

Found while reviewing this change and left out because they are independent of it:

  • IntObjectMap.set() grows its array by one doubling only, so the first write of a schema id above twice the current size throws. It is on master and is hit by the array cache of both schema transactions. It can no longer skip a publish, because the publish runs in a finally.
  • Store nodes listen on the same clear key (SchemaDriver) without a watch-start callback. I did not examine what their cache is used for.

Review scope

This PR is meant to stay at its current size and shape. For a review finding I will do the following:

  • A bug in this patch, or something that breaks one of the four items under "What this PR fixes": fixed here, with the smallest change and a regression test.
  • Anything that needs one of the excluded items above (ACK or barrier, atomic commit, conditional write, offline gate, keepalive), a PD server change, or a refactor of code this PR does not touch: answered with the reason and proposed as a follow-up issue, not added here.

The second commit is the result of such a pass over the first one. It fixes four things, each with a test: no event after a schema write that failed partway, a retry that could schedule itself twice, a generation that advanced before the caches had changed (a concurrent read could copy the old value into the array cache), and a listing flag that never recovered after a refused write-through.

The third commit follows the review comments on the mirror, hugegraph#374. It changes no production code. The index test now fails when one of its background readers stops on an error (their results were never read, so the test could pass with fewer reads than it claims), and the stopped-watch limit above is added to docs/server-runtime.md with #3478 as its follow-up. The remaining comments were checked against the code and not taken: the Response in the reader loop is released by readEntity, the Scanner reads a stream that its try closes, a stopped watch is already logged at ERROR by the PD client, a null onStarted is already rejected by name in the PD client, and the shared test helper and the missing timeouts on the cluster-test HTTP client are optional (the timeouts are a TODO in the test).

A second round of comments on the third commit led to no code change. The points it repeats are answered above. Two were new: the reset helper of the cache tests cannot stop a retry that is already running (listed under "Known gaps in the evidence"), and the retry thread stays after the pending set is empty. That thread is one idle daemon thread, created only after a first failed publish, and each class in Server core that needs a scheduler owns one in the same way.

Size

16 files, +2,692 / -121 (git diff --shortstat 8828d8238 7df011acb).

Part Added Removed
Production code (CachedSchemaTransactionV2, KvClient, meta plumbing) 628 111
Tests 1,946 3
Cluster-test harness (restart one PD node in place) 49 2
Docs 69 5

Verifying these changes

  • Trivial rework / code cleanup without any test coverage. (No Need)
  • Already covered by existing tests, such as (please modify tests here).
  • Need tests and can be verified as follows:
    • CachedSchemaTransactionV2Test (new, 35 tests): publish on every write path and after the write, failed and partial writes, failed publish and retry, watch start, and the generation guard on every cache insertion and write-through path
    • KvClientTest (7 new tests): the watch-start callback on first start, leader change, stream error and a replaced watch
    • MultiClusterSchemaCacheTest (new, 5 tests): 3 PD, 3 Store and 3 Servers

Reproductions before and after

Same scripts for both columns, through the REST API, 1 PD + 1 Store + 3 Servers (S0, S1, S2) as local processes on one host (12 CPUs, Java 17), 2026-11-13. "Before" is master 8828d8238, "after" is the head of this branch.

Case Before After
Update user_data through S0 Still missing on S1 and S2 after 30 s Visible on S1 and S2 at the first check, after 1 s
S0 appends p1, then S1 appends p2 to the same label PD ends with [a, p2]; a vertex write that sets p1 returns 400 on S2 All three Servers show [a, p1, p2]; the vertex is read back with p1 through each Server
Index status, 30 index creations through S0 S1 and S2 keep CREATING in 30 of 30 0 of 30
Base label, same 30 runs Index missing from the label on S1 in 14 of 30 and on S2 in 15 of 30; 29 of 29 vertices written through such a Server are not indexed 0 of 30; 0 vertices not indexed
Add through S0 right after a PD restart Missing on S1 and S2 after 60 s, 3 of 3 runs Visible within 2 s, 3 of 3 runs
Remove with task.sync_deletion=true Still listed on S1 and S2 after 30 s Gone on all Servers within 1 s

In the "after" run each Server logged the watch-start clear four times: once at start and once per PD restart. No Server logged a failed publish.

All of this ran on one host with sub-millisecond latency. It shows that the cases happen and stop happening, not how often they occur in production.

Test suites

Java 17, 2026-11-13. Format and multi cluster ran on the tree of the head commit. The other suites ran on the second commit; the third changes one cluster test and one doc file and adds no file. Server unit and HStore core ran before one doc paragraph and one comment in the cluster-test harness were added to the second commit.

Suite Command Result
Format mvn editorconfig:check -pl hugegraph-pd/hg-pd-test,hugegraph-server/hugegraph-test,hugegraph-cluster-test/hugegraph-clustertest-test -am -ntp BUILD SUCCESS
Licence headers mvn apache-rat:check -ntp BUILD SUCCESS
Server unit mvn test -pl hugegraph-server/hugegraph-test -am -P unit-test -ntp 1,477 run, 0 failures, 8 skipped
HStore core (PD and Store running) mvn test -pl hugegraph-server/hugegraph-test -am -P core-test,hstore -ntp 1,033 run, 0 failures, 77 skipped
Multi cluster mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test -am -P multi-cluster-test -ntp 11 run, 0 failures (5 are MultiClusterSchemaCacheTest); the runtime log check passes. Run again on the third commit with the same result
PD client (PD running) mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-client-test -ntp 122 run, 0 failures (KvClientTest: 48), run on the first commit; the PD client is unchanged since

The two cache test classes were also run 12 times in a row in fresh JVMs: 12 of 12 passed.

One existing test changed: CachedSchemaTransactionTest.testV2WarmRequestsRetainSchemaWithoutMetadataReadsAndReceiveInvalidations expected one PD put and now expects one at graph open and one more for the store clear.

Do the tests detect the bugs

  • MultiClusterSchemaCacheTest on unpatched master 8828d8238: 4 of 5 fail (update, append, index creation, PD restart).
  • Each fix was taken out in turn and CachedSchemaTransactionV2Test was run against it:
Change taken out Tests that fail
Publish after the write (publish moved before it) testEveryPublishComesAfterItsStoreWrite, testAddPublishes
Handling of a write that failed partway testWriteThatFailsHalfwayPublishesAndDropsTheElement, testRemoveThatFailsHalfwayPublishesAndDropsTheElement, testFailedWriteKeepsItsEventWhenThePublishFailsToo
Only the newest retry reschedules testWatchStartDuringARetryLeavesOneRetry
Generation advanced last in a local write testReadDuringALocalWriteDoesNotPutTheOldValueBack (3 of 3 runs)
Eviction after a refused write-through testRefusedWriteThroughEvictsWhatWasCachedMeanwhile, testReadsNeverGoBackBehindAWriteUnderConcurrentClears
Read lock around a cached listing testAClearWaitsForAListingReadFromTheCache, testListingIsNeverPartialWhileClearsRun, testReadsNeverGoBackBehindAWriteUnderConcurrentClears
Whole-list flag set after a full load testRefusedWriteThroughEvictsWhatWasCachedMeanwhile

Known gaps in the evidence

  • testRemoveReachesEveryServerWithSyncDeletion passes on unpatched master, because a new graph publishes events while its system schema is set up. The unit test and the manual check in the table above cover that gate.
  • testChangeDuringPDRestartReachesEveryServer does not prove that a watch was down when the change committed. It failed on unpatched master in the one control run. The evidence for the watch-start clear is the unit tests plus the Server log lines of the manual run.
  • testReadsNeverGoBackBehindAWriteUnderConcurrentClears and testListingIsNeverPartialWhileClearsRun depend on timing. The first one failed in about half of the runs before the generation was advanced last, and in none of 12 after.
  • The generation order is tested with a deterministic test for a local write. For a clear it is covered only by the timing test above.
  • The publish in finally after a failure inside the cache update has no test.
  • The reset helper of the cache tests cancels a scheduled retry but cannot stop one that is already running. Such a retry ends without scheduling another one, unless the next test has already failed a publish: that test's first retry then comes after 2 seconds, not 1. It was not seen in 12 runs of the two classes or in CI.
  • The in-flight write window during index creation and the writer that never returns are derived from the code and were not reproduced.
  • Not tested: a real network partition, a silent stream death, Servers on mixed versions.
  • Not run locally: api-test, the TinkerPop suites, the PD core, common and REST suites, the Store suites and the simple-cluster suite. CI covers them on this PR.

Does this PR potentially affect the following parts?

  • Dependencies (add/update license info & regenerate_known_dependencies.sh)
  • Modify configurations
  • The public API
  • Other affects: a schema call no longer fails when only the event publication failed after the commit (see "Changed behavior of schema calls"). During a burst of schema changes, schema reads on the other Servers slow down (see "Cost of an event"). New Java methods: a KvClient.listen() overload with a started callback and a default method MetaDriver.listen(key, consumer, onStarted). Each Server publishes one clear event per graph when it opens the graph.
  • Nope

Documentation Status

  • Doc - TODO: required documentation is pending; complete it before merging.
  • Doc - Done: documentation is included here or linked below.
  • Doc - No Need: no user-visible documentation is affected.

Documentation files in this PR or paired hugegraph-doc PR:

  • docs/server-runtime.md: new section "Schema caches across HStore Servers" with the guarantees and the limits
  • helm/hugegraph/docs/operations.md: the paragraph on index status across Pods

With several Servers on HStore, a schema change made through one
Server reached the others only for adds. Updates published no clear
event, removes published one only with task.sync_deletion=false, and
an event sent while a Server's PD watch was reconnecting was lost.
A Server that kept an old copy then wrote it back over newer schema.

Complete the existing clear event instead of adding a new mechanism:

- publish after every committed add, update, remove and store clear
  or truncate, and once when a graph is opened
- KvClient.listen() accepts a callback that runs each time a watch
  goes live; the Server clears its V2 schema caches when it runs
- a publish that fails after the schema write no longer fails the
  call; the graph is kept in a pending set and published again with
  backoff and when the watch is live again
- a generation counter and a lock on the shared cache keep a read,
  a listing or a write-through that started before a clear from
  caching its value after the clear
- every event clears at once, events are not merged

Not covered here: a write in flight on another Server while an index
is created can still miss the index, a writer that stops for good
between the schema write and its publish, two Servers changing the
same element at once, serving cached schema while cut off from PD,
and a watch stream that dies without an error. They are documented
in docs/server-runtime.md.
@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.46862% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.62%. Comparing base (0abd931) to head (7df011a).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...graph/backend/cache/CachedSchemaTransactionV2.java 93.62% 6 Missing and 7 partials ⚠️
...n/java/org/apache/hugegraph/meta/PdMetaDriver.java 50.00% 3 Missing ⚠️
...ain/java/org/apache/hugegraph/meta/MetaDriver.java 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3477      +/-   ##
============================================
+ Coverage     58.53%   58.62%   +0.08%     
- Complexity     2717     3130     +413     
============================================
  Files           779      780       +1     
  Lines         69439    69617     +178     
  Branches       9577     9603      +26     
============================================
+ Hits          40647    40810     +163     
- Misses        25101    25114      +13     
- Partials       3691     3693       +2     
Flag Coverage Δ *Carryforward flag
commons 96.37% <ø> (ø) Carriedforward from 2e75022
hstore 36.16% <58.60%> (+0.07%) ⬆️
pd 57.00% <100.00%> (+0.19%) ⬆️
server_hbase 42.60% <0.00%> (+0.05%) ⬆️ Carriedforward from 2e75022
server_memory 57.04% <91.62%> (+0.38%) ⬆️
server_rocksdb 42.90% <2.79%> (-0.19%) ⬇️
store 36.14% <0.00%> (-0.09%) ⬇️

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

A second pass over the schema cache clear path found four defects.

- A schema write is two or three PD puts. One that failed after the
  first put left the change partly in PD, published nothing and kept
  the writer's own copy. It now drops that copy, publishes, and still
  reports the error. The publish also runs if caching the written
  element fails.
- The generation was advanced before the caches were changed. A read
  running at that moment could note the new generation, take the old
  value from the id cache and copy it into the array cache afterwards,
  where it stayed until the next clear. The generation is now advanced
  as the last step of every change.
- A watch start during a running retry scheduled a second retry chain,
  which doubled the publishes and the backoff. Only the newest retry
  reschedules now.
- After a refused write-through the "whole list is cached" flag of
  that schema type was never set again, so each listing read PD until
  the next clear.

Tests: one per fix, plus tests for the publish order, the callback
passed by PdMetaDriver, the guard across transactions of one graph and
local invalidation events. The listing-lock test now holds a real
listing, and the concurrency test checks every read while it runs. A
V2 graph opened without PD no longer leaves a retry behind that fires
in a later test.
Follow-up to review comments on this change.

- MultiClusterSchemaCacheTest: the readers that run while an index is
  created were submitted and never checked, so a reader that stopped on
  an error left the test passing with fewer reads. Their futures are
  read once the index is created.
- docs/server-runtime.md: a PD watch that ends with a gRPC status the
  PD client does not retry is not started again, so that Server keeps
  its cached schema until it is restarted. This is how master behaves
  for every meta listener and is tracked in apache#3478.
- A TODO in the test's waitFor notes that the shared RestClient sets no
  timeouts.
Copilot AI balanced review requested due to automatic review settings November 13, 2026 13:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

8 open issues
What changed in this PR

This PR improves cross-Server schema cache correctness for HStore deployments by ensuring schema-cache-clear events are published on all schema write paths, retried on publish failure, and by clearing local V2 schema caches whenever the PD watch becomes live again (e.g., after disconnect/leader change), guarded by a per-graph generation to prevent stale refills.

Changes:

  • Add reliable schema cache invalidation + generation guard + failed-publish retry logic to CachedSchemaTransactionV2.
  • Extend PD watch plumbing (KvClient, MetaDriver/MetaManager/GraphMetaManager, PdMetaDriver) with a “watch started” callback.
  • Add extensive unit + multi-cluster tests and update operational docs for the new semantics/limits.
File Description
hugegraph-server/​hugegraph-core/​src/​main/​java/​org/​apache/​hugegraph/​backend/​cache/​CachedSchemaTransactionV2.java Publish clear events after writes (even partial failures), retry failed publishes, clear caches on watch start, add generation-based cache guard
hugegraph-pd/​hg-pd-client/​src/​main/​java/​org/​apache/​hugegraph/​pd/​client/​KvClient.java Add listen(..., startedCallback) and invoke callback on first live frame per watch lifecycle
hugegraph-server/​hugegraph-core/​src/​main/​java/​org/​apache/​hugegraph/​meta/​MetaDriver.java Add default listen(key, consumer, onStarted) API with fallback behavior
hugegraph-server/​hugegraph-core/​src/​main/​java/​org/​apache/​hugegraph/​meta/​MetaManager.java Plumb new schema-cache-clear listen overload with onStarted
hugegraph-server/​hugegraph-core/​src/​main/​java/​org/​apache/​hugegraph/​meta/​managers/​GraphMetaManager.java Add listenSchemaCacheClear(..., onStarted) overload
hugegraph-server/​hugegraph-core/​src/​main/​java/​org/​apache/​hugegraph/​meta/​PdMetaDriver.java Implement new listen(..., onStarted) using KvClient.listen overload
hugegraph-server/​hugegraph-test/​src/​main/​java/​org/​apache/​hugegraph/​unit/​cache/​CachedSchemaTransactionV2Test.java New in-memory “PD store” tests for publish ordering, retries, watch start clears, generation guard, and concurrency cases
hugegraph-pd/​hg-pd-test/​src/​main/​java/​org/​apache/​hugegraph/​pd/​client/​KvClientTest.java Add coverage for watch-start callback behavior across leader changes, stream errors, replaced watches
hugegraph-server/​hugegraph-test/​src/​main/​java/​org/​apache/​hugegraph/​unit/​cache/​CachedSchemaTransactionTest.java Adjust existing tests for watch-start callback registration and extra clear publish on graph open/store clear
hugegraph-server/​hugegraph-test/​src/​main/​java/​org/​apache/​hugegraph/​unit/​UnitTestSuite.java Include the new V2 cache unit test class
hugegraph-cluster-test/​hugegraph-clustertest-test/​src/​main/​java/​org/​apache/​hugegraph/​MultiClusterTest/​MultiClusterSuiteTest.java Include the new multi-cluster schema cache suite
hugegraph-cluster-test/​hugegraph-clustertest-test/​src/​main/​java/​org/​apache/​hugegraph/​MultiClusterTest/​MultiClusterSchemaCacheTest.java New multi-cluster integration tests validating cross-Server schema cache propagation and PD restart window
hugegraph-cluster-test/​hugegraph-clustertest-minicluster/​src/​main/​java/​org/​apache/​hugegraph/​ct/​node/​AbstractNodeWrapper.java Add restart support and improve “started” detection after restart by skipping old log bytes
hugegraph-cluster-test/​hugegraph-clustertest-minicluster/​src/​main/​java/​org/​apache/​hugegraph/​ct/​env/​AbstractEnv.java Add PD node restart/await helpers for cluster tests
helm/​hugegraph/​docs/​operations.md Update index-status behavior description post-fix and link to runtime limits
docs/​server-runtime.md Document new guarantees/limits for schema caches across HStore Servers

Review effort: Lite

Comment on lines +153 to +160
reads.add(readers.submit(() -> {
while (!stop.get()) {
Response r = client.target().path(VERTICES)
.queryParam("label", vl)
.queryParam("limit", 3).request().get();
r.readEntity(String.class);
}
}));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. readEntity(String.class) reads the whole entity and closes the entity stream, which is what releases the connection. That is the JAX-RS contract of readEntity for every type other than an input stream, so nothing stays open after each pass of the loop.

Comment on lines +372 to +386
response.readEntity(String.class);
return response.getStatus();
}

private static void ok(Response response) {
String content = response.readEntity(String.class);
Assert.assertEquals("Response with status " + response.getStatus() +
" and content " + content, 2, response.getStatus() / 100);
}

private static JsonNode json(Response response) throws Exception {
String content = response.readEntity(String.class);
Assert.assertEquals("Response with status " + response.getStatus() +
" and content " + content, 2, response.getStatus() / 100);
return MAPPER.readTree(content);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the reader loop above: status, ok and json each call readEntity(String.class), which closes the entity stream and releases the connection. No change.

Comment on lines +123 to +124
this.client.listen(key, (Consumer<WatchResponse>) consumer,
throwable -> { }, onStarted);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stop is already visible in the log: KvClient.stopWatch logs Watch for key {} stopped after a non-retryable error at ERROR, with the status, before it calls this handler. Retryable errors never reach the handler, they reconnect, so a line here would repeat that one.

What the Server should do once a watch has stopped for good is a real gap. It is the same on master for every meta listener and is tracked in #3478. 7df011a lists it under the limits in docs/server-runtime.md.

Comment on lines +80 to 82
// A V2 graph opened without a PD leaves a clear event to send again
CachedSchemaTransactionV2Test.resetPendingClearEvents();
this.graph = HugeFactory.open(FakeObjects.newConfig());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid as a style point, left as it is for now. Both classes test CachedSchemaTransactionV2 and sit in the same package, the helper resets static state of that one class, and a rename is caught at compile time. A utility class for one ten-line method would add a file without changing what the tests do. If a third test class needs the reset, that is the moment to move it out.

this.graph.close();
this.graph = null;
}
CachedSchemaTransactionV2Test.resetPendingClearEvents();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same point as the comment on line 82 of this file, answered there: left as it is until a third test class needs the helper.

Comment on lines +163 to 166
Scanner sc = new Scanner(log, StandardCharsets.UTF_8.name());
while (sc.hasNextLine()) {
String line = sc.nextLine();
if (line.contains(startLine)) return true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. The Scanner only wraps log, and log is the resource of the enclosing try, so the file handle is closed on every path, the early return true included. What the Scanner holds besides that is a heap buffer that goes away with the local variable. Listing it as a resource too would create it before log.skipNBytes(...), which is correct only because Scanner reads lazily.

Comment on lines +821 to +827
if (this.executor == null) {
this.executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "schema-cache-clear-retry");
thread.setDaemon(true);
return thread;
});
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, left as it is. The executor is created only after a first failed publish and is one daemon thread that sits idle once the pending set is empty, so it holds nothing and does not block shutdown. Shutting it down and creating it again would add a lifecycle to the retry path, which already has to handle a retry that runs while a watch start schedules a new one. The classes in Server core that need a scheduler each own one the same way. It would be worth changing if Server gets a shared scheduler for jobs like this.

Comment on lines +859 to +862
if (this.pending.isEmpty()) {
this.retryDelay = RETRY_MIN_DELAY;
return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same point as the comment on the executor field above, answered there: one idle daemon thread after a first failed publish, left as it is.

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.

[Bug] Stale schema caches on HStore Servers: updates stay invisible, appends get overwritten, new indexes miss data

2 participants