This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new 26c5086c7c docs: how to write a custom Durable State store plugin 
(#3274)
26c5086c7c is described below

commit 26c5086c7cb41466a84ed27fc0430119d8e91718
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Wed Jul 1 21:13:34 2026 +0800

    docs: how to write a custom Durable State store plugin (#3274)
    
    * docs: how to write a custom Durable State store plugin
    
    Motivation:
    Pekko Persistence supports pluggable Durable State storage backends, but
    there was no documentation explaining how to implement one. Plugin authors
    had to read the source to discover the `DurableStateUpdateStore` and
    `DurableStateStoreProvider` SPI.
    
    Modification:
    Add a "Building a storage backend for Durable State" paradox page with
    Scala and Java examples (plugin API, provider, and minimal activation
    configuration), backed by a runnable doc spec. Also fix a stale scaladoc on
    `DurableStateStoreProvider.scaladslDurableStateStore` that incorrectly
    referred to `ReadJournal` instead of `DurableStateStore`.
    
    Improvements over the upstream example: the Java 
`MyJavaStateStore.upsertObject`
    uses the generic type parameter `A` instead of `Object`, so the snippet is a
    correct, type-safe template when adapted to a concrete state type; and the
    provider uses the diamond operator to avoid raw-type usage.
    
    Deliberately divergent from upstream: Pekko's `DurableStateUpdateStore`
    already documents `@param revision` (better than the upstream `@param seqNr`
    fix), and the upstream `//#plugin-api` markers on the real trait are not
    referenced by the docs (the snippet is taken from the example), so they are
    omitted to avoid clutter in production sources.
    
    Result:
    Durable State plugin authors have a step-by-step guide with compiling,
    tested Scala and Java examples.
    
    Tests:
    - sbt "docs/testOnly docs.persistence.state.PersistenceStatePluginDocSpec" 
(2/2 passed)
    - sbt "docs/paradox" (success; all snippets resolve)
    
    References:
    Upstream: akka/akka-core@1ce8748239e968f1fba15d23213130e5be98945b 
(akka#31973), which is now Apache licensed.
    
    * docs: use pekko import shorthand in state store plugin examples
    
    Motivation:
    PR review requested using the project's standard import convention:
    `import org.apache.pekko` followed by `import pekko.*` shorthand,
    and keeping Pekko imports grouped without non-Pekko imports in between.
    
    Modification:
    - MyStateStore.scala: switched to `import org.apache.pekko` + `pekko.*`
      shorthand, moved `com.typesafe.config.Config` out of the Pekko import 
block
    - PersistenceStatePluginDocSpec.scala: same import style refactor,
      preserved //#plugin-imports snippet markers for Paradox docs
    
    Result:
    Import style matches project convention (e.g. SnapshotSpec.scala).
    Compilation, Paradox doc generation, and tests all pass.
    
    Tests:
    - sbt "docs / Compile / compile" — passed
    - sbt "docs/paradox" — passed, #plugin-imports snippet verified in HTML
    - sbt "docs / Test / testOnly 
docs.persistence.state.PersistenceStatePluginDocSpec" — 2/2 passed
    
    References:
    Refs #3274
---
 .../docs/persistence/state/MyJavaStateStore.java   | 77 ++++++++++++++++++++
 .../state/MyJavaStateStoreProvider.java            | 45 ++++++++++++
 .../paradox/durable-state/state-store-plugin.md    | 45 ++++++++++++
 .../typed/index-persistence-durable-state.md       |  1 +
 .../docs/persistence/state/MyStateStore.scala      | 71 ++++++++++++++++++
 .../state/PersistenceStatePluginDocSpec.scala      | 83 ++++++++++++++++++++++
 .../state/DurableStateStoreProvider.scala          |  2 +-
 7 files changed, 323 insertions(+), 1 deletion(-)

diff --git a/docs/src/main/java/docs/persistence/state/MyJavaStateStore.java 
b/docs/src/main/java/docs/persistence/state/MyJavaStateStore.java
new file mode 100644
index 0000000000..b643557253
--- /dev/null
+++ b/docs/src/main/java/docs/persistence/state/MyJavaStateStore.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package docs.persistence.state;
+
+// #plugin-imports
+
+import org.apache.pekko.Done;
+import org.apache.pekko.actor.ExtendedActorSystem;
+import org.apache.pekko.persistence.state.javadsl.DurableStateUpdateStore;
+import org.apache.pekko.persistence.state.javadsl.GetObjectResult;
+import com.typesafe.config.Config;
+import java.util.concurrent.CompletionStage;
+
+// #plugin-imports
+
+// #state-store-plugin-api
+class MyJavaStateStore<A> implements DurableStateUpdateStore<A> {
+
+  private ExtendedActorSystem system;
+  private Config config;
+  private String cfgPath;
+
+  public MyJavaStateStore(ExtendedActorSystem system, Config config, String 
cfgPath) {
+    this.system = system;
+    this.config = config;
+    this.cfgPath = cfgPath;
+  }
+
+  /** Returns the current state for the given persistence id. */
+  @Override
+  public CompletionStage<GetObjectResult<A>> getObject(String persistenceId) {
+    // implement getObject here
+    return null;
+  }
+
+  /**
+   * Will persist the latest state. If it's a new persistence id, the record 
will be inserted.
+   *
+   * <p>In case of an existing persistence id, the record will be updated only 
if the revision
+   * number of the incoming record is 1 more than the already existing record. 
Otherwise persist
+   * will fail.
+   */
+  @Override
+  public CompletionStage<Done> upsertObject(
+      String persistenceId, long revision, A value, String tag) {
+    // implement upsertObject here
+    return null;
+  }
+
+  /** Deprecated. Use the deleteObject overload with revision instead. */
+  @Override
+  public CompletionStage<Done> deleteObject(String persistenceId) {
+    return deleteObject(persistenceId, 0);
+  }
+
+  /**
+   * Will delete the state by setting it to the empty state and the revision 
number will be
+   * incremented by 1.
+   */
+  @Override
+  public CompletionStage<Done> deleteObject(String persistenceId, long 
revision) {
+    // implement deleteObject here
+    return null;
+  }
+}
+// #state-store-plugin-api
diff --git 
a/docs/src/main/java/docs/persistence/state/MyJavaStateStoreProvider.java 
b/docs/src/main/java/docs/persistence/state/MyJavaStateStoreProvider.java
new file mode 100644
index 0000000000..cfa42fce7a
--- /dev/null
+++ b/docs/src/main/java/docs/persistence/state/MyJavaStateStoreProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package docs.persistence.state;
+
+import org.apache.pekko.actor.ExtendedActorSystem;
+import org.apache.pekko.persistence.state.DurableStateStoreProvider;
+import org.apache.pekko.persistence.state.scaladsl.DurableStateStore;
+import com.typesafe.config.Config;
+
+// #plugin-provider
+class MyJavaStateStoreProvider implements DurableStateStoreProvider {
+
+  private ExtendedActorSystem system;
+  private Config config;
+  private String cfgPath;
+
+  public MyJavaStateStoreProvider(ExtendedActorSystem system, Config config, 
String cfgPath) {
+    this.system = system;
+    this.config = config;
+    this.cfgPath = cfgPath;
+  }
+
+  @Override
+  public DurableStateStore<Object> scaladslDurableStateStore() {
+    return new MyStateStore<>(this.system, this.config, this.cfgPath);
+  }
+
+  @Override
+  public org.apache.pekko.persistence.state.javadsl.DurableStateStore<Object>
+      javadslDurableStateStore() {
+    return new MyJavaStateStore<>(this.system, this.config, this.cfgPath);
+  }
+}
+// #plugin-provider
diff --git a/docs/src/main/paradox/durable-state/state-store-plugin.md 
b/docs/src/main/paradox/durable-state/state-store-plugin.md
new file mode 100644
index 0000000000..584ef30846
--- /dev/null
+++ b/docs/src/main/paradox/durable-state/state-store-plugin.md
@@ -0,0 +1,45 @@
+# Building a storage backend for Durable State
+
+Storage backends for state stores are pluggable in the Pekko persistence 
extension.
+This documentation describes how to build a new storage backend for durable 
state.
+
+Applications can provide their own plugins by implementing a plugin API and 
activating them by configuration.
+Plugin development requires the following imports:
+
+Scala
+:  @@snip 
[PersistenceStatePluginDocSpec.scala](/docs/src/test/scala/docs/persistence/state/PersistenceStatePluginDocSpec.scala)
 { #plugin-imports }
+
+Java
+:  @@snip 
[MyJavaStateStore.java](/docs/src/main/java/docs/persistence/state/MyJavaStateStore.java)
 { #plugin-imports }
+
+## State Store plugin API
+
+A durable state store plugin extends `DurableStateUpdateStore`.
+
+`DurableStateUpdateStore` is an interface and the methods to be implemented 
are:
+
+Scala
+:  @@snip 
[MyStateStore.scala](/docs/src/main/scala/docs/persistence/state/MyStateStore.scala)
 { #plugin-api }
+
+Java
+:  @@snip 
[MyJavaStateStore.java](/docs/src/main/java/docs/persistence/state/MyJavaStateStore.java)
 { #state-store-plugin-api }
+
+## State Store provider
+
+A `DurableStateStoreProvider` needs to be implemented to be able to create the 
plugin itself:
+
+Scala
+:  @@snip 
[MyStateStore.scala](/docs/src/main/scala/docs/persistence/state/MyStateStore.scala)
 { #plugin-provider }
+
+Java
+:  @@snip 
[MyJavaStateStoreProvider.java](/docs/src/main/java/docs/persistence/state/MyJavaStateStoreProvider.java)
 { #plugin-provider }
+
+## Configure the State Store
+
+A durable state store plugin can be activated with the following minimal 
configuration:
+
+Scala
+:  @@snip 
[PersistenceStatePluginDocSpec.scala](/docs/src/test/scala/docs/persistence/state/PersistenceStatePluginDocSpec.scala)
 { #plugin-config-scala }
+
+Java
+:  @@snip 
[PersistenceStatePluginDocSpec.scala](/docs/src/test/scala/docs/persistence/state/PersistenceStatePluginDocSpec.scala)
 { #plugin-config-java }
diff --git a/docs/src/main/paradox/typed/index-persistence-durable-state.md 
b/docs/src/main/paradox/typed/index-persistence-durable-state.md
index f96fe1eb92..aff43ca692 100644
--- a/docs/src/main/paradox/typed/index-persistence-durable-state.md
+++ b/docs/src/main/paradox/typed/index-persistence-durable-state.md
@@ -12,5 +12,6 @@ project.description: Durable state with Apache Pekko 
Persistence enables actors
 * [persistence-style](durable-state/persistence-style.md)
 * [cqrs](durable-state/cqrs.md)
 * [persistence-query](../durable-state/persistence-query.md)
+* [state-store-plugin](../durable-state/state-store-plugin.md)
 
 @@@
diff --git a/docs/src/main/scala/docs/persistence/state/MyStateStore.scala 
b/docs/src/main/scala/docs/persistence/state/MyStateStore.scala
new file mode 100644
index 0000000000..c5f585f838
--- /dev/null
+++ b/docs/src/main/scala/docs/persistence/state/MyStateStore.scala
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package docs.persistence.state
+
+import org.apache.pekko
+import pekko.Done
+import pekko.actor.ExtendedActorSystem
+import pekko.persistence.state.{ DurableStateStoreProvider, 
DurableStateStoreRegistry }
+import pekko.persistence.state.javadsl.{ DurableStateStore => 
JDurableStateStore }
+import pekko.persistence.state.scaladsl.{ DurableStateStore, 
DurableStateUpdateStore, GetObjectResult }
+
+import com.typesafe.config.Config
+
+import scala.concurrent.Future
+
+//#plugin-provider
+class MyStateStoreProvider(system: ExtendedActorSystem, config: Config, 
cfgPath: String)
+    extends DurableStateStoreProvider {
+
+  /**
+   * The `DurableStateStore` implementation for the Scala API.
+   * This corresponds to the instance that is returned by 
[[DurableStateStoreRegistry#durableStateStoreFor]].
+   */
+  override def scaladslDurableStateStore(): DurableStateStore[Any] = new 
MyStateStore(system, config, cfgPath)
+
+  /**
+   * The `DurableStateStore` implementation for the Java API.
+   * This corresponds to the instance that is returned by 
[[DurableStateStoreRegistry#getDurableStateStoreFor]].
+   */
+  override def javadslDurableStateStore(): JDurableStateStore[AnyRef] = new 
MyJavaStateStore(system, config, cfgPath)
+}
+//#plugin-provider
+
+//#plugin-api
+class MyStateStore[A](system: ExtendedActorSystem, config: Config, cfgPath: 
String) extends DurableStateUpdateStore[A] {
+
+  /**
+   * Will persist the latest state. If it's a new persistence id, the record 
will be inserted.
+   *
+   * In case of an existing persistence id, the record will be updated only if 
the revision
+   * number of the incoming record is 1 more than the already existing record. 
Otherwise persist will fail.
+   */
+  override def upsertObject(persistenceId: String, revision: Long, value: A, 
tag: String): Future[Done] = ???
+
+  /**
+   * Deprecated. Use the deleteObject overload with revision instead.
+   */
+  override def deleteObject(persistenceId: String): Future[Done] = 
deleteObject(persistenceId, 0)
+
+  /**
+   * Will delete the state by setting it to the empty state and the revision 
number will be incremented by 1.
+   */
+  override def deleteObject(persistenceId: String, revision: Long): 
Future[Done] = ???
+
+  /**
+   * Returns the current state for the given persistence id.
+   */
+  override def getObject(persistenceId: String): Future[GetObjectResult[A]] = 
???
+}
+//#plugin-api
diff --git 
a/docs/src/test/scala/docs/persistence/state/PersistenceStatePluginDocSpec.scala
 
b/docs/src/test/scala/docs/persistence/state/PersistenceStatePluginDocSpec.scala
new file mode 100644
index 0000000000..44ae411b4e
--- /dev/null
+++ 
b/docs/src/test/scala/docs/persistence/state/PersistenceStatePluginDocSpec.scala
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2009-2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package docs.persistence.state
+
+import org.apache.pekko
+import pekko.Done
+import pekko.actor.{ ActorSystem, ExtendedActorSystem }
+//#plugin-imports
+import pekko.persistence.state.scaladsl.DurableStateUpdateStore
+import pekko.persistence.state.scaladsl.GetObjectResult
+//#plugin-imports
+import pekko.persistence.Persistence
+import pekko.persistence.state.DurableStateStoreRegistry
+import pekko.testkit.TestKit
+
+import com.typesafe.config._
+import docs.persistence
+import org.scalatest.wordspec.AnyWordSpec
+
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+class PersistenceStatePluginDocSpec extends AnyWordSpec {
+
+  val providerConfigJava =
+    """
+    //#plugin-config-java
+    # Path to the state store plugin to be used
+    pekko.persistence.state.plugin = "my-java-state-store"
+
+    # My custom state store plugin
+    my-java-state-store {
+      # Class name of the plugin.
+      class = "docs.persistence.state.MyJavaStateStoreProvider"
+    }
+    //#plugin-config-java
+  """
+
+  val providerConfig =
+    """
+    //#plugin-config-scala
+    # Path to the state store plugin to be used
+    pekko.persistence.state.plugin = "my-state-store"
+
+    # My custom state store plugin
+    my-state-store {
+      # Class name of the plugin.
+      class = "docs.persistence.state.MyStateStoreProvider"
+    }
+    //#plugin-config-scala
+      """
+
+  "it should work for scala" in {
+    val system = ActorSystem("PersistenceStatePluginDocSpec", 
ConfigFactory.parseString(providerConfig))
+    try {
+      Persistence(system)
+      
DurableStateStoreRegistry(system).durableStateStoreFor[DurableStateUpdateStore[Any]]("my-state-store")
+    } finally {
+      TestKit.shutdownActorSystem(system, 10.seconds, false)
+    }
+  }
+
+  "it should work for java" in {
+    val system = ActorSystem("PersistenceStatePluginDocSpec", 
ConfigFactory.parseString(providerConfigJava))
+    try {
+      Persistence(system)
+      
DurableStateStoreRegistry(system).durableStateStoreFor[DurableStateUpdateStore[Any]]("my-java-state-store")
+    } finally {
+      TestKit.shutdownActorSystem(system, 10.seconds, false)
+    }
+  }
+}
diff --git 
a/persistence/src/main/scala/org/apache/pekko/persistence/state/DurableStateStoreProvider.scala
 
b/persistence/src/main/scala/org/apache/pekko/persistence/state/DurableStateStoreProvider.scala
index b765645313..6373636aa1 100644
--- 
a/persistence/src/main/scala/org/apache/pekko/persistence/state/DurableStateStoreProvider.scala
+++ 
b/persistence/src/main/scala/org/apache/pekko/persistence/state/DurableStateStoreProvider.scala
@@ -24,7 +24,7 @@ package org.apache.pekko.persistence.state
 trait DurableStateStoreProvider {
 
   /**
-   * The `ReadJournal` implementation for the Scala API.
+   * The `DurableStateStore` implementation for the Scala API.
    * This corresponds to the instance that is returned by 
[[org.apache.pekko.persistence.state.DurableStateStoreRegistry.durableStateStoreFor
 DurableStateStoreRegistry#durableStateStoreFor]].
    */
   def scaladslDurableStateStore(): scaladsl.DurableStateStore[Any]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to