-
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
Outbound flow control bugfix #61
Changes from 23 commits
125e6a3
757da7f
11d7169
bed8d00
fec2f7d
5df774a
6b7fdea
7621579
afd315e
42a9460
4da8aaf
d99af22
bdba585
11b91e3
18aac4f
8e9bd7c
b4bd070
59715c5
5f326a3
4a667e7
7760fcd
c76b00b
19c0ce4
9bcd734
4375f58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
object Versions { | ||
const val protobuf = "3.7.1" | ||
const val grpc = "1.21.0" | ||
const val kotlin = "1.3.40" | ||
const val coroutines = "1.2.2" | ||
const val mockk = "1.9.1" | ||
const val protobuf = "3.9.0" | ||
const val grpc = "1.22.1" | ||
const val kotlin = "1.3.41" | ||
const val coroutines = "1.3.0-RC2" | ||
const val mockk = "1.9.3" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,8 +20,10 @@ import io.grpc.stub.CallStreamObserver | |
import kotlinx.coroutines.CoroutineExceptionHandler | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.channels.ActorScope | ||
import kotlinx.coroutines.channels.Channel | ||
import kotlinx.coroutines.launch | ||
import kotlinx.coroutines.channels.SendChannel | ||
import kotlinx.coroutines.channels.actor | ||
import java.util.concurrent.atomic.AtomicBoolean | ||
import java.util.concurrent.atomic.AtomicInteger | ||
|
||
|
@@ -42,40 +44,60 @@ internal fun <T> CallStreamObserver<*>.applyInboundFlowControl( | |
} | ||
} | ||
|
||
internal typealias MessageHandler = suspend ActorScope<*>.() -> Unit | ||
|
||
internal fun <T> CoroutineScope.applyOutboundFlowControl( | ||
streamObserver: CallStreamObserver<T>, | ||
targetChannel: Channel<T> | ||
){ | ||
val isOutboundJobRunning = AtomicBoolean() | ||
): SendChannel<MessageHandler> { | ||
|
||
val isCompleted = AtomicBoolean() | ||
val channelIterator = targetChannel.iterator() | ||
streamObserver.setOnReadyHandler { | ||
if(targetChannel.isClosedForReceive){ | ||
streamObserver.completeSafely() | ||
}else if( | ||
val messageHandlerBlock: MessageHandler = handler@ { | ||
while( | ||
streamObserver.isReady && | ||
!targetChannel.isClosedForReceive && | ||
isOutboundJobRunning.compareAndSet(false, true) | ||
channelIterator.hasNext() | ||
){ | ||
launch(Dispatchers.Unconfined + CoroutineExceptionHandler { _, e -> | ||
streamObserver.completeSafely(e) | ||
targetChannel.close(e) | ||
}) { | ||
try{ | ||
while( | ||
streamObserver.isReady && | ||
!targetChannel.isClosedForReceive && | ||
channelIterator.hasNext() | ||
){ | ||
val value = channelIterator.next() | ||
streamObserver.onNext(value) | ||
} | ||
if(targetChannel.isClosedForReceive){ | ||
streamObserver.onCompleted() | ||
} | ||
} finally { | ||
isOutboundJobRunning.set(false) | ||
} | ||
streamObserver.onNext(channelIterator.next()) | ||
} | ||
if(targetChannel.isClosedForReceive && isCompleted.compareAndSet(false,true)){ | ||
streamObserver.onCompleted() | ||
channel.close() | ||
} | ||
} | ||
|
||
val messageHandlerActor = actor<MessageHandler>( | ||
capacity = Channel.UNLIMITED, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A limited capacity didn't work? I am wondering if it's possible that the channel becomes a memory leak if jobs are added faster than the worker consumes them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thats a good catch. I must've changed it while debugging. I'll test it with the value reverted to This implementation is based off the native grpc util There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am using |
||
context = Dispatchers.Unconfined + CoroutineExceptionHandler { _, e -> | ||
streamObserver.completeSafely(e) | ||
targetChannel.close(e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is missing that the |
||
} | ||
) { | ||
|
||
for (handler in channel) { | ||
if (isCompleted.get()) break | ||
handler(this) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding this here reduced the problem but didn't eliminate it. I guess there must be other paths for the scope to cancel.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the scope could cancellation can also be propagated from its parent under normal normal coroutine usage. This case was covered before because executing new launch on a cancelled scope would take care. Its hard to reproduce but Im trying a few things now. |
||
if(!isCompleted.get()) { | ||
streamObserver.completeSafely() | ||
} | ||
} | ||
|
||
targetChannel.invokeOnClose { | ||
messageHandlerActor.close() | ||
} | ||
|
||
streamObserver.setOnReadyHandler { | ||
try { | ||
if(!messageHandlerActor.isClosedForSend){ | ||
messageHandlerActor.offer(messageHandlerBlock) | ||
} | ||
}catch (e: Throwable){ | ||
// If offer throws an exception then it is | ||
// either already closed or there was a failure | ||
// which has already cleaned up call resources | ||
} | ||
} | ||
|
||
return messageHandlerActor | ||
} |
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 outbound flow control handler has been refactored. It no longer spawns multiple jobs when applying backpressure and can properly handle superfluous invocations of the on ready handler runnable.