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

Add the router and service protocol #1717

Merged
merged 2 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions Sources/GRPCCore/Call/Server/RPCRouter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2023, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// Stores and provides handlers for RPCs.
///
/// The router stores a handler for each RPC it knows about. Each handler encapsulate the business
/// logic for the RPC which is typically implemented by service owners. To register a handler you
/// can call ``registerHandler(forMethod:deserializer:serializer:handler:)``. You can check whether
/// the router has a handler for a method with ``hasHandler(forMethod:)`` or get a list of all
/// methods with handlers registered by calling ``methods``. You can also remove the handler for a
/// given method by calling ``removeHandler(forMethod:)``.
///
/// In most cases you won't need to interact with the router directly. Instead you should register
/// your services with ``Server/Services-swift.struct/register(_:)`` which will in turn register
/// each method with the router.
///
/// You may wish to not serve all methods from your service in which case you can either:
///
/// 1. Remove individual methods by calling ``removeHandler(forMethod:)``, or
/// 2. Implement ``RegistrableRPCService/registerMethods(with:)`` to register only the methods you
/// want to be served.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public struct RPCRouter: Sendable {
@usableFromInline
struct RPCHandler: Sendable {
@usableFromInline
let _fn:
@Sendable (
_ stream: RPCStream<RPCAsyncSequence<RPCRequestPart>, RPCWriter<RPCResponsePart>.Closable>,
FranzBusch marked this conversation as resolved.
Show resolved Hide resolved
_ interceptors: [any ServerInterceptor]
) async -> Void

@inlinable
init<Input, Output>(
method: MethodDescriptor,
deserializer: some MessageDeserializer<Input>,
serializer: some MessageSerializer<Output>,
handler: @Sendable @escaping (
_ request: ServerRequest.Stream<Input>
) async throws -> ServerResponse.Stream<Output>
) {
self._fn = { stream, interceptors in
await ServerRPCExecutor.execute(
stream: stream,
deserializer: deserializer,
serializer: serializer,
interceptors: interceptors,
handler: handler
)
}
}

@inlinable
func handle(
stream: RPCStream<RPCAsyncSequence<RPCRequestPart>, RPCWriter<RPCResponsePart>.Closable>,
interceptors: [any ServerInterceptor]
) async {
await self._fn(stream, interceptors)
}
}

@usableFromInline
private(set) var handlers: [MethodDescriptor: RPCHandler]

/// Creates a new router with no methods registered.
public init() {
self.handlers = [:]
}

/// Returns all descriptors known to the router in an undefined order.
public var methods: [MethodDescriptor] {
Array(self.handlers.keys)
}

/// Returns the number of methods registered with the router.
public var count: Int {
self.handlers.count
}

/// Returns whether a handler exists for a given method.
///
/// - Parameter descriptor: A descriptor of the method.
/// - Returns: Whether a handler exists for the method.
public func hasHandler(forMethod descriptor: MethodDescriptor) -> Bool {
Comment on lines +84 to +97
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do those things have to be public?

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 there's value in having methods (e.g. logging what methods your server serves). You can obviously just query methods instead of using this function but it's then O(n) rather than O(1) which is a bit sad and the main reason to add this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't this be solved if methods was a Set instead of an Array?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, while the lookup would be O(1) creating the set would still be O(n).

return self.handlers.keys.contains(descriptor)
}

/// Registers a handler with the router.
///
/// - Note: if a handler already exists for a given method then it will be replaced.
///
/// - Parameters:
/// - descriptor: A descriptor for the method to register a handler for.
/// - deserializer: A deserializer to deserialize input messages received from the client.
/// - serializer: A serializer to serialize output messages to send to the client.
/// - handler: The function which handles the request and returns a response.
@inlinable
public mutating func registerHandler<Input: Sendable, Output: Sendable>(
forMethod descriptor: MethodDescriptor,
deserializer: some MessageDeserializer<Input>,
serializer: some MessageSerializer<Output>,
handler: @Sendable @escaping (
_ request: ServerRequest.Stream<Input>
) async throws -> ServerResponse.Stream<Output>
) {
self.handlers[descriptor] = RPCHandler(
method: descriptor,
deserializer: deserializer,
serializer: serializer,
handler: handler
)
}

/// Removes any handler registered for the specified method.
///
/// - Parameter descriptor: A descriptor of the method to remove a handler for.
/// - Returns: Whether a handler was removed.
@discardableResult
public mutating func removeHandler(forMethod descriptor: MethodDescriptor) -> Bool {
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. Does this need to be public?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you don't want to offer all methods in a service then this is one way to remove the ones you don't want (the other being to reimplement the service protocol).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see. I am wondering if we should expose this publicly right away. Do we have concrete use-cases in mind where want to do this or did we get a feature request for this before?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One concrete use case is the interoperability tests. One test uses the generated client to call a method which isn't implemented on the server. At the moment we achieve this by patching the generated code.

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 also think having these methods is very low burden: they're unlikely to change and they still make sense if we change the internals of the router.

return self.handlers.removeValue(forKey: descriptor) != nil
}
}
31 changes: 31 additions & 0 deletions Sources/GRPCCore/Call/Server/RegistrableRPCService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2023, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// An RPC service which can register its methods with an ``RPCRouter``.
///
/// You typically won't have to implement this protocol yourself as the generated service code
/// provides conformance for your generated service type. However, if you need to customise which
/// methods your service offers or how the methods are registered then you can override the
/// generated conformance by implementing ``registerMethods(with:)`` manually by calling
/// ``RPCRouter/registerHandler(forMethod:deserializer:serializer:handler:)`` for each method
/// you want to register with the router.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public protocol RegistrableRPCService: Sendable {
/// Registers methods to server with the provided ``RPCRouter``.
///
/// - Parameter router: The router to register methods with.
func registerMethods(with router: inout RPCRouter)
}
62 changes: 62 additions & 0 deletions Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2023, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import GRPCCore
import XCTest

@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
final class RPCRouterTests: XCTestCase {
func testEmptyRouter() async throws {
var router = RPCRouter()
XCTAssertEqual(router.count, 0)
XCTAssertEqual(router.methods, [])
XCTAssertFalse(router.hasHandler(forMethod: MethodDescriptor(service: "foo", method: "bar")))
XCTAssertFalse(router.removeHandler(forMethod: MethodDescriptor(service: "foo", method: "bar")))
}

func testRegisterMethod() async throws {
var router = RPCRouter()
let method = MethodDescriptor(service: "foo", method: "bar")
router.registerHandler(
forMethod: method,
deserializer: IdentityDeserializer(),
serializer: IdentitySerializer()
) { _ in
throw RPCError(code: .failedPrecondition, message: "Shouldn't be called")
}

XCTAssertEqual(router.count, 1)
XCTAssertEqual(router.methods, [method])
XCTAssertTrue(router.hasHandler(forMethod: method))
}

func testRemoveMethod() async throws {
var router = RPCRouter()
let method = MethodDescriptor(service: "foo", method: "bar")
router.registerHandler(
forMethod: method,
deserializer: IdentityDeserializer(),
serializer: IdentitySerializer()
) { _ in
throw RPCError(code: .failedPrecondition, message: "Shouldn't be called")
}

XCTAssertTrue(router.removeHandler(forMethod: method))
XCTAssertFalse(router.hasHandler(forMethod: method))
XCTAssertEqual(router.count, 0)
XCTAssertEqual(router.methods, [])
}
}