Skip to content

Commit ec126ac

Browse files
committed
Fix memory leak using non-termination persistence query.
1 parent fea6000 commit ec126ac

11 files changed

+116
-96
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/project/target
44
.DS_Store
55
/project/project/
6+
/.bsp/

build.sbt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
lazy val scala213 = "2.13.4"
2+
lazy val scala300 = "3.0.0-M2"
23
lazy val supportedScalaVersions = List(scala213)
34
lazy val akkaVersion = "2.6.10"
45
lazy val rxmongoVersion = "1.0.1"
56

67
lazy val commonSettings = Seq(
78
name := "akka-reactivemongo-plugin",
89
organization := "null-vector",
9-
version := "1.4.3",
10+
version := s"1.4.4",
1011
scalaVersion := scala213,
1112
crossScalaVersions := supportedScalaVersions,
1213
scalacOptions := Seq(

core/src/main/scala/org/nullvector/CollectionNameMapping.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@ trait CollectionNameMapping {
1010

1111
class DefaultCollectionNameMapping(config: Config) extends CollectionNameMapping {
1212
private val separator: String = config.getString("akka-persistence-reactivemongo.persistence-id-separator")
13-
private val pattern: Regex = buildPattern(separator.head)
13+
private val pattern: Regex = buildPattern(separator.headOption)
1414

1515
override def collectionNameOf(persistentId: String): Option[String] = persistentId match {
16+
case pattern(name, _) if name.isEmpty => None
1617
case pattern(name, _) => Some(name)
1718
case _ => None
1819
}
1920

20-
private def buildPattern(separator: Char) = s"(\\w+)[$separator](.+)".r
21+
private def buildPattern(maybeSeparator: Option[Char]) = maybeSeparator match {
22+
case Some(char) => s"(\\w+)[$char](.+)".r
23+
case None => s"()(.+)".r
24+
}
25+
2126
}
2227

core/src/main/scala/org/nullvector/query/EventsQueries.scala

+5-6
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ trait EventsQueries
4141
(_, fromToSequences) => fromToSequences,
4242
offset => currentEventsByPersistenceId(persistenceId, offset._1, offset._2)
4343
))
44-
.flatMapConcat(identity)
44+
.mapConcat(identity)
4545
}
4646

4747
override def currentEventsByPersistenceId(persistenceId: String, fromSequenceNr: Long, toSequenceNr: Long): Source[EventEnvelope, NotUsed] = {
@@ -54,8 +54,8 @@ trait EventsQueries
5454
override def eventsByTag(tag: String, offset: Offset): Source[EventEnvelope, NotUsed] =
5555
Source
5656
.fromGraph(new PullerGraph[EventEnvelope, Offset](
57-
offset, defaultRefreshInterval, _.offset, greaterOffsetOf, o => currentEventsByTag(tag, o)))
58-
.flatMapConcat(identity)
57+
offset, defaultRefreshInterval, _.offset, greaterOffsetOf, offset => currentEventsByTag(tag, offset)))
58+
.mapConcat(identity)
5959

6060
/*
6161
* Query events that have a specific tag. Those events matching target tags would
@@ -83,11 +83,11 @@ trait EventsQueries
8383
}
8484

8585
private def eventsByTagQuery(tags: Seq[String], offset: Offset)(implicit serializableMethod: (BSONDocument, BSONDocument) => Future[Any]): Source[EventEnvelope, NotUsed] = {
86-
Source.lazyFuture(() => rxDriver.journals())
86+
Source.future(rxDriver.journals())
8787
.mapConcat(identity)
8888
.splitWhen(_ => true)
8989
.flatMapConcat(buildFindEventsByTagsQuery(_, offset, tags))
90-
.mergeSubstreams
90+
.mergeSubstreamsWithParallelism(amountOfCores)
9191
.via(document2Envelope(serializableMethod))
9292
}
9393

@@ -110,7 +110,6 @@ trait EventsQueries
110110

111111
private def buildFindEventsByTagsQuery(coll: collection.BSONCollection, offset: Offset, tags: Seq[String]) = {
112112
def query(field: String) = BSONDocument(field -> BSONDocument("$in" -> tags))
113-
114113
coll
115114
.aggregateWith[BSONDocument]()(framework =>
116115
List(

core/src/main/scala/org/nullvector/query/FromMemoryReadJournal.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.nullvector.query
33
import akka.NotUsed
44
import akka.actor.typed.ActorSystem
55
import akka.persistence.query.{EventEnvelope, NoOffset, Offset}
6+
import akka.stream.Materializer
67
import akka.stream.scaladsl.Source
78
import org.nullvector.PersistInMemory.EventWithOffset
89
import org.nullvector.{PersistInMemory, ReactiveMongoEventSerializer}
@@ -12,6 +13,7 @@ import scala.concurrent.{ExecutionContextExecutor, Future}
1213

1314
class FromMemoryReadJournal(actorSystem: ActorSystem[_]) extends ReactiveMongoScalaReadJournal {
1415
private implicit val ec: ExecutionContextExecutor = actorSystem.executionContext
16+
private implicit val mat: Materializer = Materializer.matFromSystem(actorSystem)
1517
private val memory: PersistInMemory = PersistInMemory(actorSystem)
1618
private val serializer: ReactiveMongoEventSerializer = ReactiveMongoEventSerializer(actorSystem)
1719
val defaultRefreshInterval: FiniteDuration = 500.millis
@@ -28,7 +30,7 @@ class FromMemoryReadJournal(actorSystem: ActorSystem[_]) extends ReactiveMongoSc
2830
.fromGraph(
2931
new PullerGraph[EventEnvelope, Offset](offset, defaultRefreshInterval, _.offset, greaterOffsetOf, offset => currentEventsByTag(tag, offset))
3032
)
31-
.flatMapConcat(identity)
33+
.mapConcat(identity)
3234

3335
override def currentEventsByTag(tag: String, offset: Offset): Source[EventEnvelope, NotUsed] =
3436
currentEventsByTags(Seq(tag), offset)

core/src/main/scala/org/nullvector/query/ObjectIdOffset.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ case class ObjectIdOffset(id: BSONObjectID) extends Offset with Ordered[ObjectId
1717

1818
override val toString: String = s"Offset(${id.stringify})"
1919

20-
override def compare(that: ObjectIdOffset): Int = BigInt(id.byteArray).compare(BigInt(that.id.byteArray))
20+
override def compare(that: ObjectIdOffset): Int = id.stringify.compare(that.id.stringify)
2121
}
2222

2323

core/src/main/scala/org/nullvector/query/PersistenceIdsQueries.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ trait PersistenceIdsQueries
3232
greaterOffsetOf,
3333
o => currentPersistenceIds(o)
3434
))
35-
.flatMapConcat(identity)
35+
.mapConcat(identity)
3636
.map(_.persistenceId)
3737
}
3838

core/src/main/scala/org/nullvector/query/PullerGraph.scala

+49-33
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,70 @@ package org.nullvector.query
33
import akka.NotUsed
44
import akka.stream.scaladsl.Source
55
import akka.stream.stage._
6-
import akka.stream.{Attributes, Outlet, SourceShape}
6+
import akka.stream.{Attributes, Materializer, Outlet, SourceShape}
7+
import org.slf4j.{Logger, LoggerFactory}
78

8-
import scala.concurrent.ExecutionContext
9+
import scala.collection.mutable
910
import scala.concurrent.duration.FiniteDuration
11+
import scala.concurrent.{ExecutionContext, Future}
1012

11-
class PullerGraph[D, O](
12-
initialOffset: O,
13-
refreshInterval: FiniteDuration,
14-
offsetOf: D => O,
15-
graterOf: (O, O) => O,
16-
nextChunk: O => Source[D, NotUsed],
17-
)(implicit ec: ExecutionContext) extends GraphStage[SourceShape[Source[D, NotUsed]]] {
13+
class PullerGraph[Element, Offset](
14+
initialOffset: Offset,
15+
refreshInterval: FiniteDuration,
16+
offsetOf: Element => Offset,
17+
greaterOf: (Offset, Offset) => Offset,
18+
query: Offset => Source[Element, NotUsed],
19+
)(implicit ec: ExecutionContext, mat: Materializer) extends GraphStage[SourceShape[Seq[Element]]] {
1820

19-
private val outlet: Outlet[Source[D, NotUsed]] = Outlet[Source[D, NotUsed]]("PullerGraph.OUT")
21+
private val outlet: Outlet[Seq[Element]] = Outlet[Seq[Element]]("PullerGraph.OUT")
2022

21-
override def shape: SourceShape[Source[D, NotUsed]] = SourceShape.of(outlet)
23+
override def shape: SourceShape[Seq[Element]] = SourceShape.of(outlet)
2224

2325
override def createLogic(attributes: Attributes): GraphStageLogic = new TimerGraphStageLogic(shape) {
24-
26+
var currentOffset: Offset = initialOffset
2527
private val effectiveRefreshInterval: FiniteDuration = attributes.get[RefreshInterval].fold(refreshInterval)(_.interval)
26-
var currentOffset: O = initialOffset
27-
var eventStreamConsuming = false
28-
29-
private val updateConsumingState: AsyncCallback[Boolean] = createAsyncCallback[Boolean](eventStreamConsuming = _)
30-
private val updateCurrentOffset: AsyncCallback[D] =
31-
createAsyncCallback[D](event => currentOffset = graterOf(currentOffset, offsetOf(event)))
28+
private val updateCurrentOffset = createAsyncCallback[Offset](offset => currentOffset = offset)
29+
private val failAsync = createAsyncCallback[Throwable](throwable => failStage(throwable))
30+
private val pushElements = createAsyncCallback[Seq[Element]](elements => push(outlet, elements))
3231

32+
private val timerName = "timer"
3333
setHandler(outlet, new OutHandler {
34-
override def onPull(): Unit = {}
34+
override def onPull() = scheduleNext()
3535

36-
override def onDownstreamFinish(cause: Throwable): Unit = cancelTimer("timer")
36+
override def onDownstreamFinish(cause: Throwable) = cancelTimer(timerName)
3737
})
3838

39-
override def preStart(): Unit = scheduleWithFixedDelay("timer", effectiveRefreshInterval, effectiveRefreshInterval)
40-
41-
override protected def onTimer(timerKey: Any): Unit = {
42-
if (isAvailable(outlet) && !eventStreamConsuming) {
43-
eventStreamConsuming = true
44-
val source = nextChunk(currentOffset)
45-
.mapAsync(1)(entry => updateCurrentOffset.invokeWithFeedback(entry).map(_ => entry))
46-
.watchTermination() { (mat, future) =>
47-
future.onComplete { _ => updateConsumingState.invoke(false) }
48-
mat
49-
}
50-
push(outlet, source)
39+
override protected def onTimer(timerKey: Any) = {
40+
query(currentOffset)
41+
.runFold(new Accumulator(currentOffset))((acc, element) => acc.update(element))
42+
.flatMap(_.pushOrScheduleNext())
43+
.recover { case throwable: Throwable => failAsync.invoke(throwable) }
44+
}
45+
46+
private def scheduleNext() = {
47+
if (!isTimerActive(timerName)) {
48+
scheduleOnce(timerName, effectiveRefreshInterval)
5149
}
5250
}
5351

54-
}
52+
class Accumulator(private var latestOffset: Offset, private val elements: mutable.Buffer[Element] = mutable.Buffer.empty) {
5553

54+
def update(anElement: Element): Accumulator = {
55+
latestOffset = greaterOf(latestOffset, offsetOf(anElement))
56+
elements.append(anElement)
57+
this
58+
}
59+
60+
def pushOrScheduleNext(): Future[Unit] = {
61+
if (elements.nonEmpty) {
62+
for {
63+
_ <- updateCurrentOffset.invokeWithFeedback(latestOffset)
64+
_ <- pushElements.invokeWithFeedback(elements.toSeq)
65+
} yield ()
66+
}
67+
else Future.successful(scheduleNext())
68+
}
69+
}
70+
71+
}
5672
}

0 commit comments

Comments
 (0)