pjfanning commented on code in PR #515:
URL:
https://github.com/apache/pekko-persistence-r2dbc/pull/515#discussion_r4377812556
##########
core/src/main/scala/org/apache/pekko/persistence/r2dbc/internal/BySliceQuery.scala:
##########
@@ -257,6 +259,7 @@ import org.slf4j.Logger
toTimestamp = Some(toTimestamp),
behindCurrentTime = Duration.Zero,
backtracking = false)
+ .filter(row => filterEventsBeforeSnapshots(row.persistenceId,
row.seqNr, row.source))
Review Comment:
Fixed in fbbc33d: the filter now runs after `ContinuousQuery`, on the
envelopes, so dropped rows still move `latest` and `rowCount` forward.
`EventsBySliceStartingFromSnapshotSmallBufferSpec` is your repro with
`buffer-size = 10`, for both the current and the live query. Both tests fail
without the fix. One cost: the dropped events are now deserialized before they
are dropped. I put that in #528.
##########
core/src/main/scala/org/apache/pekko/persistence/r2dbc/internal/BySliceQuery.scala:
##########
@@ -463,6 +467,7 @@ import org.slf4j.Logger
toTimestamp,
behindCurrentTime,
backtracking = newState.backtracking)
+ .filter(row => filterEventsBeforeSnapshots(row.persistenceId,
row.seqNr, row.source))
Review Comment:
Fixed by the same change, covered by the live test in
`EventsBySliceStartingFromSnapshotSmallBufferSpec`. Without the fix, the live
query read the same page again and emitted the snapshot's event after the
snapshot.
##########
core/src/main/scala/org/apache/pekko/persistence/r2dbc/query/scaladsl/R2dbcReadJournal.scala:
##########
@@ -169,37 +216,203 @@ final class R2dbcReadJournal(system:
ExtendedActorSystem, config: Config, cfgPat
offset: Offset): Source[EventEnvelope[Event], NotUsed] = {
val dbSource = bySlice[Event].liveBySlices("eventsBySlices", entityType,
minSlice, maxSlice, offset)
if (settings.journalPublishEvents) {
- val pubSub = PubSub(typedSystem)
- val pubSubSource =
- Source
- .actorRef[EventEnvelope[Event]](
- completionMatcher = PartialFunction.empty,
- failureMatcher = PartialFunction.empty,
- bufferSize = settings.bufferSize,
- // dropHead drops the oldest buffered event when full; any dropped
pub/sub events are
- // recovered from the database source which is merged below, and
deduplication handles
- // any events received via both paths.
- // OverflowStrategy.dropNew is long deprecated and removed in
Pekko 2.0.0.
- overflowStrategy = OverflowStrategy.dropHead)
- .mapMaterializedValue { ref =>
- pubSub.eventTopics[Event](entityType, minSlice, maxSlice).foreach
{ topic =>
- import pekko.actor.typed.scaladsl.adapter._
- topic ! Topic.Subscribe(ref.toTyped[EventEnvelope[Event]])
- }
- }
- .filter { env =>
- val slice = sliceForPersistenceId(env.persistenceId)
- minSlice <= slice && slice <= maxSlice
- }
- dbSource
- .mergePrioritized(pubSubSource, leftPriority = 1, rightPriority = 10)
- .via(skipPubSubTooFarAhead(settings.backtrackingEnabled,
- settings.backtrackingWindow.toJava))
- .via(deduplicate(settings.deduplicateCapacity))
+ val pubSubSource = eventsBySlicesPubSubSource[Event](entityType,
minSlice, maxSlice)
+ mergeDbAndPubSubSources(dbSource, pubSubSource)
} else
dbSource
}
+ /**
+ * Same as `currentEventsBySlices` but with the purpose to use snapshots as
starting points and thereby reducing
+ * number of events that have to be loaded. This can be useful if the
consumer start from zero without any previously
+ * processed offset or if it has been disconnected for a long while and its
offset is far behind.
+ *
+ * First it loads all snapshots with timestamps greater than or equal to the
offset timestamp. There is at most one
+ * snapshot per persistenceId. The snapshots are transformed to events with
the given `transformSnapshot` function.
+ *
+ * After emitting the snapshot events the ordinary events with sequence
numbers after the snapshots are emitted.
+ *
+ * To use `currentEventsBySlicesStartingFromSnapshots` you must enable
configuration
+ * `pekko.persistence.r2dbc.query.start-from-snapshot.enabled` and follow
the instructions in the migration guide.
+ */
+ override def currentEventsBySlicesStartingFromSnapshots[Snapshot, Event](
+ entityType: String,
+ minSlice: Int,
+ maxSlice: Int,
+ offset: Offset,
+ transformSnapshot: Snapshot => Event): Source[EventEnvelope[Event],
NotUsed] = {
+ checkStartFromSnapshotEnabled("currentEventsBySlicesStartingFromSnapshots")
+ val timestampOffset = toTimestampOffset(offset)
+
+ val snapshotSource =
+ snapshotsBySlice[Snapshot, Event](transformSnapshot)
+ .currentBySlices("currentSnapshotsBySlices", entityType, minSlice,
maxSlice, offset)
+
+ Source.fromGraph(
+ new StartingFromSnapshotStage[Event](
+ snapshotSource,
+ { snapshotOffsets =>
+ val initOffset = initOffsetAfterSnapshots(timestampOffset, offset,
snapshotOffsets)
+
+ log.debug(
+ "currentEventsBySlicesStartingFromSnapshots initOffset [{}] with
[{}] snapshots",
+ initOffset,
+ snapshotOffsets.size)
+
+ bySlice.currentBySlices(
+ "currentEventsBySlices",
+ entityType,
+ minSlice,
+ maxSlice,
+ initOffset,
+ filterEventsBeforeSnapshots(snapshotOffsets, backtrackingEnabled =
false))
+ }))
+ }
+
+ /**
+ * Same as `eventsBySlices` but with the purpose to use snapshots as
starting points and thereby reducing number of
+ * events that have to be loaded. This can be useful if the consumer start
from zero without any previously processed
+ * offset or if it has been disconnected for a long while and its offset is
far behind.
+ *
+ * First it loads all snapshots with timestamps greater than or equal to the
offset timestamp. There is at most one
+ * snapshot per persistenceId. The snapshots are transformed to events with
the given `transformSnapshot` function.
+ *
+ * After emitting the snapshot events the ordinary events with sequence
numbers after the snapshots are emitted.
+ *
+ * To use `eventsBySlicesStartingFromSnapshots` you must enable configuration
+ * `pekko.persistence.r2dbc.query.start-from-snapshot.enabled` and follow
the instructions in the migration guide.
+ */
+ override def eventsBySlicesStartingFromSnapshots[Snapshot, Event](
+ entityType: String,
+ minSlice: Int,
+ maxSlice: Int,
+ offset: Offset,
+ transformSnapshot: Snapshot => Event): Source[EventEnvelope[Event],
NotUsed] = {
+ checkStartFromSnapshotEnabled("eventsBySlicesStartingFromSnapshots")
+ val timestampOffset = toTimestampOffset(offset)
+
+ val snapshotSource =
+ snapshotsBySlice[Snapshot, Event](transformSnapshot)
+ .currentBySlices("snapshotsBySlices", entityType, minSlice, maxSlice,
offset)
+
+ Source.fromGraph(
+ new StartingFromSnapshotStage[Event](
+ snapshotSource,
+ { snapshotOffsets =>
+ val initOffset = initOffsetAfterSnapshots(timestampOffset, offset,
snapshotOffsets)
+
+ log.debug(
+ "eventsBySlicesStartingFromSnapshots initOffset [{}] with [{}]
snapshots",
+ initOffset,
+ snapshotOffsets.size)
+
+ val dbSource =
+ bySlice[Event].liveBySlices(
+ "eventsBySlices",
+ entityType,
+ minSlice,
+ maxSlice,
+ initOffset,
+ filterEventsBeforeSnapshots(snapshotOffsets,
settings.backtrackingEnabled))
+
+ if (settings.journalPublishEvents) {
+ // Note that events via PubSub are not filtered by
snapshotOffsets. It's unlikely that
+ // those will be earlier than the snapshots and duplicates must be
handled downstream in that case.
+ // If we would use the filterEventsBeforeSnapshots function for
PubSub it would be difficult to
+ // know when memory of that Map can be released or the filter
function would have to be shared
+ // and thread safe, which is not worth it.
+ val pubSubSource = eventsBySlicesPubSubSource[Event](entityType,
minSlice, maxSlice)
+ mergeDbAndPubSubSources(dbSource, pubSubSource)
+ } else
+ dbSource
+ }))
+ }
+
+ private def initOffsetAfterSnapshots(
+ timestampOffset: TimestampOffset,
+ offset: Offset,
+ snapshotOffsets: Map[String, (Long, Instant)]): Offset =
+ if (timestampOffset == TimestampOffset.Zero && snapshotOffsets.nonEmpty) {
Review Comment:
Fixed in fbbc33d: the event query now always starts at the given offset.
That also covers a snapshot that moved past `toTimestamp` while the snapshot
query was running. New test: `include older events of entities without a
snapshot for NoOffset` (current and live). The docs now say that all events are
still read from the offset. Akka found the same problem and fixed it in 1.3.12
(akka/akka-persistence-r2dbc#717), but that code is under the BSL until
2029-02-06, so this fix is our own. #717 also fixes a related problem:
snapshots are emitted before events that have earlier timestamps, so the
offsets go backwards. A projection that restarts from a saved offset can miss
events. I documented this for now and opened #528 for a clean-room redesign.
##########
core/src/main/resources/reference.conf:
##########
@@ -131,6 +131,16 @@ pekko.persistence.r2dbc {
use-connection-factory = ${pekko.persistence.r2dbc.use-connection-factory}
log-db-calls-exceeding = ${pekko.persistence.r2dbc.log-db-calls-exceeding}
+
+ # When enabled, the timestamp and tags of the corresponding event are
stored with the snapshot.
+ # Required by the queries that use snapshots as starting points, see
query.start-from-snapshot.
+ start-from-snapshot.enabled =
${pekko.persistence.r2dbc.query.start-from-snapshot.enabled}
+
+ # the journal table that the timestamp and tags of the corresponding event
are read from
+ journal-table = ${pekko.persistence.r2dbc.journal.table}
Review Comment:
Done: `snapshots.md` now lists the snapshot plugin settings that default to
the default plugin paths and the journal `schema`. When start-from-snapshot is
enabled and the event isn't found, `eventTimestampAndTags` logs a warning that
names the journal table.
##########
ddl-scripts/migration/snapshot_start_from_snapshot_postgres.sql:
##########
@@ -0,0 +1,13 @@
+-- Adds the snapshot columns and index that are needed by the queries that use
snapshots as starting points
+-- (eventsBySlicesStartingFromSnapshots,
eventsByPersistenceIdStartingFromSnapshot and their current variants).
+-- Use this for both the BYTEA and the JSONB schema. See the migration guide
for a rolling update procedure.
+
+ALTER TABLE snapshot ADD COLUMN IF NOT EXISTS db_timestamp timestamp with time
zone;
+ALTER TABLE snapshot ADD COLUMN IF NOT EXISTS tags TEXT ARRAY;
+
+-- populate the new columns from the corresponding events
+UPDATE snapshot s SET db_timestamp = e.db_timestamp, tags = e.tags
+ FROM event_journal e
+ WHERE s.persistence_id = e.persistence_id AND s.seq_nr = e.seq_nr;
Review Comment:
Done: the Postgres, Yugabyte and MySQL updates now filter `deleted = false`.
I checked the Postgres script against a deleted event. The migration guide now
suggests running the update in batches by `slice` range for large tables.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]