-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add flatten
to Source[T]
#198
Merged
Merged
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
2a3831d
feat: add `flatten` extension method for Source[Source[T]]
nimatrueway 803d577
test: add case to cover receiver being closed
nimatrueway ef3174c
refactor: clean tests a bit
nimatrueway 52bf1ae
feat: use a better approach to implement it
nimatrueway 3015ba3
fix: correct type name and add a warning
nimatrueway fe03261
fix: gracefully handle nested sources
nimatrueway 2372934
refactor: fix contention tests using a rendezvous stage channel
nimatrueway File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
165 changes: 165 additions & 0 deletions
165
core/src/test/scala/ox/channels/SourceOpsFlattenTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package ox.channels | ||
|
||
import org.scalatest.OptionValues | ||
import org.scalatest.flatspec.AnyFlatSpec | ||
import org.scalatest.matchers.should.Matchers | ||
import ox.* | ||
|
||
import java.util.concurrent.CountDownLatch | ||
import scala.collection.mutable.ListBuffer | ||
|
||
class SourceOpsFlattenTest extends AnyFlatSpec with Matchers with OptionValues { | ||
|
||
"flatten" should "pipe all elements of the child sources into the output source" in { | ||
supervised { | ||
val source = Source.fromValues( | ||
Source.fromValues(10), | ||
Source.fromValues(20, 30), | ||
Source.fromValues(40, 50, 60) | ||
) | ||
source.flatten.toList should contain theSameElementsAs List(10, 20, 30, 40, 50, 60) | ||
} | ||
} | ||
|
||
it should "handle empty source" in { | ||
supervised { | ||
val source = Source.empty[Source[Int]] | ||
source.flatten.toList should contain theSameElementsAs Nil | ||
} | ||
} | ||
|
||
it should "handle singleton source" in { | ||
supervised { | ||
val source = Source.fromValues(Source.fromValues(10)) | ||
source.flatten.toList should contain theSameElementsAs List(10) | ||
} | ||
} | ||
|
||
it should "pipe elements realtime" in { | ||
supervised { | ||
val source = Channel.bufferedDefault[Source[Int]] | ||
val lockA = CountDownLatch(1) | ||
val lockB = CountDownLatch(1) | ||
source.send(Source.fromValues(10)) | ||
source.send { | ||
val subSource = Channel.bufferedDefault[Int] | ||
subSource.send(20) | ||
forkUnsupervised { | ||
lockA.await() // 30 won't be added until, lockA is released after 20 consumption | ||
subSource.send(30) | ||
subSource.done() | ||
} | ||
subSource | ||
} | ||
forkUnsupervised { | ||
lockB.await() // 40 won't be added until, lockB is released after 30 consumption | ||
source.send(Source.fromValues(40)) | ||
source.done() | ||
} | ||
|
||
val collected = ListBuffer[Int]() | ||
source.flatten.foreachOrError { e => | ||
collected += e | ||
if e == 20 then lockA.countDown() | ||
else if e == 30 then lockB.countDown() | ||
} | ||
collected should contain theSameElementsAs List(10, 20, 30, 40) | ||
} | ||
} | ||
|
||
it should "propagate error of any of the child sources and stop piping" in { | ||
supervised { | ||
val child1 = Channel.rendezvous[Int] | ||
val lock = CountDownLatch(1) | ||
fork { | ||
child1.send(10) | ||
// wait for child2 to emit an error | ||
lock.await() | ||
// `flatten` will not receive this, as it will be short-circuited by the error | ||
child1.sendOrClosed(30) | ||
} | ||
val child2 = Channel.rendezvous[Int] | ||
fork { | ||
child2.send(20) | ||
child2.error(new Exception("intentional failure")) | ||
lock.countDown() | ||
} | ||
val source = Source.fromValues(child1, child2) | ||
|
||
val (collectedElems, collectedError) = source.flatten.toPartialList() | ||
collectedError.value.getMessage shouldBe "intentional failure" | ||
collectedElems should contain theSameElementsAs List(10, 20) | ||
child1.receive() shouldBe 30 | ||
} | ||
} | ||
|
||
it should "propagate error of the parent source and stop piping" in { | ||
supervised { | ||
val child1 = Channel.rendezvous[Int] | ||
val lock = CountDownLatch(1) | ||
fork { | ||
child1.send(10) | ||
lock.countDown() | ||
// depending on how quick it picks up the error from the parent | ||
// `flatten` may or may not receive this | ||
child1.send(20) | ||
child1.done() | ||
} | ||
val source = Channel.rendezvous[Source[Int]] | ||
fork { | ||
source.send(child1) | ||
// make sure the first element of child1 is consumed before emitting error | ||
lock.await() | ||
source.error(new Exception("intentional failure")) | ||
} | ||
|
||
val (collectedElems, collectedError) = source.flatten.toPartialList() | ||
collectedError.value.getMessage shouldBe "intentional failure" | ||
collectedElems should contain atLeastOneElementOf List(10, 20) | ||
} | ||
} | ||
|
||
it should "stop pulling from the sources when the receiver is closed" in { | ||
val child1 = Channel.rendezvous[Int] | ||
|
||
Thread.startVirtualThread(() => { | ||
child1.send(10) | ||
// at this point `flatten` channel is closed | ||
// so although `flatten` thread receives "20" element | ||
// it can not push it to its output channel and it will be lost | ||
child1.send(20) | ||
child1.send(30) | ||
child1.done() | ||
}) | ||
|
||
supervised { | ||
val source = Source.fromValues(child1) | ||
val flattenSource = { | ||
implicit val capacity: StageCapacity = StageCapacity(0) | ||
source.flatten | ||
} | ||
flattenSource.receive() shouldBe 10 | ||
} | ||
|
||
child1.receiveOrClosed() shouldBe 30 | ||
child1.receiveOrClosed() shouldBe ChannelClosed.Done | ||
} | ||
|
||
extension [T](source: Source[T]) { | ||
def toPartialList(cb: T | Throwable => Unit = (_: Any) => ()): (List[T], Option[Throwable]) = { | ||
val elementCapture = ListBuffer[T]() | ||
var errorCapture = Option.empty[Throwable] | ||
try { | ||
for (t <- source) { | ||
cb(t) | ||
elementCapture += t | ||
} | ||
} catch { | ||
case ChannelClosedException.Error(e) => | ||
cb(e) | ||
errorCapture = Some(e) | ||
} | ||
(elementCapture.toList, errorCapture) | ||
} | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that we can not know the result of select stems from which select statement is limiting. In Golang there's a hacky way to do it.
https://stackoverflow.com/a/19992525/1556045
I think both
(v: V)
or(c: ChannelClosed)
results if they hint that which select/source they stem from would make the API more robust.Update: created a ticket for it as Adam suggested #201