Skip to content

[controller][test] Add durable store update callback - #2950

Merged
misyel merged 11 commits into
linkedin:mainfrom
misyel:mkwong/store-update-callback
Sep 8, 2026
Merged

[controller][test] Add durable store update callback#2950
misyel merged 11 commits into
linkedin:mainfrom
misyel:mkwong/store-update-callback

Conversation

@misyel

@misyel misyel commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem Statement

The controller does not provide an extension point at the durable store-update execution boundary. Implementations that need to react to the final store state cannot participate in the existing admin-operation retry and checkpoint semantics without modifying the controller execution path.

Solution

Add a generic StoreUpdateHandler that is injectable through VeniceControllerContext and defaults to a no-op.

For parent-controller UPDATE_STORE operations, AdminExecutionTask invokes the handler with:

  • A read-only snapshot of the final store state.
  • An immutable set of config keys copied from the durable UPDATE_STORE message.

The config-key set remains stable across retries, so handlers can identify the requested changes without comparing pre-update and post-update snapshots. The callback runs after the metadata update succeeds and before the successful execution ID advances. Handler failures propagate, leaving the operation eligible for the admin channel's existing retry and restart behavior.

Existing constructor signatures delegate to the no-op handler, preserving compatibility for deployments and tests that do not configure the extension.

Code changes

  • Added new code behind a config. If so list the config names and their default values in the PR description.
  • Introduced new log lines.
    • Confirmed if logs need to be rate limited to avoid excessive logging.

No configuration or log changes are introduced. The handler defaults to StoreUpdateHandler.NO_OP.

Concurrency-Specific Checks

Both reviewer and PR author to verify

  • Code has no race conditions or thread safety issues.
  • Proper synchronization mechanisms (e.g., synchronized, RWLock) are used where needed.
  • No blocking calls inside critical sections that could lead to deadlocks or performance degradation.
  • Verified thread-safe collections are used (e.g., ConcurrentHashMap, CopyOnWriteArrayList).
  • Validated proper exception handling in multi-threaded code to avoid silent thread termination.

The callback runs outside the store repository lock. Implementations may be invoked concurrently for different stores and receive read-only store snapshots and immutable config-key sets. Exceptions intentionally propagate so failed operations are not checkpointed.

How was this PR tested?

  • Local code review completed.
  • New unit tests added.
  • New integration tests added.
  • Modified or extended existing tests.
  • Verified backward compatibility (if applicable).

The focused controller tests cover default and explicit handler injection, parent-only invocation, final-state delivery, immutable config-key delivery, ordering before checkpoint advancement, child-controller behavior, and failure propagation. They also verify that retries receive the same config-key set from the durable admin message.

The integration test injects separate handlers into a real parent-child controller topology, fails the parent handler's first callback attempt, and verifies that the admin operation retries and eventually delivers the final read-only store state with the same immutable config-key set. It also verifies that the child handler is never invoked.

Does this PR introduce any user-facing or breaking changes?

  • No. You can skip the rest of this section.
  • Yes. Clearly explain the behavior change and its impact.

🤖 Generated with GitHub Copilot CLI

Add a no-op-by-default callback that runs after parent store updates and
before execution checkpoint advancement. Propagate the callback through the
controller context and cover parent, child, ordering, and failure behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings July 30, 2026 20:43

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.

🟡 Not ready to approve

The new callback path performs store fetch/clone work even when the default NO_OP handler is used, introducing avoidable overhead in the common case.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Adds an injectable extension point for parent-controller UPDATE_STORE admin operations so external code can react to the final durable store state with the same retry/checkpoint semantics as the controller’s admin execution path.

Changes:

  • Introduces StoreUpdateHandler (default NO_OP) and wires it through VeniceControllerContextVeniceControllerVeniceControllerService → admin consumer pipeline.
  • Invokes the handler in AdminExecutionTask after a successful UPDATE_STORE metadata update and before advancing the successful execution ID (passing a ReadOnlyStore snapshot).
  • Adds/updates controller tests to validate defaulting, injection, parent-only invocation, ordering, and failure propagation.
File summaries
File Description
services/venice-controller/src/main/java/com/linkedin/venice/controller/StoreUpdateHandler.java Adds the new handler interface and NO_OP default.
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceControllerContext.java Allows injecting StoreUpdateHandler via context builder with NO_OP default.
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceController.java Plumbs the handler from context into controller service creation.
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceControllerService.java Threads the handler into AdminConsumerService creation while preserving existing constructor compatibility.
services/venice-controller/src/main/java/com/linkedin/venice/controller/kafka/consumer/AdminConsumerService.java Stores and passes the handler into AdminConsumptionTask (with backward-compatible constructor).
services/venice-controller/src/main/java/com/linkedin/venice/controller/kafka/consumer/AdminConsumptionTask.java Stores and passes the handler into AdminExecutionTask (with backward-compatible constructor).
services/venice-controller/src/main/java/com/linkedin/venice/controller/kafka/consumer/AdminExecutionTask.java Invokes the handler for parent UPDATE_STORE before checkpoint advancement using a read-only store snapshot.
services/venice-controller/src/test/java/com/linkedin/venice/controller/VeniceControllerContextTest.java Verifies default and explicit handler injection in controller context.
services/venice-controller/src/test/java/com/linkedin/venice/controller/kafka/consumer/AdminExecutionTaskTest.java Adds tests for handler invocation ordering, parent/child behavior, read-only snapshot, and failure propagation.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Exercise the callback through a real parent and child controller topology.
Verify that a first-attempt handler failure is retried, the final read-only
store state is delivered, and child controllers do not invoke the handler.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 30, 2026 22:12

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.

🟡 Not ready to approve

The new callback path in AdminExecutionTask performs an avoidable extra cloneStore() (and can NPE if getStore returns null), which is easy to fix and improves robustness/performance.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (1)

services/venice-controller/src/main/java/com/linkedin/venice/controller/kafka/consumer/AdminExecutionTask.java:397

  • AdminExecutionTask fetches the final store snapshot via admin.getStore(...).cloneStore() and then wraps it in ReadOnlyStore. In this code path admin.getStore() already returns a cloned/detached store (via the Helix store repositories), so the extra cloneStore() adds avoidable overhead and can become a triple-clone in the controller stack. Also, admin.getStore(...) may return null; today that would NPE on cloneStore() and obscure the failure reason.
    if (storeUpdated && isParentController) {
      Store finalStore = admin.getStore(clusterName, storeName).cloneStore();
      // Invoke before advancing checkpoints so callback failures leave the admin operation eligible for retry.
      storeUpdateHandler.handleStoreUpdate(clusterName, new ReadOnlyStore(finalStore));
    }
  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Provide the immutable config-key set from the durable UPDATE_STORE message
alongside the final read-only store snapshot. Verify the same update intent
is preserved across callback retries and that child controllers do not invoke
the handler.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 31, 2026 00:50

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.

🟡 Not ready to approve

The default NO_OP path still performs an extra store fetch/clone on every parent UPDATE_STORE, which is an avoidable performance/operational regression.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (1)

services/venice-controller/src/main/java/com/linkedin/venice/controller/kafka/consumer/AdminExecutionTask.java:401

  • The store update callback path currently fetches and clones the store even when the injected handler is the default StoreUpdateHandler.NO_OP, which adds an extra metadata read and cloneStore() cost to every parent UPDATE_STORE operation even when no extension is configured. This can be a noticeable regression since the default behavior should be truly no-op.
    if (storeUpdated && isParentController) {
      Store finalStore = admin.getStore(clusterName, storeName).cloneStore();
      // Invoke before advancing checkpoints so callback failures leave the admin operation eligible for retry.
      storeUpdateHandler.handleStoreUpdate(clusterName, new ReadOnlyStore(finalStore), updatedConfigs);
    }
  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Avoid fetching and cloning final store metadata when the default no-op
handler is configured. Preserve checkpoint advancement and cover the parent
UPDATE_STORE regression that failed the controller unit-test matrices.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 31, 2026 17:18

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.

🟡 Not ready to approve

The new callback path dereferences admin.getStore(...).cloneStore() without a null-check, risking an NPE with an unclear failure mode during admin-op processing.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

services/venice-controller/src/main/java/com/linkedin/venice/controller/kafka/consumer/AdminExecutionTask.java:401

  • admin.getStore(clusterName, storeName) can return null (e.g., store deleted or metadata temporarily unavailable). Calling .cloneStore() without a null-check will throw an NPE and obscure the actual failure; it’s better to fail with an explicit exception so the admin op retries with a clear reason.
    if (storeUpdated && isParentController && !storeUpdateHandler.isNoOp()) {
      Store finalStore = admin.getStore(clusterName, storeName).cloneStore();
      // Invoke before advancing checkpoints so callback failures leave the admin operation eligible for retry.
      storeUpdateHandler.handleStoreUpdate(clusterName, new ReadOnlyStore(finalStore), updatedConfigs);
    }
  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@misyel
misyel marked this pull request as ready for review July 31, 2026 17:27
@kvargha
kvargha requested a balanced review from Copilot August 25, 2026 22:27

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Resolve conflict in AdminExecutionTaskTest.java by keeping test methods
from both branches: the durable store-update-callback tests and the
pubSubEncryptionKeyUrn tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 22:01

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.

🟡 Changes recommended

Callback exceptions can accidentally trigger the existing missing-store auto-skip path, permanently bypassing a failed handler.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 11/11 changed files
  • Comments generated: 2
  • Review effort level: Balanced

- Wrap handler failures in a dedicated StoreUpdateCallbackException so a
  handler throwing VeniceNoStoreException cannot be misclassified by
  AdminConsumptionTask as the UPDATE_STORE target being absent, which would
  permanently auto-skip an operation whose durable update already succeeded.
- Fail explicitly instead of NPE'ing when the final store snapshot is missing.
- Document the handler contract: concurrent invocation across stores,
  at-least-once/idempotency on checkpoint retry, no reentrant controller
  operations (the admin-message lock is still held by the originating caller),
  and that an empty updatedConfigs set means a replicate-all update rather
  than "nothing changed".
- Parameterize the UPDATE_STORE test fixture so config-key assertions use the
  production explicit-config-list shape (replicateAllConfigs = false), and add
  a replicate-all case covering the empty-set semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 2, 2026 17:31
@misyel

misyel commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the outstanding review comments in 2a389b9.

Handler exceptions misclassified as missing-store (auto-skip)
Added StoreUpdateCallbackException and wrapped every handler failure in it before it leaves AdminExecutionTask. AdminConsumptionTask inspects ExecutionException.getCause() for VeniceNoStoreException, so a handler throwing that type can no longer trigger the UPDATE_STORE auto-skip path for an operation whose durable update already succeeded. Covered by testStoreUpdateHandlerVeniceNoStoreExceptionIsWrapped.

NPE on a null final store
admin.getStore(...) is now null-checked and fails with an explicit, retriable message instead of NPE'ing on cloneStore(). Covered by testStoreUpdateHandlerFailsWhenFinalStoreMissing.

Missing handler contract
The interface javadoc now states the three semantics implementations depend on: concurrent invocation across different stores, at-least-once delivery when checkpointing is retried, and no reentrant controller operations. On the last one, the originating updateStore caller still holds the per-store admin-message lock while the handler runs, so a handler that synchronously issues another admin operation for the same store would block on that lock until timeout.

Unrealistic replicate-all fixture
createUpdateStoreWrapper is now parameterized. The default keeps replicateAllConfigs = true with an empty updatedConfigsList, matching what StoreConfigUpdater emits. Config-key assertions use the new createExplicitConfigListUpdateStoreWrapper (replicateAllConfigs = false with a non-empty list). Added testParentStoreUpdateHandlerReceivesEmptyConfigSetForReplicateAllUpdate, and the javadoc now defines an empty set as "replicate-all, so fall back to the store snapshot" rather than "nothing changed".

The NO_OP fast path from the earlier round is unchanged: isNoOp() still short-circuits the fetch and clone entirely.

The prior IntegrationTests_18 failure was a Gradle plugin download error (spotless-plugin-gradle-6.12.0.jar could not be resolved), not a code failure. This push re-triggers CI.

Testing Done

  • AdminExecutionTaskTest and VeniceControllerContextTest pass locally (43 tests).
  • :internal:venice-test-common:compileIntegrationTestJava succeeds.
  • spotlessApply clean.

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.

🔵 Needs a closer look

It modifies durable admin retry and checkpoint behavior, warranting final human validation despite strong test coverage.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

…ughput

configs

Update StoreUpdateHandlerIntegrationTest to exercise the durable
UPDATE_STORE callback with the nearline write throughput quota configs
(throughput_quota_in_bytes / throughput_quota_in_records) instead of
read quota, mirroring the internal StoreUpdateHandler use case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 4, 2026 20:38

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.

🟡 Changes recommended

Exception classification and same-store concurrency behavior need to be addressed in the callback contract.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

services/venice-controller/src/main/java/com/linkedin/venice/controller/StoreUpdateHandler.java:16

  • The concurrency contract is too narrow. AdminConsumptionTask tracks scheduled stores only within one cycle and invokeAll cancels timed-out futures; if a handler does not terminate on interruption, the next cycle can invoke the same queued operation for the same store concurrently. Implementations therefore need to tolerate concurrent duplicate invocations, not only calls for different stores.
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Balanced

…overridable probe

The store update callback was skipped via storeUpdateHandler.isNoOp(),
which is user-overridable and executed outside the try/catch that wraps
handler failures in StoreUpdateCallbackException. A custom isNoOp() that
threw (e.g. VeniceNoStoreException) would escape directly to
AdminConsumptionTask and risk auto-skipping an already-applied
UPDATE_STORE, exactly what StoreUpdateCallbackException prevents.

Gate on the StoreUpdateHandler.NO_OP identity instead and drop the
overridable isNoOp() probe entirely, so no handler-supplied code runs
outside the callback exception boundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 5, 2026 00:44

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.

🟡 Changes recommended

Retriable callback failures are misclassified operationally, and the concurrency contract omits possible overlapping same-operation retries.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

services/venice-controller/src/main/java/com/linkedin/venice/controller/StoreUpdateHandler.java:16

  • The same-store serialization guarantee is incomplete. AdminConsumptionTask.invokeAll cancels a timed-out task, but a handler that does not terminate on interruption can continue running; the next cycle rebuilds its local storesWithScheduledTask set and can submit the same store again (AdminConsumptionTask.java:559–614). This allows overlapping callbacks for the same durable operation, so implementations following the stated “different stores” contract can still race. Either prevent rescheduling while the prior task remains active, or explicitly require thread safety for overlapping same-operation retries.
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Balanced

…retries aren't logged as errors

StoreUpdateCallbackException extended VeniceException, so it bypassed the
VeniceRetriableException branch in AdminExecutionTask.call. Every
intentional callback retry was therefore counted as a non-retriable
failure and logged at ERROR each cycle. Extend VeniceRetriableException
instead; the dedicated wrapper type still prevents a nested
VeniceNoStoreException from reaching the missing-store auto-skip
classifier.

Strengthen testStoreUpdateHandlerFailureLeavesExecutionIdUnadvanced to
assert the wrapper is retriable and that the retriable consumption stat
is recorded (not the non-retriable one).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 16:31

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.

🔵 Needs a closer look

Region-filtered updates can invoke the handler even when the parent’s local metadata update was skipped.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

services/venice-controller/src/main/java/com/linkedin/venice/controller/kafka/consumer/AdminExecutionTask.java:331

  • storeUpdated is set even when the durable update was not applied locally. StoreConfigUpdater.applyOnChild returns without updating metadata when regionsFilter excludes the current region (StoreConfigUpdater.java:261-273), but this path still invokes the parent handler with an unchanged store and config keys that were never applied. This contradicts the callback's post-update/final-state contract. Propagate whether the local update was applied (or check the region filter consistently) and skip the callback for filtered-out updates; add a targeted-region test.
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Since StoreUpdateCallbackException now extends VeniceRetriableException,
`thrown instanceof VeniceRetriableException` is statically always true
and tripped spotbugs BC_VACUOUS_INSTANCEOF. The retriable classification
is already asserted by verifying recordFailedRetriableAdminConsumption is
called and recordFailedAdminConsumption is not, so remove the redundant
instanceof check and its now-unused import.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 17:03

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.

🟡 Changes recommended

Retry classification and same-store concurrency semantics need correction before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

services/venice-controller/src/main/java/com/linkedin/venice/controller/StoreUpdateHandler.java:13

  • The concurrency contract is narrower than the executor behavior. AdminConsumptionTask cancels a worker after processingCycleTimeoutInMs; if a handler does not terminate on interruption, the next cycle can schedule the same queued operation while the old invocation is still running. Implementations therefore cannot assume same-store serialization, so this contract should explicitly cover concurrent invocations for the same store/operation.
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Balanced

The missing-final-store guard after a successful UPDATE_STORE deliberately
retains the queue entry for retry, but throwing plain VeniceException routed
it through the non-retriable path in AdminExecutionTask.call (ERROR log +
recordFailedAdminConsumption). Throw VeniceRetriableException so it is
classified consistently with callback retries. Strengthen the test to assert
the retriable type and the retriable stats classification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 17:18

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.

🔵 Needs a closer look

The synchronous callback changes durable admin retry and checkpoint behavior, warranting final human review despite comprehensive tests.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@misyel
misyel merged commit a34b490 into linkedin:main Sep 8, 2026
117 checks passed
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.

3 participants