Skip to content

Commit bf20e93

Browse files
authored
First implementation of gRPC based on SwiftNIO (#281)
* First experiments with a NIO-based gRPC server. Contains the following commits: - Refactor gRPC decoding into dedicated codec classes. - Start work on GRPCServerHandler. - Add a "unary call handler" and use that for the tests. - Refactoring starting a GRPC server into a dedicated class. - Fix sending unary responses. - Add a handler for client-streaming calls. - Also implement bidirectional-streaming calls. - Make sure to flush in server-streaming calls after each sent message. - Add the missing test cases to `allTests`. - Refactor `StatusSendingHandler` into its own class. - Rename `GRPCServerHandler` to `GRPCChannelHandler`. - Remove a FIXME. - Add a few more comments. - Attach the actual call handlers as channel handlers instead of manually forwarding messages to them. Remove SwiftGRPCNIO's dependency on SwiftGRPC and move the responsibility for encoding GRPC statuses to HTTP1ToRawGRPCServerCoded. Temporarily disable two test cases that are failing at the moment. Add SwiftGRPCNIO as an exposed library. Another try at getting CI to work with SwiftGRPCNIO. More dependency fixes. Add `SwiftGRPCNIO.EchoServerTests` to LinuxMain.swift. Fix a string comparison in `.travis-install.sh`. Add nghttp2 to the list of CI dependencies. Another try with installing nghttp2 via brew. Another try at using libnghttp2-dev under Ubuntu 14.04. More Travis fixes. One last try. Disable two more tests for now, as they sometimes fail on CI. Make Carthage debug builds verbose. Only use SwiftGRPC-Carthage.xcodeproj for Carthage builds. * Make `ServerStreamingCallHandler.sendMessage` return a send future as well. * Re-enable two more tests and suppress two warnings. * Unify the interface across the different call handlers. * Rename `...CallHandler.handler` to `.eventObserver`. * Add support for returning custom statuses (e.g. with additional metadata attached) from calls that have a unary response. * Minor argument reordering. * Avoid forcing unary call handlers to return an event loop future. Instead, they should fulfill `handler.responsePromise`. * Add a TODO. * Add codegen support for non-TestStub NIO server code. * Add more properties to GRPCCallHandler. * Store the full `HTTPRequestHead` alongside a gRPC call handler. * Add support for having client-streaming request handlers return a future as their event handler. This allows them to perform some asynchronous work before having to return an event handler (e.g. to authenticate the request asynchronously before providing an event handler that relies on such authentication). * Make `StatusSendingHandler.statusPromise` public. * Convert a few non-blocking calls in tests to blocking ones to simplify things. * Refactoring: pass special `ResponseHandler` objects to NIO server call handlers with a much lower API surface. In addition, these `ResponseHandler` are easier to stub for testing. * Code review fixes, interface improvements. * Rename a few NIO tests. * Add documentation. * Rename "headers" to "request". * Minor performance improvement by avoiding one copy. * Make unary calls take a `StatusOnlyCallContext` instead of `UnaryResponseCallContext`, as suggested by @kevints. * Rename `sendOperationChain` in tests to `endOfSendOperationQueue`. * Review fixes. * Add one more comment to the README. * Oops, fix the tests. * Remove two unnecessary server channel options. * Add some more documentation. * Pin `SwiftNIOHTTP2` for the time being.
1 parent 1d422fc commit bf20e93

30 files changed

+1361
-50
lines changed

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ sudo: false
3434

3535
addons:
3636
apt:
37+
sources:
38+
- sourceline: 'ppa:ondrej/apache2' # for libnghttp2-dev
3739
packages:
3840
- clang-3.8
3941
- lldb-3.8
@@ -46,6 +48,10 @@ addons:
4648
- uuid-dev
4749
- curl
4850
- unzip
51+
- libnghttp2-dev
52+
53+
before_install:
54+
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install nghttp2; fi
4955

5056
install: ./.travis-install.sh
5157

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ test-plugin:
4545
protoc Sources/Examples/Echo/echo.proto --proto_path=Sources/Examples/Echo --plugin=.build/debug/protoc-gen-swift --plugin=.build/debug/protoc-gen-swiftgrpc --swiftgrpc_out=/tmp --swiftgrpc_opt=TestStubs=true
4646
diff -u /tmp/echo.grpc.swift Sources/Examples/Echo/Generated/echo.grpc.swift
4747

48+
test-plugin-nio:
49+
swift build $(CFLAGS) --product protoc-gen-swiftgrpc
50+
protoc Sources/Examples/Echo/echo.proto --proto_path=Sources/Examples/Echo --plugin=.build/debug/protoc-gen-swift --plugin=.build/debug/protoc-gen-swiftgrpc --swiftgrpc_out=/tmp --swiftgrpc_opt=Client=false,NIO=true
51+
diff -u /tmp/echo.grpc.swift Tests/SwiftGRPCNIOTests/echo_nio.grpc.swift
52+
4853
xcodebuild: project
4954
xcodebuild -project SwiftGRPC.xcodeproj -configuration "Debug" -parallelizeTargets -target SwiftGRPC -target Echo -target Simple -target protoc-gen-swiftgrpc build
5055

Package.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ import PackageDescription
1818

1919
var packageDependencies: [Package.Dependency] = [
2020
.package(url: "https://github.com/apple/swift-protobuf.git", .upToNextMinor(from: "1.1.1")),
21-
.package(url: "https://github.com/kylef/Commander.git", from: "0.8.0"),
22-
.package(url: "https://github.com/apple/swift-nio-zlib-support.git", from: "1.0.0")
21+
.package(url: "https://github.com/kylef/Commander.git", .upToNextMinor(from: "0.8.0")),
22+
.package(url: "https://github.com/apple/swift-nio-zlib-support.git", .upToNextMinor(from: "1.0.0")),
23+
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMinor(from: "1.11.0")),
24+
.package(url: "https://github.com/apple/swift-nio-nghttp2-support.git", .upToNextMinor(from: "1.0.0")),
25+
.package(url: "https://github.com/apple/swift-nio-http2.git", .revision("38b8235868e1e6277c420b73ac5cfdfa66382a85"))
2326
]
2427

2528
var cGRPCDependencies: [Target.Dependency] = []
@@ -35,11 +38,18 @@ let package = Package(
3538
name: "SwiftGRPC",
3639
products: [
3740
.library(name: "SwiftGRPC", targets: ["SwiftGRPC"]),
41+
.library(name: "SwiftGRPCNIO", targets: ["SwiftGRPCNIO"]),
3842
],
3943
dependencies: packageDependencies,
4044
targets: [
4145
.target(name: "SwiftGRPC",
4246
dependencies: ["CgRPC", "SwiftProtobuf"]),
47+
.target(name: "SwiftGRPCNIO",
48+
dependencies: [
49+
"NIOFoundationCompat",
50+
"NIOHTTP1",
51+
"NIOHTTP2",
52+
"SwiftProtobuf"]),
4353
.target(name: "CgRPC",
4454
dependencies: cGRPCDependencies),
4555
.target(name: "RootsEncoder"),
@@ -58,7 +68,8 @@ let package = Package(
5868
.target(name: "Simple",
5969
dependencies: ["SwiftGRPC", "Commander"],
6070
path: "Sources/Examples/Simple"),
61-
.testTarget(name: "SwiftGRPCTests", dependencies: ["SwiftGRPC"])
71+
.testTarget(name: "SwiftGRPCTests", dependencies: ["SwiftGRPC"]),
72+
.testTarget(name: "SwiftGRPCNIOTests", dependencies: ["SwiftGRPC", "SwiftGRPCNIO"])
6273
],
6374
cLanguageStandard: .gnu11,
6475
cxxLanguageStandard: .cxx11)

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,22 @@ testing with the following versions:
141141
- Swift 4.0
142142
- swift-protobuf 1.1.1
143143

144+
## `SwiftGRPCNIO` package
145+
146+
`SwiftGRPCNIO` is a clean-room implementation of the gRPC protocol on top of the [`SwiftNIO`](http://github.com/apple/swift-nio) library. This implementation is not yet production-ready as it lacks several things recommended for production use:
147+
148+
- Better test coverage
149+
- Full error handling
150+
- SSL support
151+
- Client support
152+
- Example projects
153+
- iOS support
154+
- Removal of the `libnghttp2` dependency from `SwiftNIOHTTP2`
155+
156+
However, if you are planning to implement a gRPC service based on `SwiftNIO` or the Vapor framework, you might find this package useful. In addition, once ready, this package should provide more predictable and reliable behavior in the future, combined with an improved API and better developer experience.
157+
158+
You may also want to have a look at [this presentation](https://docs.google.com/presentation/d/1Mnsaq4mkeagZSP4mK1k0vewZrJKynm_MCteRDyM3OX8/edit) for more details on the motivation for this package.
159+
144160
## License
145161

146162
grpc-swift is released under the same license as
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Foundation
2+
import SwiftProtobuf
3+
import NIO
4+
import NIOHTTP1
5+
6+
/// Provides a means for decoding incoming gRPC messages into protobuf objects.
7+
///
8+
/// Calls through to `processMessage` for individual messages it receives, which needs to be implemented by subclasses.
9+
public class BaseCallHandler<RequestMessage: Message, ResponseMessage: Message>: GRPCCallHandler {
10+
public func makeGRPCServerCodec() -> ChannelHandler { return GRPCServerCodec<RequestMessage, ResponseMessage>() }
11+
12+
/// Called whenever a message has been received.
13+
///
14+
/// Overridden by subclasses.
15+
public func processMessage(_ message: RequestMessage) {
16+
fatalError("needs to be overridden")
17+
}
18+
19+
/// Called when the client has half-closed the stream, indicating that they won't send any further data.
20+
///
21+
/// Overridden by subclasses if the "end-of-stream" event is relevant.
22+
public func endOfStreamReceived() { }
23+
}
24+
25+
extension BaseCallHandler: ChannelInboundHandler {
26+
public typealias InboundIn = GRPCServerRequestPart<RequestMessage>
27+
public typealias OutboundOut = GRPCServerResponsePart<ResponseMessage>
28+
29+
public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
30+
switch self.unwrapInboundIn(data) {
31+
case .head: preconditionFailure("should not have received headers")
32+
case .message(let message): processMessage(message)
33+
case .end: endOfStreamReceived()
34+
}
35+
}
36+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Foundation
2+
import SwiftProtobuf
3+
import NIO
4+
import NIOHTTP1
5+
6+
/// Handles bidirectional streaming calls. Forwards incoming messages and end-of-stream events to the observer block.
7+
///
8+
/// - The observer block is implemented by the framework user and calls `context.sendResponse` as needed.
9+
/// - To close the call and send the status, fulfill `context.statusPromise`.
10+
public class BidirectionalStreamingCallHandler<RequestMessage: Message, ResponseMessage: Message>: BaseCallHandler<RequestMessage, ResponseMessage> {
11+
public typealias EventObserver = (StreamEvent<RequestMessage>) -> Void
12+
private var eventObserver: EventLoopFuture<EventObserver>?
13+
14+
private var context: StreamingResponseCallContext<ResponseMessage>?
15+
16+
// We ask for a future of type `EventObserver` to allow the framework user to e.g. asynchronously authenticate a call.
17+
// If authentication fails, they can simply fail the observer future, which causes the call to be terminated.
18+
public init(channel: Channel, request: HTTPRequestHead, eventObserverFactory: (StreamingResponseCallContext<ResponseMessage>) -> EventLoopFuture<EventObserver>) {
19+
super.init()
20+
let context = StreamingResponseCallContextImpl<ResponseMessage>(channel: channel, request: request)
21+
self.context = context
22+
let eventObserver = eventObserverFactory(context)
23+
self.eventObserver = eventObserver
24+
// Terminate the call if no observer is provided.
25+
eventObserver.cascadeFailure(promise: context.statusPromise)
26+
context.statusPromise.futureResult.whenComplete {
27+
// When done, reset references to avoid retain cycles.
28+
self.eventObserver = nil
29+
self.context = nil
30+
}
31+
}
32+
33+
public override func processMessage(_ message: RequestMessage) {
34+
eventObserver?.whenSuccess { observer in
35+
observer(.message(message))
36+
}
37+
}
38+
39+
public override func endOfStreamReceived() {
40+
eventObserver?.whenSuccess { observer in
41+
observer(.end)
42+
}
43+
}
44+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
import SwiftProtobuf
3+
import NIO
4+
import NIOHTTP1
5+
6+
/// Handles client-streaming calls. Forwards incoming messages and end-of-stream events to the observer block.
7+
///
8+
/// - The observer block is implemented by the framework user and fulfills `context.responsePromise` when done.
9+
public class ClientStreamingCallHandler<RequestMessage: Message, ResponseMessage: Message>: BaseCallHandler<RequestMessage, ResponseMessage> {
10+
public typealias EventObserver = (StreamEvent<RequestMessage>) -> Void
11+
private var eventObserver: EventLoopFuture<EventObserver>?
12+
13+
private var context: UnaryResponseCallContext<ResponseMessage>?
14+
15+
// We ask for a future of type `EventObserver` to allow the framework user to e.g. asynchronously authenticate a call.
16+
// If authentication fails, they can simply fail the observer future, which causes the call to be terminated.
17+
public init(channel: Channel, request: HTTPRequestHead, eventObserverFactory: (UnaryResponseCallContext<ResponseMessage>) -> EventLoopFuture<EventObserver>) {
18+
super.init()
19+
let context = UnaryResponseCallContextImpl<ResponseMessage>(channel: channel, request: request)
20+
self.context = context
21+
let eventObserver = eventObserverFactory(context)
22+
self.eventObserver = eventObserver
23+
// Terminate the call if no observer is provided.
24+
eventObserver.cascadeFailure(promise: context.responsePromise)
25+
context.responsePromise.futureResult.whenComplete {
26+
// When done, reset references to avoid retain cycles.
27+
self.eventObserver = nil
28+
self.context = nil
29+
}
30+
}
31+
32+
public override func processMessage(_ message: RequestMessage) {
33+
eventObserver?.whenSuccess { observer in
34+
observer(.message(message))
35+
}
36+
}
37+
38+
public override func endOfStreamReceived() {
39+
eventObserver?.whenSuccess { observer in
40+
observer(.end)
41+
}
42+
}
43+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
import SwiftProtobuf
3+
import NIO
4+
import NIOHTTP1
5+
6+
/// Handles server-streaming calls. Calls the observer block with the request message.
7+
///
8+
/// - The observer block is implemented by the framework user and calls `context.sendResponse` as needed.
9+
/// - To close the call and send the status, complete the status future returned by the observer block.
10+
public class ServerStreamingCallHandler<RequestMessage: Message, ResponseMessage: Message>: BaseCallHandler<RequestMessage, ResponseMessage> {
11+
public typealias EventObserver = (RequestMessage) -> EventLoopFuture<GRPCStatus>
12+
private var eventObserver: EventObserver?
13+
14+
private var context: StreamingResponseCallContext<ResponseMessage>?
15+
16+
public init(channel: Channel, request: HTTPRequestHead, eventObserverFactory: (StreamingResponseCallContext<ResponseMessage>) -> EventObserver) {
17+
super.init()
18+
let context = StreamingResponseCallContextImpl<ResponseMessage>(channel: channel, request: request)
19+
self.context = context
20+
self.eventObserver = eventObserverFactory(context)
21+
context.statusPromise.futureResult.whenComplete {
22+
// When done, reset references to avoid retain cycles.
23+
self.eventObserver = nil
24+
self.context = nil
25+
}
26+
}
27+
28+
29+
public override func processMessage(_ message: RequestMessage) {
30+
guard let eventObserver = self.eventObserver,
31+
let context = self.context else {
32+
//! FIXME: Better handle this error?
33+
print("multiple messages received on unary call")
34+
return
35+
}
36+
37+
let resultFuture = eventObserver(message)
38+
resultFuture
39+
// Fulfill the status promise with whatever status the framework user has provided.
40+
.cascade(promise: context.statusPromise)
41+
self.eventObserver = nil
42+
}
43+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
import SwiftProtobuf
3+
import NIO
4+
import NIOHTTP1
5+
6+
/// Handles unary calls. Calls the observer block with the request message.
7+
///
8+
/// - The observer block is implemented by the framework user and returns a future containing the call result.
9+
/// - To return a response to the client, the framework user should complete that future
10+
/// (similar to e.g. serving regular HTTP requests in frameworks such as Vapor).
11+
public class UnaryCallHandler<RequestMessage: Message, ResponseMessage: Message>: BaseCallHandler<RequestMessage, ResponseMessage> {
12+
public typealias EventObserver = (RequestMessage) -> EventLoopFuture<ResponseMessage>
13+
private var eventObserver: EventObserver?
14+
15+
private var context: UnaryResponseCallContext<ResponseMessage>?
16+
17+
public init(channel: Channel, request: HTTPRequestHead, eventObserverFactory: (UnaryResponseCallContext<ResponseMessage>) -> EventObserver) {
18+
super.init()
19+
let context = UnaryResponseCallContextImpl<ResponseMessage>(channel: channel, request: request)
20+
self.context = context
21+
self.eventObserver = eventObserverFactory(context)
22+
context.responsePromise.futureResult.whenComplete {
23+
// When done, reset references to avoid retain cycles.
24+
self.eventObserver = nil
25+
self.context = nil
26+
}
27+
}
28+
29+
public override func processMessage(_ message: RequestMessage) {
30+
guard let eventObserver = self.eventObserver,
31+
let context = self.context else {
32+
//! FIXME: Better handle this error?
33+
print("multiple messages received on unary call")
34+
return
35+
}
36+
37+
let resultFuture = eventObserver(message)
38+
resultFuture
39+
// Fulfill the response promise with whatever response (or error) the framework user has provided.
40+
.cascade(promise: context.responsePromise)
41+
self.eventObserver = nil
42+
}
43+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Foundation
2+
import SwiftProtobuf
3+
import NIO
4+
import NIOHTTP1
5+
6+
/// Processes individual gRPC messages and stream-close events on a HTTP2 channel.
7+
public protocol GRPCCallHandler: ChannelHandler {
8+
func makeGRPCServerCodec() -> ChannelHandler
9+
}
10+
11+
/// Provides `GRPCCallHandler` objects for the methods on a particular service name.
12+
///
13+
/// Implemented by the generated code.
14+
public protocol CallHandlerProvider: class {
15+
/// The name of the service this object is providing methods for, including the package path.
16+
///
17+
/// - Example: "io.grpc.Echo.EchoService"
18+
var serviceName: String { get }
19+
20+
/// Determines, calls and returns the appropriate request handler (`GRPCCallHandler`), depending on the request's
21+
/// method. Returns nil for methods not handled by this service.
22+
func handleMethod(_ methodName: String, request: HTTPRequestHead, serverHandler: GRPCChannelHandler, channel: Channel) -> GRPCCallHandler?
23+
}
24+
25+
/// Listens on a newly-opened HTTP2 subchannel and yields to the sub-handler matching a call, if available.
26+
///
27+
/// Once the request headers are available, asks the `CallHandlerProvider` corresponding to the request's service name
28+
/// for an `GRPCCallHandler` object. That object is then forwarded the individual gRPC messages.
29+
public final class GRPCChannelHandler {
30+
private let servicesByName: [String: CallHandlerProvider]
31+
32+
public init(servicesByName: [String: CallHandlerProvider]) {
33+
self.servicesByName = servicesByName
34+
}
35+
}
36+
37+
extension GRPCChannelHandler: ChannelInboundHandler {
38+
public typealias InboundIn = RawGRPCServerRequestPart
39+
public typealias OutboundOut = RawGRPCServerResponsePart
40+
41+
public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
42+
let requestPart = self.unwrapInboundIn(data)
43+
switch requestPart {
44+
case .head(let requestHead):
45+
// URI format: "/package.Servicename/MethodName", resulting in the following components separated by a slash:
46+
// - uriComponents[0]: empty
47+
// - uriComponents[1]: service name (including the package name);
48+
// `CallHandlerProvider`s should provide the service name including the package name.
49+
// - uriComponents[2]: method name.
50+
let uriComponents = requestHead.uri.components(separatedBy: "/")
51+
guard uriComponents.count >= 3 && uriComponents[0].isEmpty,
52+
let providerForServiceName = servicesByName[uriComponents[1]],
53+
let callHandler = providerForServiceName.handleMethod(uriComponents[2], request: requestHead, serverHandler: self, channel: ctx.channel) else {
54+
ctx.writeAndFlush(self.wrapOutboundOut(.status(.unimplemented(method: requestHead.uri))), promise: nil)
55+
return
56+
}
57+
58+
var responseHeaders = HTTPHeaders()
59+
responseHeaders.add(name: "content-type", value: "application/grpc")
60+
ctx.write(self.wrapOutboundOut(.headers(responseHeaders)), promise: nil)
61+
62+
let codec = callHandler.makeGRPCServerCodec()
63+
ctx.pipeline.add(handler: codec, after: self)
64+
.then { ctx.pipeline.add(handler: callHandler, after: codec) }
65+
//! FIXME(lukasa): Fix the ordering of this with NIO 1.12 and replace with `remove(, promise:)`.
66+
.whenComplete { _ = ctx.pipeline.remove(handler: self) }
67+
68+
case .message, .end:
69+
preconditionFailure("received \(requestPart), should have been removed as a handler at this point")
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)