-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
servlet: Implement gRPC server as a Servlet #4738
Conversation
d967b77
to
47b7090
Compare
Trailers should be supported. Is there an easy way for me to test this so I can see what is wrong? |
@stuartwdouglas The StackOverflow question has provided a simple servlet, I couldn't even make that work. I might have missed some configuration. If you can successfully run that servlet and verify that client side receives the trailers, just let me know the steps I might have missed. Really appreciate it. On my machine, I can verify Glassfish, Tomcat and Jetty all send trailers, but Undertow/Wildfly does not. |
This should be fixed in Undertow 2.0.12.Final, which will be released shortly. |
Thanks a lot @stuartwdouglas , I will test on it once it's released. Right now it is not straight forward to run the InteropTest for this PR, I'm trying to make the steps easier. |
@dapengzhang0 can you try Jetty 9.4.11? It should have similar support for 4.0, trailers, etc. that Jetty 10 has. |
@sbordet I tried Jetty 9.4.11, it did not work
It seems it does not support servlet 4.0. Actually its release note didn't claim it supports servlet 4.0. <dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
... |
47b7090
to
1beb1ea
Compare
@dapengzhang0 thanks for trying. Yes, with 9.4.x you need to downcast to Jetty specific classes to get the trailer methods. Never mind 9.4.x, and happy that Jetty 10.0.x works well for this case. |
1beb1ea
to
9dcafd1
Compare
61b001f
to
09c60d8
Compare
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.
There's a lot here, and much of it has obvious things that need to be cleaned up before a real review. What did you want me to look at for the initial review?
b441dd7
to
ef44f10
Compare
@ejona86 aside from impl details, for the initial review, is there anything fundamentally in a wrong approach, or anything important is missing (such as |
1ebbc6e
to
6667208
Compare
@ejona86 Current write path implementation is so difficult to see the correctness. Will test out using a lock for the write path. Will update the PR if it works for all the three major containers. If any of containers has an issue, I will update the PR once the issue is fixed on the container side (Must guarantee the impl is well tested on three containers). |
Inspired by @creamsoup , I found a simple and clean solution, reusing Executor wirteExecutor = new SerializingExecutor(directExecutor());// SyncCtx also works
boolean isWriteReady;
Queue< WritableBuffer> wirteQueue = new ArrayDeque();
void onWritePossible() {
writeExecutor.execute(
() -> {
assert !isWriteReady;
isWriteReady = outputStream.isReady();
// the above is the ONLY place isWriteReady could flip from false to true
while (isWriteReady && !writQueue.isEmpty()) {
WritableBuffer buffer = writeQueue.poll();
outputStream.wirte(writeQueue.poll().readableBytes());
isWriteReady = outputStream.isReady();
// if the above returns false, another onWritePossible() will be called
// by the container *later*,
}
});
}
// The difference from @creamsoup's approach is that writeFrame here never calls drain()
void writeFrame(WritableBuffer buffer) {
writeExecutor.execute(
() -> {
if (isWriteReady) {
assert writeQueue.isEmpty();
outputStream.wirte(buffer.readableBytes());
isWriteReady = outputStream.isReady();
// if the above returns false, onWritePossible() will be called
// by the container *later*
} else {
writeQueue.offer(buffer);
}
})
} Update: This approach also has a subtle bug. |
@dapengzhang0, that looks very concise and easy to follow. does it work on all 3 servlets? |
7c82ff5
to
b2be863
Compare
b2be863
to
3fd4ca8
Compare
transportListener.transportTerminated(); | ||
} | ||
|
||
private static final class GrpcAsycListener implements AsyncListener { |
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.
typo? Asyc -> Async
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.
Yeah. Thanks. I also noticed that just had not a chance to fix yet.
} | ||
|
||
@Override | ||
protected void startServer(ServerBuilder<?> builer) { |
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.
protected void startServer(ServerBuilder<?> builder)
change builer to builder on this line and the next?
Hi all, |
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.
Firstly, really nice work on this feature - I think it's a brilliantly useful idea. I've been doing some testing and one bug came up for me that I wanted to mention!
this.logId = logId; | ||
this.asyncCtx = asyncCtx; | ||
this.resp = (HttpServletResponse) asyncCtx.getResponse(); | ||
resp.getOutputStream().setWriteListener(new GrpcWriteListener()); |
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.
When testing against Payara server, this class seemingly fails to completely initialise unless the writer
field is initialised before the write listener on this line. When swapped I get NPEs in the OutputBuffer, but I'm willing to assume this could just be an expected Grizzly bug. I do, however, at least get the expected communication between the client and server.
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.
In c903d75#diff-61714cd00cc4b70a569fd5ac7ac6919bcc9e2e76b6c0384ffbbeda9587c5ff93 of #8596, I swapped the assignment of the writer
field. Thanks for catching this.
Is this still maintained? |
@dapengzhang0 are you planning to drive this to completion? |
Trying to use this in Payara, and getting all sorts of intermittent errors. Doesn't look like this works correctly currently |
@lprimak, I tested GlassFish 5.0/Grizzly, it didn't pass all the interop tests like Jetty/UnderTow/Tomcat. Seems there are some HTTP/2 & aysnc-servlet issues in Grizzly. Payara is also based on Grizzly kernel so there could be the same problems. I didn't have time to identify the Grizzly bugs. Sorry for being inactive for a long time. |
@dapengzhang0 Thanks, but are you maintaining this branch? Will it ever be merged? |
Looks like these issues have been fixed in Payara in 2018 @dapengzhang0 |
Can you at least update this PR to fix #4738 (review) and have it working on the lastes gRPC-Java please? |
@dapengzhang0 is this worked on? Looking forward to this! <3 |
A newer version of this work is #8596, although there is no update on it for a long time too. Close this PR. |
#8596 is now merged in https://github.com/grpc/grpc-java/releases/tag/v1.53.0 |
The goal is as described in #1621 to add the ability to run a gRPC server as a Servlet on any web container with the Servlet 4.0 support and HTTP/2 enabled.
cc @vlsinitsyn @cs224 @jnehlmeier @hofmanndavid
cc @meltsufin for high level design
API
This PR provides the following API:
An adapter
and a grpc server builder
and a servlet impl
A
ServletAdapter
instance will be backing one or more gRPC services with aServletServerBuilder
A servlet powering the gRPC services will be as simple as either
or
Alternative API
Hello World example
See examples/example-servlet/src/main/java/io/grpc/servlet/examples/helloworld/
Implementation
Aside from trivial API pluming, what the impl actually does is (in
doPost()
method)AsyncContext
WriteListener
andReadListener
for theServletOutputStream
andServletInputStream
io.grpc.servlet.ServletServerStream
toServerTransportListener.streamCreated(serverStream, method, headers)
ServletServerStream
callsstream.transportState().inboundDataReceived()
in theReadListener.onDataAvailable()
callbackServletServerStream
holds a WritableBufferAllocator for the outbound data that will write to theServletOutputStream
, and uses an atomic ref ofServletServerStream.WriteState
to coordinate with theWriteListener.onWritePossible()
callback.ServletServerStream.TransportState.runOnTransportThread()
method uses aSerializingExecutor(directExecutor())
Test result
We tested it with a servlet backed by the InteropTest gRPC service
TestServiceImp
, and an ordinary gRPC InteropTest client, for the test casesEMPTY_UNARY
,LARGE_UNARY
,CLIENT_STREAMING
,SERVER_STREAMING
, andPING_PONG
. The following are the test results of some of the web container vendorsJetty 10.0-SNAPSHOT
All tests passed!
Jetty looks fully compliant with the Servlet 4.0 spec and HTTP/2 protocol. 💯
@sbordet
GlassFish 5.0 - Web Profile
All tests passed with minor workaround.
An issue of GlassFish is filed, cc @MattGill98 for help:
Tomcat 9.0.10
Non-blocking servlet I/O (ReadListener/WriteListener) practically can not work in full-duplex case. Only
EMPTY_UNARY
test passed.(Update:
Filed multiple bugs to Tomcat: cc @markt-asf for help)
(Update: Tomcat 9.0.x trunk
All tests passed! 💯)
Undertow 2.0.11.Final/WildFly-14.0.0.Beta1
Not able to test. Simply can not make the servlet container send out trailers for plain servlet. cc @ctomc @stuartwdouglas for help
(Update: Identified as undertow bug
)
(Update: Undertow 2.0.12.Final
All tests passed! 💯)