Skip to content

Commit 67544c3

Browse files
committed
Updates
- include feedback - more config parameters * idle time * fromSequenceNr - user docs - tests
1 parent 8154b8e commit 67544c3

File tree

8 files changed

+310
-37
lines changed

8 files changed

+310
-37
lines changed

akka-docs/rst/java/persistence.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ Architecture
5656
processor. A view itself does not journal new messages, instead, it updates internal state only from a processor's
5757
replicated message stream.
5858

59+
* *Producer*: A producer can be thought of a view that implements the `Reactive Streams`_ ``Producer`` interface. In
60+
contrast to a view, a producer only reads from a processor's message stream if explicitly requested from downstream
61+
consumers.
62+
5963
* *Channel*: Channels are used by processors and views to communicate with other actors. They prevent that replayed
6064
messages are redundantly delivered to these actors and provide at-least-once message delivery semantics, also in
6165
case of sender and receiver JVM crashes.
@@ -73,6 +77,7 @@ Architecture
7377
development of event sourced applications (see section :ref:`event-sourcing-java`)
7478

7579
.. _Community plugins: http://akka.io/community/
80+
.. _Reactive Streams: http://www.reactive-streams.org/
7681

7782
.. _processors-java:
7883

@@ -238,6 +243,13 @@ name in its actor hierarchy and hence influences only part of the view id. To fu
238243
The ``viewId`` must differ from the referenced ``processorId``, unless :ref:`snapshots-java` of a view and its
239244
processor shall be shared (which is what applications usually do not want).
240245

246+
.. _producers-java:
247+
248+
Producers
249+
=========
250+
251+
Java API coming soon. See also Scala :ref:`producers` documentation.
252+
241253
.. _channels-java:
242254

243255
Channels

akka-docs/rst/scala/code/docs/persistence/PersistenceDocSpec.scala

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ trait PersistenceDocSpec {
2121
//#auto-update
2222
"""
2323

24-
val system: ActorSystem
24+
implicit val system: ActorSystem
2525

2626
import system._
2727

@@ -355,4 +355,41 @@ trait PersistenceDocSpec {
355355
view ! Update(await = true)
356356
//#view-update
357357
}
358+
359+
new AnyRef {
360+
//#producer-creation
361+
import org.reactivestreams.api.Producer
362+
363+
import akka.persistence.{ Persistent, PersistentFlow, ProducerSettings }
364+
import akka.stream.{ FlowMaterializer, MaterializerSettings }
365+
import akka.stream.scaladsl.Flow
366+
367+
val materializer = FlowMaterializer(MaterializerSettings())
368+
369+
val flow: Flow[Persistent] = PersistentFlow.fromProcessor("some-processor-id")
370+
val producer: Producer[Persistent] = flow.toProducer(materializer)
371+
//#producer-creation
372+
373+
//#producer-buffer-size
374+
PersistentFlow.fromProcessor("some-processor-id", ProducerSettings(maxBufferSize = 200))
375+
//#producer-buffer-size
376+
377+
//#producer-examples
378+
// 1 producer and 2 consumers:
379+
val producer1: Producer[Persistent] =
380+
PersistentFlow.fromProcessor("processor-1").toProducer(materializer)
381+
Flow(producer1).foreach(p => println(s"consumer-1: ${p.payload}")).consume(materializer)
382+
Flow(producer1).foreach(p => println(s"consumer-2: ${p.payload}")).consume(materializer)
383+
384+
// 2 producers (merged) and 1 consumer:
385+
val producer2: Producer[Persistent] =
386+
PersistentFlow.fromProcessor("processor-2").toProducer(materializer)
387+
val producer3: Producer[Persistent] =
388+
PersistentFlow.fromProcessor("processor-3").toProducer(materializer)
389+
Flow(producer2).merge(producer3).foreach { p =>
390+
println(s"consumer-3: ${p.payload}")
391+
}.consume(materializer)
392+
//#producer-examples
393+
}
394+
358395
}

akka-docs/rst/scala/persistence.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Architecture
4444
processor. A view itself does not journal new messages, instead, it updates internal state only from a processor's
4545
replicated message stream.
4646

47+
* *Producer*: A producer can be thought of a view that implements the `Reactive Streams`_ ``Producer`` interface. In
48+
contrast to a view, a producer only reads from a processor's message stream if explicitly requested from downstream
49+
consumers.
50+
4751
* *Channel*: Channels are used by processors and views to communicate with other actors. They prevent that replayed
4852
messages are redundantly delivered to these actors and provide at-least-once message delivery semantics, also in
4953
case of sender and receiver JVM crashes.
@@ -61,6 +65,7 @@ Architecture
6165
development of event sourced applications (see section :ref:`event-sourcing`)
6266

6367
.. _Community plugins: http://akka.io/community/
68+
.. _Reactive Streams: http://www.reactive-streams.org/
6469

6570
.. _processors:
6671

@@ -228,6 +233,42 @@ name in its actor hierarchy and hence influences only part of the view id. To fu
228233
The ``viewId`` must differ from the referenced ``processorId``, unless :ref:`snapshots` of a view and its
229234
processor shall be shared (which is what applications usually do not want).
230235

236+
.. _producers:
237+
238+
Producers
239+
=========
240+
241+
A `Reactive Streams`_ ``Producer`` can be created from a processor's message stream via the ``PersistentFlow``
242+
extension of the Akka Streams Scala DSL:
243+
244+
.. includecode:: code/docs/persistence/PersistenceDocSpec.scala#producer-creation
245+
246+
The created ``flow`` object is of type ``Flow[Persistent]`` and can be composed with other flows using ``Flow``
247+
combinators (= methods defined on ``Flow``). Calling the ``toProducer`` method on ``flow`` creates a producer
248+
of type ``Producer[Persistent]``.
249+
250+
A persistent message producer only reads from a processor's journal when explicitly requested by downstream
251+
consumers. In order to avoid frequent, fine grained read access to a processor's journal, the producer tries
252+
to buffer persistent messages in memory from which it serves downstream requests. The maximum buffer size per
253+
producer is configurable with a ``ProducerSettings`` configuration object.
254+
255+
.. includecode:: code/docs/persistence/PersistenceDocSpec.scala#producer-buffer-size
256+
257+
Other ``ProducerSettings`` parameters are:
258+
259+
* ``fromSequenceNr``: specifies from which sequence number the persistent message stream shall start (defaults
260+
to ``1L``). Please note that specifying ``fromSequenceNr`` is much more efficient than using the ``drop(Int)``
261+
combinator, especially for larger sequence numbers.
262+
263+
* ``idle``: an optional parameter that specifies how long a producer shall wait after a journal read attempt didn't return
264+
any new persistent messages. If not defined, the producer uses the ``akka.persistence.view.auto-update-interval``
265+
configuration parameter, otherwise, it uses the defined ``idle`` parameter.
266+
267+
Here are two examples how persistent message producers can be connected to downstream consumers using the Akka
268+
Streams Scala DSL and its ``PersistentFlow`` extension.
269+
270+
.. includecode:: code/docs/persistence/PersistenceDocSpec.scala#producer-examples
271+
231272
.. _channels:
232273

233274
Channels

akka-persistence/src/main/scala/akka/persistence/View.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ trait View extends Actor with Recovery {
126126
/**
127127
* Switches to `idle` state and schedules the next update if `autoUpdate` returns `true`.
128128
*/
129-
private[persistence] def onReplayComplete(await: Boolean): Unit = {
129+
private def onReplayComplete(await: Boolean): Unit = {
130130
_currentState = idle
131131
if (autoUpdate) schedule = Some(context.system.scheduler.scheduleOnce(autoUpdateInterval, self, Update(await = false, autoUpdateReplayMax)))
132132
if (await) receiverStash.unstashAll()

akka-persistence/src/main/scala/akka/persistence/ViewProducer.scala

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
3+
*/
14
package akka.persistence
25

36
import scala.util.control.NonFatal
@@ -33,21 +36,30 @@ object PersistentFlow {
3336
/**
3437
* Configuration object for a [[Persistent]] stream producer.
3538
*
36-
* @param maxBufferSize maximum number of persistent messages to be buffered in memory (per producer).
39+
* @param fromSequenceNr Sequence number where the produced stream shall start (inclusive).
40+
* Default is `1L`.
41+
* @param maxBufferSize Maximum number of persistent messages to be buffered in memory (per producer).
42+
* Default is `100`.
43+
* @param idle Optional duration to wait if no more persistent messages can be pulled from the journal
44+
* before attempting the next pull. Default is `None` which causes the producer to take
45+
* the value defined by the `akka.persistence.view.auto-update-interval` configuration
46+
* key. If defined, the `idle` value is taken directly.
3747
*/
38-
case class ProducerSettings(maxBufferSize: Int = 20)
48+
case class ProducerSettings(fromSequenceNr: Long = 1L, maxBufferSize: Int = 100, idle: Option[FiniteDuration] = None) {
49+
require(fromSequenceNr > 0L, "fromSequenceNr must be > 0")
50+
}
3951

40-
private[akka] object ViewProducer {
52+
private object ViewProducer {
4153
def props(processorId: String, producerSettings: ProducerSettings, settings: MaterializerSettings): Props =
4254
Props(classOf[ViewProducerImpl], processorId, producerSettings, settings)
4355
}
4456

45-
private[akka] case class ViewProducerNode(processorId: String, producerSettings: ProducerSettings) extends ProducerNode[Persistent] {
57+
private case class ViewProducerNode(processorId: String, producerSettings: ProducerSettings) extends ProducerNode[Persistent] {
4658
def createProducer(settings: MaterializerSettings, context: ActorRefFactory): Producer[Persistent] =
4759
new ActorProducer(context.actorOf(ViewProducer.props(processorId, producerSettings, settings)))
4860
}
4961

50-
private[akka] class ViewProducerImpl(processorId: String, producerSettings: ProducerSettings, materializerSettings: MaterializerSettings)
62+
private class ViewProducerImpl(processorId: String, producerSettings: ProducerSettings, materializerSettings: MaterializerSettings)
5163
extends Actor
5264
with ActorLogging
5365
with SubscriberManagement[Persistent]
@@ -58,13 +70,11 @@ private[akka] class ViewProducerImpl(processorId: String, producerSettings: Prod
5870

5971
type S = ActorSubscription[Persistent]
6072

61-
private val view = context.actorOf(Props(classOf[ViewBuffer], processorId, producerSettings.maxBufferSize, self))
73+
private val view = context.actorOf(Props(classOf[ViewBuffer], processorId, producerSettings, self), "view-buffer")
6274

6375
private var pub: ActorPublisher[Persistent] = _
6476
private var shutdownReason: Option[Throwable] = ActorPublisher.NormalShutdownReason
6577

66-
context.setReceiveTimeout(materializerSettings.downstreamSubscriptionTimeout)
67-
6878
final def receive = {
6979
case ExposedPublisher(pub)
7080
this.pub = pub.asInstanceOf[ActorPublisher[Persistent]]
@@ -74,7 +84,6 @@ private[akka] class ViewProducerImpl(processorId: String, producerSettings: Prod
7484
final def waitingForSubscribers: Receive = {
7585
case SubscribePending
7686
pub.takePendingSubscribers() foreach registerSubscriber
77-
context.setReceiveTimeout(Duration.Undefined)
7887
context.become(active)
7988
}
8089

@@ -129,22 +138,22 @@ private object ViewBuffer {
129138
}
130139

131140
/**
132-
* A view that buffers up to `maxBufferSize` persistent messages in memory. Downstream demands
133-
* (requests) are served if the buffer is non-empty either while filling the buffer or after
134-
* having filled the buffer. When the buffer becomes empty new persistent messages are loaded
135-
* from the journal (in batches up to `maxBufferSize`).
141+
* A view that buffers up to `producerSettings.maxBufferSize` persistent messages in memory.
142+
* Downstream demands (requests) are served if the buffer is non-empty either while filling
143+
* the buffer or after having filled the buffer. When the buffer becomes empty new persistent
144+
* messages are loaded from the journal (in batches up to `producerSettings.maxBufferSize`).
136145
*/
137-
private[akka] class ViewBuffer(val processorId: String, maxBufferSize: Int, producer: ActorRef) extends View {
146+
private class ViewBuffer(val processorId: String, producerSettings: ProducerSettings, producer: ActorRef) extends View {
138147
import ViewBuffer._
139148
import context.dispatcher
140149

141150
private var replayed = 0
142151
private var requested = 0
143152
private var buffer: Vector[Persistent] = Vector.empty
144153

145-
val filling: Receive = {
154+
private val filling: Receive = {
146155
case p: Persistent
147-
buffer = buffer :+ p
156+
buffer :+= p
148157
replayed += 1
149158
if (requested > 0) respond(requested)
150159
case Filled
@@ -158,14 +167,14 @@ private[akka] class ViewBuffer(val processorId: String, maxBufferSize: Int, prod
158167
if (buffer.nonEmpty) respond(requested)
159168
}
160169

161-
val pausing: Receive = {
170+
private val pausing: Receive = {
162171
case Request(num)
163172
requested += num
164173
respond(requested)
165174
if (buffer.isEmpty) fill()
166175
}
167176

168-
val scheduled: Receive = {
177+
private val scheduled: Receive = {
169178
case Fill
170179
fill()
171180
case Request(num)
@@ -174,33 +183,47 @@ private[akka] class ViewBuffer(val processorId: String, maxBufferSize: Int, prod
174183

175184
def receive = filling
176185

177-
def fill(): Unit = {
186+
override def onReplaySuccess(receive: Receive, await: Boolean): Unit = {
187+
super.onReplaySuccess(receive, await)
188+
self ! Filled
189+
}
190+
191+
override def onReplayFailure(receive: Receive, await: Boolean, cause: Throwable): Unit = {
192+
super.onReplayFailure(receive, await, cause)
193+
self ! Filled
194+
}
195+
196+
override def lastSequenceNr: Long =
197+
math.max(producerSettings.fromSequenceNr - 1L, super.lastSequenceNr)
198+
199+
override def autoUpdateInterval: FiniteDuration =
200+
producerSettings.idle.getOrElse(super.autoUpdateInterval)
201+
202+
override def autoUpdateReplayMax: Long =
203+
producerSettings.maxBufferSize
204+
205+
override def autoUpdate: Boolean =
206+
false
207+
208+
private def fill(): Unit = {
178209
replayed = 0
179210
context.become(filling)
180-
self ! Update(false, maxBufferSize)
211+
self ! Update(false, autoUpdateReplayMax)
181212
}
182213

183-
def pause(): Unit = {
214+
private def pause(): Unit = {
184215
context.become(pausing)
185216
}
186217

187-
def schedule(): Unit = {
218+
private def schedule(): Unit = {
188219
context.become(scheduled)
189220
context.system.scheduler.scheduleOnce(autoUpdateInterval, self, Fill)
190221
}
191222

192-
def respond(num: Int): Unit = {
223+
private def respond(num: Int): Unit = {
193224
val (res, buf) = buffer.splitAt(num)
194225
producer ! Response(res)
195226
buffer = buf
196227
requested -= res.size
197228
}
198-
199-
private[persistence] override def onReplayComplete(await: Boolean): Unit = {
200-
super.onReplayComplete(await)
201-
self ! Filled
202-
}
203-
204-
override def autoUpdateReplayMax: Long = maxBufferSize
205-
override def autoUpdate: Boolean = false
206229
}

0 commit comments

Comments
 (0)