Skip to content

Conversation

@glbrntt
Copy link
Collaborator

@glbrntt glbrntt commented Nov 24, 2023

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

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
@glbrntt glbrntt added the version/v2 Relates to v2 label Nov 24, 2023
Comment on lines 65 to 67
/// 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).
Copy link
Collaborator

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?

Copy link
Collaborator Author

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...

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

case stopped
}

private let state: LockedValueBox<State>
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Comment on lines 74 to 77
public var transports: Transports

/// The services registered which the server is serving.
public var services: Services
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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 {
Copy link
Collaborator

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()
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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?

Suggested change
try await self.withInProcessClientConnectedToServer(services: []) { client, _ in
try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in

let counter = ManagedAtomic(0)

try await self.withInProcessClientConnectedToServer(
services: [],
Copy link
Collaborator

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()
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

/// ## 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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


/// 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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

Copy link
Collaborator

@gjcairo gjcairo left a 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 👍🏼

glbrntt and others added 3 commits November 27, 2023 12:55
@glbrntt glbrntt merged commit 3835e14 into grpc:main Nov 27, 2023
@glbrntt glbrntt deleted the gb-server branch November 27, 2023 14:46
@glbrntt glbrntt mentioned this pull request Nov 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

version/v2 Relates to v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants