Skip to content
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

Make clients sendable #1386

Merged
merged 5 commits into from
Apr 11, 2022
Merged

Conversation

glbrntt
Copy link
Collaborator

@glbrntt glbrntt commented Apr 5, 2022

Motivation:

gRPC clients should be Sendable.

Modifications:

This commit contains a number of changes to enable clients to be
'Sendable'. The are:

  • Require GRPCChannel to be 'Sendable' since the underlying API should
    be thread-safe.
  • Require GRPCClient to be 'Sendable' as this just provides wrappers
    around a GRPCChannel which is also thread-safe. This propagates to
    generated client protocols which refine GRPCClient.
  • Generated clients using the NIO API are classes (!) holding mutable
    call options and interceptor factories. Codegen was updated so make
    these @unchecked Sendable by protecting mutable state with a lock.
    Because this generates additional allocations and should never have
    been a class it is now marked deprecated in favor of a struct based
    client.
  • AnyServiceClient is also a class and has been deprecated in favor of
    the struct based GRPCAnyServiceClient.
  • A separate commit fixes these deprecation warnings.
  • The test clients have also been marked as deprecated: they rely on an
    EmbeddedChannel which is not and will never be Sendable (without
    also being deprecated). The test clients are also marked
    as @unchecked Sendable to satisfy the requirement on GRPCClient
    despite being deprecated. The same is true for the FakeChannel (a
    GRPCChannel). We reccomend using a client and server on localhost as
    a replacement.
  • Various supporting value types are marked as Sendable.

Result:

  • Clients are now Sendable.
  • Clients are now always structs.
  • Generated test clients are deprecated.

@glbrntt
Copy link
Collaborator Author

glbrntt commented Apr 5, 2022

Note to reviewers:

  • The bulk of the changes are in: 5500255
  • Generated clients are updated in: 409b285
  • Deprecation warnings are fixed in: 14e3b26

@@ -1,113 +0,0 @@
/*
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This file was only used in tests, its new home is in Tests/GRPCTests/ClientTimeoutTests.swift.

Comment on lines +172 to +262
private final class EmbeddedGRPCChannel: GRPCChannel {
let embeddedChannel: EmbeddedChannel
let multiplexer: EventLoopFuture<HTTP2StreamMultiplexer>

let logger: Logger
let scheme: String
let authority: String
let errorDelegate: ClientErrorDelegate?

func close() -> EventLoopFuture<Void> {
return self.embeddedChannel.close()
}

var eventLoop: EventLoop {
return self.embeddedChannel.eventLoop
}

init(
logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }),
errorDelegate: ClientErrorDelegate? = nil
) {
let embeddedChannel = EmbeddedChannel()
self.embeddedChannel = embeddedChannel
self.logger = logger
self.multiplexer = embeddedChannel.configureGRPCClient(
errorDelegate: errorDelegate,
logger: logger
).flatMap {
embeddedChannel.pipeline.handler(type: HTTP2StreamMultiplexer.self)
}
self.scheme = "http"
self.authority = "localhost"
self.errorDelegate = errorDelegate
}

internal func makeCall<Request: Message, Response: Message>(
path: String,
type: GRPCCallType,
callOptions: CallOptions,
interceptors: [ClientInterceptor<Request, Response>]
) -> Call<Request, Response> {
return Call(
path: path,
type: type,
eventLoop: self.eventLoop,
options: callOptions,
interceptors: interceptors,
transportFactory: .http2(
channel: self.makeStreamChannel(),
authority: self.authority,
scheme: self.scheme,
// This is internal and only for testing, so max is fine here.
maximumReceiveMessageLength: .max,
errorDelegate: self.errorDelegate
)
)
}

internal func makeCall<Request: GRPCPayload, Response: GRPCPayload>(
path: String,
type: GRPCCallType,
callOptions: CallOptions,
interceptors: [ClientInterceptor<Request, Response>]
) -> Call<Request, Response> {
return Call(
path: path,
type: type,
eventLoop: self.eventLoop,
options: callOptions,
interceptors: interceptors,
transportFactory: .http2(
channel: self.makeStreamChannel(),
authority: self.authority,
scheme: self.scheme,
// This is internal and only for testing, so max is fine here.
maximumReceiveMessageLength: .max,
errorDelegate: self.errorDelegate
)
)
}

private func makeStreamChannel() -> EventLoopFuture<Channel> {
let promise = self.eventLoop.makePromise(of: Channel.self)
self.multiplexer.whenSuccess {
$0.createStreamChannel(promise: promise) {
$0.eventLoop.makeSucceededVoidFuture()
}
}
return promise.futureResult
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

glbrntt added 3 commits April 5, 2022 15:19
Motivation:

gRPC clients should be 'Sendable'.

Modifications:

This commit contains a number of changes to enable clients to be
'Sendable'. The are:

- Require `GRPCChannel` to be 'Sendable' since the underlying API should
  be thread-safe.
- Require `GRPCClient` to be 'Sendable' as this just provides wrappers
  around a `GRPCChannel` which is also thread-safe. This propagates to
  generated client protocols which refine `GRPCClient`.
- Generated clients using the NIO API are classes (!) holding mutable
  call options and interceptor factories. Codegen was updated so make
  these `@unchecked Sendable` by protecting mutable state with a lock.
  Because this generates additional allocations and should never have
  been a class it is now marked deprecated in favor of a struct based
  client.
- `AnyServiceClient` is also a class and has been deprecated in favor of
  the struct based `GRPCAnyServiceClient`.
- A separate commit fixes these deprecation warnings.
- The test clients have also been marked as deprecated: they rely on an
  `EmbeddedChannel` which is not and will never be `Sendable` (without
  also being deprecated). The test clients are also marked
  as `@unchecked Sendable` to satisfy the requirement on `GRPCClient`
  despite being deprecated. The same is true for the `FakeChannel` (a
  `GRPCChannel`). We reccomend using a client and server on localhost as
  a replacement.
- Various supporting value types are marked as `Sendable`.

Result:

- Clients are now `Sendable`.
- Clients are now always `struct`s.
- Generated test clients are deprecated.
@glbrntt glbrntt force-pushed the gb-more-sendable branch from c6599ea to 14e3b26 Compare April 5, 2022 14:19
@@ -160,14 +174,22 @@ extension CallOptions {
}

/// Provide a factory to generate request IDs.
#if swift(>=5.6)
public static func generated(
_ requestIDFactory: @escaping @Sendable () -> String
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 promote the StringFactory typealias to public here to avoid the extra #if?

// Interceptors sendability is unchecked: they have their own documented rules for thread safety
// which we can't check. We require them to be sendable in order for interceptor factories to be
// sendable and in turn, clients.
extension ClientInterceptor: @unchecked Sendable {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this need to be preconcurrency, given that the class already existed and is open for subclassing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, yes it sure does!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually I'm no so sure: we'd need to mark the class with @preconcurrency and doing so means we'd need to duplicate the whole class.

Also if we are to require it be Sendable then we must mark the base class as @unchecked Sendable because it's open (if we mark it as GRPCSendable we get "Non-final class 'ClientInterceptor' cannot conform to 'Sendable'; use '@unchecked Sendable'", which makes sense, we don't know the concrete type at compile time).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd have to be both @preconcurrency and @unchecked. But I think it's API breaking if we don't add the @preconcurrency annotation, because any adopters who are not concurrency aware but build in 5.6 mode will break.

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 think they inherit the @unchecked (none of the subclasses within GRPCTests complained about not being sendable, some of which have mutable state)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think @unchecked should be inherited, so far as I know.

Copy link
Collaborator

Choose a reason for hiding this comment

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

However, I can't find any immediate citation for that belief.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmmm, this seems relevant.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As does the final post https://forums.swift.org/t/improving-sendable/52377/26:

There are a number of unresolved issues with Sendable that could be tackled separately. Here's the list I know of:
...

  • Sendable with class types needs to be revisited. We likely want to require subclasses to restate Sendable conformance, decide if there are any cases where that conformance need not be @unchecked (e.g., all state is immutable and Sendable) and whether it can ever be inferred (same conditions as for removing @unchecked?).

I'm not sure exactly where that leaves us but I think @preconcurrency and @unchecked is probably the safest bet here.

@glbrntt glbrntt marked this pull request as ready for review April 5, 2022 15:26
@glbrntt glbrntt added the 🆕 semver/minor Adds new public API. label Apr 5, 2022
@glbrntt
Copy link
Collaborator Author

glbrntt commented Apr 5, 2022

Also not sure if ...NIOClient is the right new spelling for the future/callback based API (the client with the async based API is ...AsyncClient). Happy to take suggestions.

@Lukasa
Copy link
Collaborator

Lukasa commented Apr 5, 2022

I think NIOClient is totally fine there.

@glbrntt glbrntt requested a review from Lukasa April 11, 2022 08:34
@Lukasa Lukasa merged commit 305677e into grpc:1.7.1-async-await Apr 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants