Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions fs2-aws-dynamodb/src/main/scala/fs2/aws/dynamodb/StreamScan.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ trait StreamScan[F[_]] {
/** Scans Dynamodb into the FS2 stream
*
* @param scanRequest AWS SDK2 DynamoDB scan request
* @param pageSize the page size of the scan, defines how many
* records will be loaded to the memory
* @param pageSize the maximum number of records requested per scan page; must be positive
* @return an fs2 Stream that emits RAW DynamoDB items, terminates, once scan exhausted
*/
def scanDynamoDB(
Expand All @@ -33,24 +32,24 @@ object StreamScan {
scanRequest: ScanRequest,
pageSize: Int
): Stream[F, Chunk[JMap[String, AttributeValue]]] =
for {
if (pageSize <= 0) throw new IllegalArgumentException("pageSize must be greater than 0")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated invalid page-size validation to Stream.raiseError and added two tests using an observable publisher subscription: invalid sizes are captured by attempt, and take(1) invokes cancel(). The project compiles and both StreamScanCancellationSuite tests pass with sbt 1.13.0 / Scala 3.3.8. The standard formatting task is blocked by the existing incompatible AsciiSortImports / Imports(sort=original) configuration; I have kept that unrelated config unchanged.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@dajiaohuang another good catch. Thanks!

else for {
dispatcher <- Stream.resource(Dispatcher.parallel[F])
queue <- Stream.eval(Queue.bounded[F, Option[Chunk[JMap[String, AttributeValue]]]](1))
sub <- Stream.eval(Ref[F].of[Option[Subscription]](None))
error <- Stream.eval(Deferred[F, Throwable])
_ <- Stream.eval(
ddb
.scanPaginator(scanRequest)
.scanPaginator(scanRequest.toBuilder().limit(pageSize).build())
.map { publisher =>
// subscribe to the paginator, every time we request to deliver next pageSize items from the DDB table
// we use FS2 Queue as bounded buffer with size 1, this way we implement back pressure, not allowing
// paginator exhaust memory
// Limit each DynamoDB response to pageSize items and request one response at a time.
// The bounded FS2 queue provides back pressure while a page is consumed.
publisher.subscribe(new Subscriber[ScanResponse] {

override def onSubscribe(s: Subscription): Unit =
dispatcher
.unsafeRunSync(
sub.set(s.some) >> Async[F].delay(s.request(pageSize.toLong))
sub.set(s.some) >> Async[F].delay(s.request(1))
)

override def onNext(t: ScanResponse): Unit =
Expand All @@ -59,7 +58,7 @@ object StreamScan {
for {
_ <- queue.offer(Chunk(t.items().asScala.toList*).some)
s <- sub.get
_ <- Async[F].delay(s.map(_.request(pageSize.toLong)))
_ <- Async[F].delay(s.map(_.request(1)))
} yield ()
)

Expand All @@ -71,10 +70,10 @@ object StreamScan {
})
}
)
stream <- Stream.fromQueueNoneTerminated(queue) ++ Stream.eval(error.tryGet).flatMap {
stream <- (Stream.fromQueueNoneTerminated(queue) ++ Stream.eval(error.tryGet).flatMap {
case Some(value) => Stream.raiseError[F](value)
case None => Stream.empty
}
}).onFinalize(sub.get.flatMap(_.traverse_(s => Async[F].delay(s.cancel()))))
Comment thread
barryoneill marked this conversation as resolved.
} yield stream

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@ class StreamScanSpec extends AnyWordSpec with Matchers with ScalaFutures {
)
.build()
)
scanned <- StreamScan[IO](ddb)
chunks <- StreamScan[IO](ddb)
.scanDynamoDB(ScanRequest.builder().tableName(tableName).build(), 3)
.unchunks
.compile
.toList
} yield scanned.map(_.get("name").s()) should contain theSameElementsAs List(
"Dmytro",
"Barry",
"Ryan",
"Vlad"
)
.toVector
} yield {
chunks.forall(_.size <= 3) shouldBe true
chunks.flatMap(_.toList).map(_.get("name").s()) should contain theSameElementsAs List(
"Dmytro",
"Barry",
"Ryan",
"Vlad"
)
}
}
.unsafeToFuture()
.futureValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class DDBReader[F[_]](ss: StreamScan[F]) {
.segment(segment)
.totalSegments(totalSegments)
.tableName(ddbName)
.limit(100)
.build(),
pageSize
)
Expand Down