-
Notifications
You must be signed in to change notification settings - Fork 435
Add new Server object #1721
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 new Server object #1721
Conversation
Motivation: The server is the main entry point for running gRPC services. It combines a set of transports, services, and interceptors. Modifications: - Add the 'Server' and 'ServerError' Result: Can piece together transports, interceptors and services
Sources/GRPCCore/Server.swift
Outdated
| /// you can cancel the task running your server. If your services require additional resources that | ||
| /// need their lifecycles managed you should consider using [Swift Service | ||
| /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). |
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.
I love the callout but I think we should be even more specific here. Not only when you need handling additional resources but also when you want to add support to gracefully shutdown the server. WDYT?
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.
Can you be more specific? We support graceful shutdown 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.
Maybe we can give a general recommendation that we recommend running the server in a ServiceGroup since that gives you orchestration of multiple services and support for listening to signals to trigger graceful shutdown.
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.
I'm a bit hesitant to document specifics here. I think the best solution is to have a DocC article which runs through using lifecycle with grpc-swift (which we should link to from these docs). It also means there's a single place which needs to be kept up-to-date.
Sources/GRPCCore/Server.swift
Outdated
| case stopped | ||
| } | ||
|
|
||
| private let state: LockedValueBox<State> |
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.
Interesting. I didn't expect to have a struct with reference semantics in the end. For ServiceLifecycle we decided to use an actor for the ServiceGroup. Here I would have actually expected a class. Especially since you can do weird things like this
var server = Server()
server.transport = [SomeTransport()]
async let _ = server.run()
server.transports = [OtherTransport()]
async let _ = server.run()
IMO here having a class for the Server makes more 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.
Yeah, probably should be a class tbh. I started with a struct and didn't think to back out that change when I added the state management.
Sources/GRPCCore/Server.swift
Outdated
| public var transports: Transports | ||
|
|
||
| /// The services registered which the server is serving. | ||
| public var services: Services |
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.
I am a bit unsure about making those vars. In the ServiceGroup I had the same decision and went with expecting everything in the inits. The reason for this was that you really expect everything to be setup before run() is called and allowing users to change them after run() is called is a bit weird.
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.
IMO these make the API easier to use, passing everything in init is quite awkward when there are potentially many things to pass in and is awkward w.r.t. API evolution. Sure, you can wrap everything up in a config, but that just adds more indirection for users.
I don't see changing things after run() to be an issue to be honest, trying to make changes after run() has been called just makes no sense at all so it's highly unlikely that someone would fall foul to this. We also document quite clearly that this isn't supported.
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.
Hm, I am not fully convinced yet. Let's imagine the following
var server = Server()
server.transports = [NIOH2Transport(port: 1234)]
async let _ = try await server.run()
server.transports = [NIOH2Transport(port: 5677])
async let _ try await server.run()
There really isn't a reason the second run should fail but it does because of the shared state in the struct. At least personally, I find these semantics confusing. I do get the reason why we want a struct here but then maybe we shouldn't share the state across run invocations?
| public var interceptors: Interceptors | ||
|
|
||
| /// The state of the server. | ||
| private enum State { |
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.
Can we move the transports & interceptors into the State so we release them as early as possible?
|
|
||
| for transport in self.transports.values { | ||
| do { | ||
| let listener = try await transport.listen() |
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.
Interesting. I did not expect this. I expected that we had to create a child task per transport and each transport has a run method as well? In which task do transports run 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.
We do have a child task per transport (see handleRequests). We start them all in the same task so we can ensure they all start successfully.
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.
Just to clarify this. Does that mean that beginListening is expected to bind the port? Because so far we always followed the run pattern where inside the run() method call we bind the ports and then consume the inbound connection/data.
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.
listen() is expected to bind the port. listen() is on the transport which is a lower-level API which users aren't expected to interact with. The run() on the server which we do expect users to interact with does do the bind and consume.
| } | ||
|
|
||
| func testUnimplementedMethod() async throws { | ||
| try await self.withInProcessClientConnectedToServer(services: []) { client, _ in |
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.
Should we also have a test where we do register the service but we also check we get an unimplemented error?
| try await self.withInProcessClientConnectedToServer(services: []) { client, _ in | |
| try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in |
| let counter = ManagedAtomic(0) | ||
|
|
||
| try await self.withInProcessClientConnectedToServer( | ||
| services: [], |
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.
Same here, may be worth having a test that checks the interceptors are not applied even when we have a service registered but that doesn't implement a given method.
|
|
||
| func testTestRunServerWithNoTransport() async throws { | ||
| let server = Server() | ||
| try await server.run() |
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.
Would it be reasonable to throw if we try to run a server without a transport configured?
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.
Good question, I thought about this too but decided against it at the time because it was easier to explain in a sentence what the function does. I think I've changed my mind (again) though: the most likely reason for not configuring a transport is a bug, we should make that more obvious so let's throw an error.
Sources/GRPCCore/Server.swift
Outdated
| /// ## Starting and stopping the server | ||
| /// | ||
| /// Once you have configured the server call ``run()`` to start it. Calling ``run()`` starts each | ||
| /// of the servers transports. A ``ServerError`` is thrown if any of the transports can't be |
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.
| /// of the servers transports. A ``ServerError`` is thrown if any of the transports can't be | |
| /// of the server's transports. A ``ServerError`` is thrown if any of the transports can't be |
Sources/GRPCCore/Server.swift
Outdated
|
|
||
| /// Signal to the server that it should stop listening for new requests. | ||
| /// | ||
| /// By calling this function you indicate to clients that they mustn't make start new requests |
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.
| /// By calling this function you indicate to clients that they mustn't make start new requests | |
| /// By calling this function you indicate to clients that they mustn't start new requests |
gjcairo
left a comment
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.
Left a tiny nit about a doc but looking good 👍🏼
Co-authored-by: Gustavo Cairo <me@gustavocairo.com>
Motivation:
The server is the main entry point for running gRPC services. It combines a set of transports, services, and interceptors.
Modifications:
Result:
Can piece together transports, interceptors and services