Skip to content

Setup for establishing connection(s) in a multi-service client application using v2 #2211

Open
@majelbstoat

Description

@majelbstoat

What are you trying to achieve?

I am building an iOS app for a complex product that speaks to multiple logical GRPC services, all served from the same server. So, for example, I can call UserService.GetUser() and ProgramService.GetProgram() and they will both go to the same (load-balanced, horizontally scaled) host.

Every example I've seen for usage of Swift GRPC 2 is for a one-off single call to a single service, looking like:

try await withGRPCClient(
      transport: .http2NIOPosix(
        target: .dns(host: hostname, port: 443),
        transportSecurity: .tls
      )
    ) { client in
      let greeter = GreetingService.Client(wrapping: client)
      let greeting = try await greeter.sayHello(.with { $0.name = "swift.org" })
      print(greeting.message)
    }

I'd like to understand best practices for usage with multiple services, within an actual app that needs a connection to function. A couple of options spring to mind:

  1. Create a new GRPCClient for each service client.
  2. Create a single GRPCClient and then re-use that for each service client.
  3. Create a single GRPCClient that has some kind of pool behind it (of say, 4 connections), then re-use that single exposed GRPCClient for each service client.

Option 2/3 is most similar to what I had for Swift GRPC 1, and my preferred approach if possible. (There are already a dozen services, and there will be more in the future.)

I would also like to understand if there's a blocking/synchronous approach to ensuring a connection is established.

What have you tried so far?

Previously, I had a single synchronously established GRPCChannel, exposed as a static computed variable:

class GRPCManager {
    private static var host: String {
        Bundle.main.infoDictionary?["APP_CORE_HOST"] as! String
    }

    static var connection: GRPCChannel {
        guard host != "" else {
            fatalError("GRPC Host is not set")
        }
        return connect()
    }

    private static func connect() -> GRPCChannel {
        let eventLoopGroup = NIOTSEventLoopGroup(loopCount: 1, defaultQoS: .default)
        return ClientConnection.usingPlatformAppropriateTLS(for: eventLoopGroup)
            .connect(host: host, port: 443)
    }
}

and then each services had an init like:

 init() {
        client = App_UserServiceAsyncClient(channel: GRPCManager.connection)
    }
 init() {
        client = App_ProgramServiceAsyncClient(channel: GRPCManager.connection)
    }

This lazily established the connection the first time it was needed, requiring no explicit setup in the @main App. It seemed to work well. I appreciate that client.runConnections() is now asynchronous, but I need to know when it's reliably available/block until the connection is established, because the app is unusable without it.

I would like to achieve this while keeping the service clients themselves isolated and independent of each other, in particular not requiring a specific order of service usage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status/triageCollecting information required to triage the issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions