Skip to content

Add DurableStateStore TCK to persistence-tck module#2833

Draft
pjfanning wants to merge 3 commits intoapache:mainfrom
pjfanning:copilot/add-durable-state-store-support
Draft

Add DurableStateStore TCK to persistence-tck module#2833
pjfanning wants to merge 3 commits intoapache:mainfrom
pjfanning:copilot/add-durable-state-store-support

Conversation

@pjfanning
Copy link
Copy Markdown
Member

Summary

See #2831

Adds a standardised Technology Compatibility Kit (TCK) for DurableStateStore implementations to the persistence-tck module. This allows plugin authors (such as those working on pekko-persistence-jdbc, pekko-persistence-r2dbc, and pekko-persistence-cassandra) to include a consistent test suite in their projects.

Changes

persistence-tck/src/main/scala/org/apache/pekko/persistence/CapabilityFlags.scala

  • Added DurableStateStoreCapabilityFlags trait with a supportsDeleteWithRevisionCheck capability flag, following the same pattern as JournalCapabilityFlags and SnapshotStoreCapabilityFlags.

persistence-tck/src/main/scala/org/apache/pekko/persistence/state/DurableStateStoreSpec.scala (new)

Scala TCK spec that plugin authors extend. Tests:

  • getObject on a non-existing persistence ID returns None
  • upsertObject followed by getObject returns the stored value and correct revision
  • Upserting twice updates the state (last write wins)
  • deleteObject(persistenceId, revision) makes the state unavailable
  • Different persistence IDs are independent of one another
  • Optional (supportsDeleteWithRevisionCheck): deleting with a mismatched revision fails and leaves the original state intact

persistence-tck/src/main/scala/org/apache/pekko/persistence/japi/state/JavaDurableStateStoreSpec.scala (new)

Java/JUnit-consumable wrapper around DurableStateStoreSpec, following the same pattern as JavaJournalSpec and JavaSnapshotStoreSpec.

persistence-testkit/src/test/scala/…/PersistenceTestKitDurableStateStoreTCKSpec.scala (new)

Reference test implementation that exercises the new TCK using the existing in-memory PersistenceTestKitDurableStateStore, verifying the TCK itself works correctly.

Usage by plugin authors

class MyPluginDurableStateStoreSpec
    extends DurableStateStoreSpec(
      ConfigFactory.parseString("""
        pekko.persistence.state.plugin = "my.plugin.durable-state"
        my.plugin.durable-state.class = "com.example.MyDurableStateStoreProvider"
      """)
    ) {
  override protected def supportsDeleteWithRevisionCheck: CapabilityFlag = CapabilityFlag.on()
}

Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/77803e79-3631-4111-98a8-4a1cf6f7beca

@pjfanning pjfanning marked this pull request as draft April 3, 2026 10:46
He-Pin

This comment was marked as duplicate.

Copy link
Copy Markdown
Member

@He-Pin He-Pin left a comment

Choose a reason for hiding this comment

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

Deep CR: PR #2833 - Add DurableStateStore TCK to persistence-tck module

Architecture Review

This PR fills a notable gap in the Pekko persistence TCK. Prior to this change, only Journal and SnapshotStore had standardized TCK specs. DurableStateStore plugin authors had no reference test suite to validate their implementations against.

Design follows established patterns:

  • DurableStateStoreCapabilityFlags mirrors JournalCapabilityFlags and SnapshotStoreCapabilityFlags
  • DurableStateStoreSpec follows the same structure as JournalSpec (extends PluginSpec, uses MayVerb/OptionalTests)
  • JavaDurableStateStoreSpec follows the identical boilerplate pattern as JavaJournalSpec
  • The reference test (PersistenceTestKitDurableStateStoreTCKSpec) validates the TCK itself works

Capability Flag Design: The supportsDeleteWithRevisionCheck flag is well-chosen as the first capability flag. It represents a behavioral difference that not all stores support (some may not track revisions strictly). The OptionalTests mixin pattern handles this correctly - tests with CapabilityFlag.off() are skipped rather than failed.

Binary Compatibility

New public classes and traits are added to persistence-tck:

  • DurableStateStoreCapabilityFlags trait
  • DurableStateStoreSpec abstract class
  • JavaDurableStateStoreSpec class

These are all additive changes to the TCK module. Existing code is not affected. No MiMa exclusions needed.

Test Coverage Analysis

The TCK covers 5 mandatory tests and 1 optional:

Mandatory tests:

  1. not find a non-existing object - basic read miss
  2. persist a state and retrieve it - basic write/read roundtrip
  3. update a state - revision incrementing
  4. delete a state - deletion
  5. handle different persistence IDs independently - isolation

Optional test:
6. fail to delete a state when the revision does not match - optimistic locking

Missing test scenarios:

  • deleteObject with a revision that matches - should succeed. Currently only tests deletion with revision=2L after upsert with revision=1L, which implicitly tests the happy path, but does not explicitly verify that deleteObject(pid, 1L) after upsertObject(pid, 1L, ...) behaves correctly (the revision mismatch case tests deleteObject(pid, 99L)).
  • Concurrent upsert with same revision - what happens if two clients try to upsert the same persistence ID with the same revision? This is a key scenario for distributed systems.
  • Large state values - no test for state objects that exceed typical size limits.
  • getObject after deleteObject with wrong revision - the optional test verifies the delete fails, but does not verify the state is still accessible with the original revision. Actually wait, it does - the test checks result.value shouldBe Some(value) and result.revision shouldBe 1L. Good.
  • Revision ordering - what happens if you upsert with revision 5 then revision 3? Should revision 3 be rejected or accepted?

Code Quality

durableStateStore() default plugin ID: The method uses an empty string "" as the plugin ID:

DurableStateStoreRegistry(system).durableStateStoreFor[DurableStateUpdateStore[Any]]("")

This is actually correct behavior - when an empty string is passed, DurableStateStoreRegistry falls back to the default plugin configured under pekko.persistence.state.plugin. This matches how JournalSpec works (it uses the default journal plugin from config). Plugin authors configure the plugin path in their test config, and the TCK picks it up automatically.

intercept[Exception]: The optional test uses intercept[Exception] which is broad. However, this is intentional - different store implementations may throw different exception types (e.g., OptimisticLockingException, IllegalStateException, or a store-specific exception). The TCK cannot mandate a specific exception type without constraining plugin APIs. This is acceptable for a TCK.

Java API Coverage

The JavaDurableStateStoreSpec provides a Java-consumable TCK following the established pattern. The boilerplate override def methods (14 of them) are necessary for JUnit compatibility with ScalaTest. This is consistent with JavaJournalSpec and JavaSnapshotStoreSpec.

Suggestions

  1. Consider adding a test for deleteObject with a matching revision to explicitly verify the happy path deletion.
  2. Consider documenting in the scaladoc what plugin authors should expect regarding revision ordering behavior (is it enforced by the TCK or left to the implementation?).
  3. The PR description references #2831 - ensure that issue is linked properly in the final PR.

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