Coder Social home page Coder Social logo

vyshane / grpc-swift-combine Goto Github PK

View Code? Open in Web Editor NEW
74.0 5.0 12.0 300 KB

Combine framework integration for Swift gRPC

Home Page: https://vyshane.com/2019/07/28/swift-grpc-and-combine-better-together/

License: Apache License 2.0

Swift 97.54% Makefile 0.49% Ruby 1.97%
grpc swift-grpc combine-framework swift protobuf

grpc-swift-combine's Introduction

CombineGRPC

CombineGRPC is a library that provides Combine framework integration for Swift gRPC.

CombineGRPC provides two flavours of functionality, call and handle. Use call to make gRPC calls on the client side, and handle to handle incoming requests on the server side. The library provides versions of call and handle for all RPC styles. Here are the input and output types for each.

RPC Style Input and Output Types
Unary Request -> AnyPublisher<Response, RPCError>
Server streaming Request -> AnyPublisher<Response, RPCError>
Client streaming AnyPublisher<Request, Error> -> AnyPublisher<Response, RPCError>
Bidirectional streaming AnyPublisher<Request, Error> -> AnyPublisher<Response, RPCError>

When you make a unary call, you provide a request message, and get back a response publisher. The response publisher will either publish a single response, or fail with an RPCError error. Similarly, if you are handling a unary RPC call, you provide a handler that takes a request parameter and returns an AnyPublisher<Response, RPCError>.

You can follow the same intuition to understand the types for the other RPC styles. The only difference is that publishers for the streaming RPCs may publish zero or more messages instead of the single response message that is expected from the unary response publisher.

Quick Tour

Let's see a quick example. Consider the following protobuf definition for a simple echo service. The service defines one bidirectional RPC. You send it a stream of messages and it echoes the messages back to you.

syntax = "proto3";

service EchoService {
  rpc SayItBack (stream EchoRequest) returns (stream EchoResponse);
}

message EchoRequest {
  string message = 1;
}

message EchoResponse {
  string message = 1;
}

Server Side

To implement the server, you provide a handler function that takes an input stream AnyPublisher<EchoRequest, Error> and returns an output stream AnyPublisher<EchoResponse, RPCError>.

import Foundation
import Combine
import CombineGRPC
import GRPC
import NIO

class EchoServiceProvider: EchoProvider {
  
  // Simple bidirectional RPC that echoes back each request message
  func sayItBack(context: StreamingResponseCallContext<EchoResponse>) -> EventLoopFuture<(StreamEvent<EchoRequest>) -> Void> {
    CombineGRPC.handle(context) { requests in
      requests
        .map { req in
          EchoResponse.with { $0.message = req.message }
        }
        .setFailureType(to: RPCError.self)
        .eraseToAnyPublisher()
    }
  }
}

Start the server. This is the same process as with Swift gRPC.

let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
  try! eventLoopGroup.syncShutdownGracefully()
}

// Start the gRPC server and wait until it shuts down.
_ = try Server
  .insecure(group: eventLoopGroup)
  .withServiceProviders([EchoServiceProvider()])
  .bind(host: "localhost", port: 8080)
  .flatMap { $0.onClose }
  .wait()

Client Side

Now let's setup our client. Again, it's the same process that you would go through when using Swift gRPC.

let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let channel = ClientConnection
  .insecure(group: eventLoopGroup)
  .connect(host: "localhost", port: 8080)
let echoClient = EchoServiceNIOClient(channel: channel)

To call the service, create a GRPCExecutor and use its call method. You provide it with a stream of requests AnyPublisher<EchoRequest, Error> and you get back a stream AnyPublisher<EchoResponse, RPCError> of responses from the server.

let requests = repeatElement(EchoRequest.with { $0.message = "hello"}, count: 10)
let requestStream: AnyPublisher<EchoRequest, Error> =
  Publishers.Sequence(sequence: requests).eraseToAnyPublisher()
let grpc = GRPCExecutor()

grpc.call(echoClient.sayItBack)(requestStream)
  .filter { $0.message == "hello" }
  .count()
  .sink(receiveValue: { count in
    assert(count == 10)
  })

That's it! You have set up bidirectional streaming between a server and client. The method sayItBack of EchoServiceNIOClient is generated by Swift gRPC. Notice that call is curried. You can preselect RPC calls using partial application:

let sayItBack = grpc.call(echoClient.sayItBack)

sayItBack(requestStream).map { response in
  // ...
}

Configuring RPC Calls

The GRPCExecutor allows you to configure CallOptions for your RPC calls. You can provide the GRPCExecutor's initializer with a stream AnyPublisher<CallOptions, Never>, and the latest CallOptions value will be used when making calls.

let timeoutOptions = CallOptions(timeout: try! .seconds(5))
let grpc = GRPCExecutor(callOptions: Just(timeoutOptions).eraseToAnyPublisher())

Retry Policy

You can also configure GRPCExecutor to automatically retry failed calls by specifying a RetryPolicy. In the following example, we retry calls that fail with status .unauthenticated. We use CallOptions to add a Bearer token to the authorization header, and then retry the call.

// Default CallOptions with no authentication
let callOptions = CurrentValueSubject<CallOptions, Never>(CallOptions())

let grpc = GRPCExecutor(
  callOptions: callOptions.eraseToAnyPublisher(),
  retry: .failedCall(
    upTo: 1,
    when: { error in
      error.status.code == .unauthenticated
    },
    delayUntilNext: { retryCount, error in  // Useful for implementing exponential backoff
      // Retry the call with authentication
      callOptions.send(CallOptions(customMetadata: HTTPHeaders([("authorization", "Bearer xxx")])))
      return Just(()).eraseToAnyPublisher()
    },
    didGiveUp: {
      print("Authenticated call failed.")
    }
  )
)

grpc.call(client.authenticatedRpc)(request)
  .map { response in
    // ...
  }

You can imagine doing something along those lines to seamlessly retry calls when an ID token expires. The back-end service replies with status .unauthenticated, you obtain a new ID token using your refresh token, and the call is retried.

More Examples

Check out the CombineGRPC tests for examples of all the different RPC calls and handler implementations. You can find the matching protobuf here.

Logistics

Generating Swift Code from Protobuf

To generate Swift code from your .proto files, you'll need to first install the protoc Protocol Buffer compiler.

brew install protobuf swift-protobuf grpc-swift

Now you are ready to generate Swift code from protobuf interface definition files.

Let's generate the message types, gRPC server and gRPC client for Swift.

protoc example_service.proto --swift_out=Generated/
protoc example_service.proto --grpc-swift_out=Generated/

You'll see that protoc has created two source files for us.

ls Generated/
example_service.grpc.swift
example_service.pb.swift

Adding CombineGRPC to Your Project

You can easily add CombineGRPC to your project using Swift Package Manager. To add the package dependency to your Package.swift:

dependencies: [
  .package(url: "https://github.com/vyshane/grpc-swift-combine.git", from: "1.1.0"),
],

Compatibility

Since this library integrates with Combine, it only works on platforms that support Combine. This currently means the following minimum versions:

Platform Minimum Supported Version
macOS 10.15 (Catalina)
iOS & iPadOS 13
tvOS 13
watchOS 6

Feature Status

RPC Client Calls

  • Unary
  • Client streaming
  • Server streaming
  • Bidirectional streaming
  • Retry policy for automatic client call retries

Server Side Handlers

  • Unary
  • Client streaming
  • Server streaming
  • Bidirectional streaming

End-to-end Tests

  • Unary
  • Client streaming
  • Server streaming
  • Bidirectional streaming

Contributing

Generate Swift source for the protobuf that is used in tests:

make protobuf

You can then open Package.swift in Xcode, build and run the tests.

grpc-swift-combine's People

Contributors

abrampers avatar j-j-m avatar jcmuller avatar kdubb avatar sip-technocidal avatar taoshotaro avatar vyshane avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

grpc-swift-combine's Issues

Cocoapods 1.0.12 version pod error

POD: pod 'CombineGRPC', '~> 1.0.12'

Hi, I am trying to update to version 1.0.12 using cocoapods but I get the following error:

CocoaPods could not find compatible versions for pod "CombineGRPC":
In Podfile:
CombineGRPC (~> 1.0.12)

None of your spec sources contain a spec satisfying the dependency: CombineGRPC (~> 1.0.12).

You have either:

  • out-of-date source repos which you can update with pod repo update or with pod install --repo-update.
  • mistyped the name or version.
  • not added the source repo that hosts the Podspec to your Podfile.

I am doing something wrong ? or this is a bug? Thank you!

SPM plugin to generate RemoteDataSource

Hi,

nice repo, I have an idea - not sure if its gonna work, to add SPM plugin to generate whole RemoteDataSoucre from service using CombineGrpc. Would be a great tool to generate entirely whole server side code ๐Ÿ˜ธ

Like:

payService.proto

service PayService {
    rpc pay(Request) returns (Response) {}
}

PayRemoteDataSource.swift

public protocol PayRemoteDataSource {
    //... service methods with GRPC generated structs
   func pay(Request) -> AnyPublisher<Response, RPCError>
}

PayRemoteDataSourceImpl.swift

struct PayRemoteDataSourceImpl: PayRemoteDataSource  {
    //... real implementation using grpcExecutor 
}

I know that is possible but I have no idea how to build this kind of plugin

Expose trailingMetadata

Support for GRPCStatusAndMetadata - https://github.com/grpc/grpc-swift/blob/main/Sources/GRPC/GRPCStatusAndMetadata.swift would be awesome.

Currently on GRPCStatus is exposed - https://github.com/vyshane/grpc-swift-combine/blob/master/Sources/CombineGRPC/Client/GRPCExecutor.swift#L93

discussion - GRPCError should implement error

Based on the great work done in #12 - I think GRPCError should implement Error rather than GRPCStatus implementing it.

Currently you have to do example Future<SomeResponse, GRPCStatus> -- but with GRPCError containing other stuff that might be needed downstream I think it should implement Error.

Most Combine publishers require an Error as part of the return.

Thoughts?

Support SwiftGRPC 1.0.0.alpha-10+

I was trying to use SwiftGRPC and CombineGRPC on the weekend and noticed some source incompatibilities between your project and version 1.0.0.alpha-10 and above of SwiftGRPC.

Any plan to update in the near future?

bitcode error - 0.21.0

Finally got around upgrading using Pods from 0.17 to 0.21 and getting this error when trying to build;

CleanShot 2020-12-03 at 22 52 40@2x

Recieve value of each request in stream

Is there a way to get the completion event of each request in the request array in the case of a client stream request?
I get only one value when all queries in the array are completed

  func create() -> AnyPublisher<UInt64, Error> {
      let requests = requests
      let requestStream: AnyPublisher<Node_UploadPartsReq, Error> = Publishers.Sequence(sequence: requests)
          .eraseToAnyPublisher()
      let grpc = GRPCExecutor()
      return grpc.call(client.uploadParts)(requestStream)
          .map({ _ in
              return UploadPartsResponse(nodeId: connection.nodeId) })
          .replaceEmpty(with: UploadPartsResponse(nodeId: connection.nodeId))
          .mapError({ error in
              Log.error("\(error)")
              return AppError.convertFrom(error)
          })
          .map({ response in
              return response.nodeId
          })
          .eraseToAnyPublisher()
  }

Multiple retry policies

Hi, how can I support multiple retry policies? I'd like to handle different status codes accordingly but the library seems to only support one retry policy if I'm reading it correctly.

How do I finish a bidirectional stream?

I have a Bidirectional stream and my server side waits for the client side to finish. When I cancel the cancelable call.sendEnd() is not triggered although I think it should be. My server side waits forever for the stream to finish. I can dispose the client which cancels all pending streams but I think cancelling or ending the stream separately should be the correct behaviour.

I will try to implement this, but I am very new to swift and combine. If this is already a feature please point me to it.

Greetings krjw

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.