Skip to content

Commit adec011

Browse files
committed
Publish(expectedCollectors) overload
1 parent 9e731ca commit adec011

File tree

6 files changed

+143
-132
lines changed

6 files changed

+143
-132
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Extensions to the Kotlin Flow library.
1111

1212
```groovy
1313
dependencies {
14-
implementation "com.github.akarnokd:kotlin-flow-extensions:0.0.5"
14+
implementation "com.github.akarnokd:kotlin-flow-extensions:0.0.6"
1515
}
1616
```
1717

@@ -30,7 +30,7 @@ Table of contents
3030
- `Flow.concatWith`
3131
- `Flow.groupBy`
3232
- `Flow.parallel`
33-
- `Flow.publish`
33+
- [`Flow.publish`](#flowpublish)
3434
- `Flow.replay`
3535
- `Flow.startCollectOn`
3636
- `Flow.takeUntil`
@@ -182,3 +182,30 @@ range(1, 10)
182182
)
183183
```
184184

185+
## Flow.publish
186+
187+
Shares a single connection to the upstream source which can be consumed by many collectors inside a `transform` function,
188+
which then yields the resulting items for the downstream.
189+
190+
Effectively, one collector to the output `Flow<R>` will trigger exactly one collection of the upstream `Flow<T>`. Inside
191+
the `transformer` function though, the presented `Flow<T>` can be collected as many times as needed; it won't trigger
192+
new collections towards the upstream but share items to all inner collectors as they become available.
193+
194+
Unfortunately, the suspending nature of coroutines/`Flow` doesn't give a clear indication when the `transformer` chain
195+
has been properly established, which can result in item loss or run-to-completion without any item being collected.
196+
If the number of the inner collectors inside `transformer` can be known, the `publish(expectedCollectors)` overload
197+
can be used to hold back the upstream until the expected number of collectors have started/ready collecting items.
198+
199+
#### Example:
200+
201+
```kotlin
202+
range(1, 5)
203+
.publish(2) {
204+
shared -> merge(shared.filter { it % 2 == 0 }, shared.filter { it % 2 != 0 })
205+
}
206+
.assertResult(1, 2, 3, 4, 5)
207+
```
208+
209+
In the example, it is known `merge` will establish 2 collectors, thus the `publish` can be instructed to await those 2.
210+
Without the argument, `range` would rush through its items as `merge` doesn't start collecting in time, causing an
211+
empty result list.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
GROUP=com.github.akarnokd
2-
VERSION_NAME=0.0.5
2+
VERSION_NAME=0.0.6
33

44
POM_ARTIFACT_ID=kotlin-flow-extensions
55
POM_NAME=Kotlin Flow Extensions

src/main/kotlin/hu/akarnokd/kotlin/flow/FlowExtensions.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,35 @@ import java.util.concurrent.TimeUnit
2828
* Shares a single collector towards the upstream source and multicasts
2929
* values to any number of consumers which then can produce the output
3030
* flow of values.
31+
*
32+
* Note that due to how coroutines/[Flow] are implemented, it is not guaranteed
33+
* the [transform] function connects the upstream with the downstream in time,
34+
* causing item loss or even run-to-completion without any single upstream item
35+
* being collected and transformed. To avoid such scenarios, use the
36+
* `publish(expectedCollectors)` overload.
3137
*/
3238
@FlowPreview
3339
fun <T, R> Flow<T>.publish(transform: suspend (Flow<T>) -> Flow<R>) : Flow<R> =
34-
FlowMulticastFunction(this, { /* MulticastSubject()*/ PublishSubject() }, transform)
40+
FlowMulticastFunction(this, { PublishSubject() }, transform)
41+
42+
/**
43+
* Shares a single collector towards the upstream source and multicasts
44+
* values to any number of consumers which then can produce the output
45+
* flow of values.
46+
*
47+
* Note that due to how coroutines/[Flow] are implemented, it is not guaranteed
48+
* the [transform] function connects the upstream with the downstream in time,
49+
* causing item loss or even run-to-completion without any single upstream item
50+
* being collected and transformed. To avoid such scenarios, specify the
51+
* [expectedCollectors] to delay the collection of the upstream until the number
52+
* of inner collectors has reached the specified number.
53+
*
54+
* @param expectedCollectors the number of collectors to wait for before resuming the source, allowing
55+
* the desired number of collectors to arrive and be ready for the upstream items
56+
*/
57+
@FlowPreview
58+
fun <T, R> Flow<T>.publish(expectedCollectors: Int, transform: suspend (Flow<T>) -> Flow<R>) : Flow<R> =
59+
FlowMulticastFunction(this, { /* MulticastSubject()*/ MulticastSubject(expectedCollectors) }, transform)
3560

3661
/**
3762
* Shares a single collector towards the upstream source and multicasts

src/main/kotlin/hu/akarnokd/kotlin/flow/MulticastSubject.kt

Lines changed: 70 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -30,178 +30,137 @@ import java.util.concurrent.atomic.AtomicInteger
3030
import java.util.concurrent.atomic.AtomicReference
3131

3232
/**
33-
* A subject implementation that dispatches signals to multiple
34-
* consumers or buffers them until such consumers arrive.
33+
* A subject implementation that awaits a certain number of collectors
34+
* to start consuming, then allows the producer side to deliver items
35+
* to them.
3536
*
3637
* @param <T> the element type of the [Flow]
3738
* @param bufferSize the number of items to buffer until consumers arrive
3839
*/
3940
@FlowPreview
40-
class MulticastSubject<T>(private val bufferSize: Int = 32) : AbstractFlow<T>(), SubjectAPI<T> {
41+
class MulticastSubject<T>(private val expectedCollectors: Int) : AbstractFlow<T>(), SubjectAPI<T> {
4142

42-
val queue = ConcurrentLinkedQueue<T>()
43+
val collectors = AtomicReference<Array<ResumableCollector<T>>>(EMPTY as Array<ResumableCollector<T>>)
4344

44-
val availableQueue = AtomicInteger(bufferSize)
45+
val producer = Resumable()
4546

46-
val consumers = AtomicReference(EMPTY as Array<ResumableCollector<T>>)
47-
48-
val producerAwait = Resumable()
49-
50-
val wip = AtomicInteger()
47+
val remainingCollectors = AtomicInteger(expectedCollectors)
5148

5249
@Volatile
53-
var error: Throwable? = null
50+
var terminated : Throwable? = null
5451

5552
override suspend fun emit(value: T) {
56-
while (availableQueue.get() == 0) {
57-
producerAwait.await()
53+
awaitCollectors()
54+
for (collector in collectors.get()) {
55+
try {
56+
collector.next(value)
57+
} catch (ex: CancellationException) {
58+
remove(collector)
59+
}
5860
}
59-
queue.offer(value)
60-
availableQueue.decrementAndGet();
61-
drain()
6261
}
6362

6463
override suspend fun emitError(ex: Throwable) {
65-
error = ex
66-
drain()
64+
//awaitCollectors()
65+
terminated = ex;
66+
for (collector in collectors.getAndSet(TERMINATED as Array<ResumableCollector<T>>)) {
67+
try {
68+
collector.error(ex)
69+
} catch (_: CancellationException) {
70+
// ignored at this point
71+
}
72+
}
6773
}
6874

6975
override suspend fun complete() {
70-
error = DONE
71-
drain()
76+
//awaitCollectors()
77+
terminated = DONE
78+
for (collector in collectors.getAndSet(TERMINATED as Array<ResumableCollector<T>>)) {
79+
try {
80+
collector.complete()
81+
} catch (_: CancellationException) {
82+
// ignored at this point
83+
}
84+
}
7285
}
7386

7487
override fun hasCollectors(): Boolean {
75-
return consumers.get().isNotEmpty();
88+
return collectors.get().isNotEmpty()
7689
}
7790

7891
override fun collectorCount(): Int {
79-
return consumers.get().size;
92+
return collectors.get().size
8093
}
8194

82-
override suspend fun collectSafely(collector: FlowCollector<T>) {
83-
val c = ResumableCollector<T>()
84-
if (add(c)) {
85-
c.readyConsumer()
86-
drain()
87-
c.drain(collector) { remove(it) }
88-
} else {
89-
val ex = error
90-
if (ex != null && ex != DONE) {
91-
throw ex
92-
}
95+
private suspend fun awaitCollectors() {
96+
if (remainingCollectors.get() != 0) {
97+
producer.await()
9398
}
9499
}
95100

96-
private fun add(collector: ResumableCollector<T>) : Boolean {
101+
@Suppress("UNCHECKED_CAST", "")
102+
private fun add(inner: ResumableCollector<T>) : Boolean {
97103
while (true) {
98-
val a = consumers.get()
99-
if (a == TERMINATED) {
104+
105+
val a = collectors.get()
106+
if (a as Any == TERMINATED as Any) {
100107
return false
101108
}
102-
val b = Array<ResumableCollector<T>>(a.size + 1) { idx ->
103-
if (idx < a.size) a[idx] else collector
104-
}
105-
if (consumers.compareAndSet(a, b)) {
109+
val n = a.size
110+
val b = a.copyOf(n + 1)
111+
b[n] = inner
112+
if (collectors.compareAndSet(a, b as Array<ResumableCollector<T>>)) {
106113
return true
107114
}
108115
}
109116
}
110-
private fun remove(collector: ResumableCollector<T>) {
117+
118+
@Suppress("UNCHECKED_CAST")
119+
private fun remove(inner: ResumableCollector<T>) {
111120
while (true) {
112-
val a = consumers.get()
121+
val a = collectors.get()
113122
val n = a.size
114123
if (n == 0) {
115124
return
116125
}
117-
var j = -1
118-
for (i in 0 until n) {
119-
if (a[i] == collector) {
120-
j = i
121-
break
122-
}
123-
}
126+
127+
val j = a.indexOf(inner)
124128
if (j < 0) {
125-
return;
129+
return
126130
}
131+
127132
var b = EMPTY as Array<ResumableCollector<T>?>
128133
if (n != 1) {
129-
b = Array<ResumableCollector<T>?>(n - 1) { null }
134+
b = Array(n - 1) { null }
130135
System.arraycopy(a, 0, b, 0, j)
131136
System.arraycopy(a, j + 1, b, j, n - j - 1)
132137
}
133-
if (consumers.compareAndSet(a, b as Array<ResumableCollector<T>>)) {
134-
return;
135-
}
136-
}
137-
}
138-
139-
private suspend fun drain() {
140-
if (wip.getAndIncrement() != 0) {
141-
return;
142-
}
143-
144-
while (true) {
145-
val collectors = consumers.get()
146-
if (collectors.isNotEmpty()) {
147-
val ex = error;
148-
val v = queue.poll();
149-
150-
if (v == null && ex != null) {
151-
finish(ex)
152-
}
153-
else if (v != null) {
154-
var k = 0;
155-
for (collector in consumers.get()) {
156-
try {
157-
//println("MulticastSubject -> [$k]: $v")
158-
collector.next(v)
159-
} catch (ex: CancellationException) {
160-
remove(collector);
161-
}
162-
k++
163-
}
164-
availableQueue.getAndIncrement()
165-
producerAwait.resume()
166-
continue
167-
}
168-
} else {
169-
val ex = error;
170-
if (ex != null && queue.isEmpty()) {
171-
finish(ex)
172-
}
173-
}
174-
if (wip.decrementAndGet() == 0) {
175-
break
138+
if (collectors.compareAndSet(a, b as Array<ResumableCollector<T>>)) {
139+
return
176140
}
177141
}
178142
}
179143

180-
private suspend fun finish(ex: Throwable) {
181-
if (ex == DONE) {
182-
for (collector in consumers.getAndSet(TERMINATED as Array<ResumableCollector<T>>)) {
183-
try {
184-
collector.complete()
185-
} catch (_: CancellationException) {
186-
// ignored
187-
}
144+
override suspend fun collectSafely(collector: FlowCollector<T>) {
145+
val rc = ResumableCollector<T>()
146+
if (add(rc)) {
147+
if (remainingCollectors.decrementAndGet() == 0) {
148+
producer.resume()
188149
}
150+
rc.drain(collector) { remove(it) }
189151
} else {
190-
for (collector in consumers.getAndSet(TERMINATED as Array<ResumableCollector<T>>)) {
191-
try {
192-
collector.error(ex)
193-
} catch (_: CancellationException) {
194-
// ignored
195-
}
152+
val ex = terminated;
153+
if (ex != null && ex != DONE) {
154+
throw ex
196155
}
197156
}
198157
}
199158

200159
companion object {
201-
val DONE: Throwable = Throwable("Subject Completed")
160+
val EMPTY = arrayOf<ResumableCollector<Any>>()
202161

203-
val EMPTY = arrayOf<ResumableCollector<Any>>();
162+
val TERMINATED = arrayOf<ResumableCollector<Any>>()
204163

205-
val TERMINATED = arrayOf<ResumableCollector<Any>>();
164+
val DONE = Throwable("Subject completed")
206165
}
207166
}

0 commit comments

Comments
 (0)