-
Notifications
You must be signed in to change notification settings - Fork 981
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
Allocation-free Write(AndFlush)Async using ValueTask #375
base: dev
Are you sure you want to change the base?
Conversation
@@ -35,10 +36,10 @@ protected override void ChannelRead0(IChannelHandlerContext contex, string msg) | |||
response = "Did you say '" + msg + "'?\r\n"; | |||
} | |||
|
|||
Task wait_close = contex.WriteAndFlushAsync(response); | |||
ValueTask wait_close = contex.WriteAndFlushAsync(response); |
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.
waitClose?
} | ||
catch (Exception ex) | ||
{ | ||
return TaskEx.FromException(new EncoderException(ex)); | ||
throw new EncoderException(ex); |
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.
throwing synchronously in async method is a bad practice. Can we return failure in ValueTask?
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.
Any reason it was diverged from original netty in a first place:
https://github.com/netty/netty/blob/0692bf1b6a6764dee22132630bef7ee2f850771e/codec/src/main/java/io/netty/handler/codec/MessageToByteEncoder.java#L125
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.
and what happens to that exception?
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.
Presumable it baubles up all the way to a caller
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.
However ChannelHandlerContext always converts it to failed future:
https://github.com/netty/netty/blob/74f24a5c19f8f351e9c6a7a84bdd9fbcc7a07ada/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java#L740
So probably context is assumed to be higher level exception handler here.
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.
so, pro: we're collecting stack trace which might be useful debugging real issue
cons: we're collecting stack trace which is costly (prohibitively so in some scenarios).
not sure what is worse here. I'd guess if we wanted to return a failed future here it would be a valuetask wrapping failed Task, right?
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.
main pro I see: we're close to original source code so it won't cause unexpected behavior when porting code based on this encoder/decoder infra in future.
For failed future yes, Exception -> Task -> ValueTask. Stephen Toub explicitly said that exceptions are already expensive enough and making ValueTask reference exception instance directly doesn't make sense.
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.
@nayato @maksimkim If we assume Exceptions are rare, it would ok to accept the perf hit.
df42e17
to
9e5d322
Compare
{ | ||
try | ||
{ | ||
await future; |
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.
AbstractPromise now can flow both execution and synchronization context. What is our context management story?
- Can we use await in dotnetty
- If we can, should it always be ConfigureAwait false?
- Should we suppress execution context?
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.
if you do .ConfigureAwait(false)
, you would erase notion of custom task scheduler that dispatches continuation to this same event loop.
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.
So in case of ValueTask it's IValueTaskSource implementation responsibility to execute continuation. That implementation presumably not aware of any task scheduler.
d0d827a
to
e4a836d
Compare
d3240b7
to
c468763
Compare
CloseInput((IChunkedInput<T>)promise.Message); | ||
promise.TrySetException(ex); | ||
} | ||
} |
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.
@StormHub , I've aligned behavior with netty here: progress() never completes promise
https://github.com/netty/netty/blob/8d78893a76a8c14c18b304ffe1fce2dc43ed4206/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java#L278
Any reason it was ported in different way?
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.
@maksimkim I don't think there is a particular reason. Let me double check.
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.
@maksimkim I think I did it wrong. Netty progress never completes the promise.
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.
@maksimkim also note that success also calls progress
https://github.com/netty/netty/blob/4f982be91e6d61c7c7a9d8d63115e0ebe65b2774/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java#L350
build.cake
Outdated
@@ -50,7 +50,7 @@ Task("Restore-NuGet-Packages") | |||
.Description("Restores dependencies") | |||
.Does(() => | |||
{ | |||
DotNetCoreRestore(); | |||
DotNetCoreRestore(new DotNetCoreRestoreSettings { ConfigFile = ".nuget\\nuget.config" }); |
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.
do we still need this? and change to nuget.config above?
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.
we don't
@@ -67,11 +67,13 @@ public interface IChannelHandlerContext : IAttributeMap | |||
|
|||
IChannelHandlerContext Read(); | |||
|
|||
Task WriteAsync(object message); // todo: optimize: add flag saying if handler is interested in task, do not produce task if it isn't needed | |||
ValueTask WriteAsync(object message); // todo: optimize: add flag saying if handler is interested in task, do not produce task if it isn't needed | |||
|
|||
IChannelHandlerContext Flush(); | |||
|
|||
Task WriteAndFlushAsync(object message); |
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.
Since WriteAndFlushAsync is consequent WriteAsync and Flush, and later can cause former to complete synchronously, ValueTask returned from WriteAsync backed by recyclable future is always materialized in full blown Task. So I kept Task as return type. Assumption is that those who want to do write+flush in non-allocating fire-and-forget mode call overload and pass false in notifyComplete. In that case overload always returns completed ValueTask.
7f473c1
to
1236174
Compare
Task writeTask = ctx.WriteAndFlushAsync(upgradeResponse); | ||
|
||
// Perform the upgrade to the new protocol. | ||
this.sourceCodec.UpgradeFrom(ctx); |
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.
@StormHub, check this out. I change it to align with netty. So upgrade doesn't wait write to complete.
https://github.com/netty/netty/blob/00afb19d7a37de21b35ce4f6cb3fa7f74809f2ab/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java#L324
} | ||
|
||
Task task = base.WriteAsync(ctx, msg).AsTask(); | ||
task.ContinueWith(this.upgradeCompletedContinuation, ctx, TaskContinuationOptions.ExecuteSynchronously); |
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.
2b21e67
to
c1c9865
Compare
Motivation:
corefx 2.1 will introduce ability to implement allocation free async awaitables using existing ValueTask struct and new IValueTaskSource interface (https://github.com/dotnet/corefx/issues/27445). PR adopts this new feature for write apis to avoid unnecessary Task allocation on every WriteAsync call.
ValueTask backed by IValueTaskSource by design has lots of limitations (see corefx issue for details).
In this PR IValueTaskSource is mostly implemented by recyclable objects (ChannelOutboundBuffer.Entry, PendingWriteQueue.PendingWrite, AbstractChannelHandlerContext.WriteTask, etc). It means than once completed it is immediately recycled which has certain implication on how it can be used inside and outside DotNetty code:
Once ValueTask is returned by WriteAsync call it should be either immediately awaited or immediately converted to Task or never touched again (e.g. storing it as a class field is not allowed).
Not following this restriction can't be verified by compiler and can cause runtime error (if it was recycled after completion) or unpredictable result (if it was reactivated from pool for different async operation).
WriteAndFlushAsync method doesn't follow guideline above (it calls WriteAsync then Flush and then returns ValueTask to a caller) that is why new overload of WriteAndFlushAsync is introduced.
New notifyComplete bool parameter defines if caller is interested in completion notification (for the penalty of Task allocation) or not. In later case completed ValueTask is returned from WriteAndFlushAsync.
Modifications:
Result: