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

2.0 API proposal #45

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
6 changes: 4 additions & 2 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
--swiftversion 5.2
--swiftversion 5.7

# file options

Expand All @@ -13,8 +13,10 @@
--stripunusedargs closure-only
--wraparguments before-first

# Configure the placement of an extension's access control keyword.
--extensionacl on-declarations

# rules

--disable blankLinesAroundMark
--disable wrapMultilineStatementBraces

13 changes: 7 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.6
// swift-tools-version:5.7

//===----------------------------------------------------------------------===//
//
Expand All @@ -18,20 +18,21 @@ import PackageDescription

let package = Package(
name: "swift-service-discovery",
platforms: [
.macOS(.v13),
],
products: [
.library(name: "ServiceDiscovery", targets: ["ServiceDiscovery"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-atomics", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-log", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.target(name: "ServiceDiscovery", dependencies: [
.target(name: "ServiceDiscovery", dependencies: []),
.testTarget(name: "ServiceDiscoveryTests", dependencies: [
"ServiceDiscovery",
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "Logging", package: "swift-log"),
]),

.testTarget(name: "ServiceDiscoveryTests", dependencies: ["ServiceDiscovery"]),
]
)
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ for try await instances in serviceDiscovery.subscribe(to: service) {

Underlying the async `subscribe` API is an `AsyncSequence`. To end the subscription, simply break out of the `for`-loop.

Note the `AsyncSequence` is of a `Result` type, wrapping either the instances discovered, or a discovery error if such occurred.
A client should decide how to best handle errors in this case, e.g. terminate the subscription or continue and handle the errors.

### Combinators

SwiftServiceDiscovery includes combinators for common requirements such as transforming and filtering instances. For example:
Expand Down
125 changes: 25 additions & 100 deletions Sources/ServiceDiscovery/Docs.docc/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ``ServiceDiscovery``

A Service Discovery API for Swift.

## Overview

Service discovery is how services locate one another within a distributed system. This API library is designed to establish a standard that can be implemented by various service discovery backends such as DNS-based, key-value store like Zookeeper, etc. In other words, this library defines the API only, similar to [SwiftLog](https://github.com/apple/swift-log) and [SwiftMetrics](https://github.com/apple/swift-metrics); actual functionalities are provided by backend implementations.
Expand All @@ -10,10 +10,7 @@ Service discovery is how services locate one another within a distributed system

If you have a server-side Swift application and would like to locate other services within the same system for making HTTP requests or RPCs, then ServiceDiscovery is the right library for the job. Below you will find all you need to know to get started.

### Concepts

- **Service Identity**: Each service must have a unique identity. `Service` denotes the identity type used in a backend implementation.
- **Service Instance**: A service may have zero or more instances, each of which has an associated location (typically host-port). `Instance` denotes the service instance type used in a backend implementation.
A service may have zero or more instances, each of which has an associated location (for example host-port). `Instance` denotes the service instance type used in a backend implementation.

## Selecting a service discovery backend implementation (applications only)

Expand All @@ -39,68 +36,24 @@ As the API has just launched, not many implementations exist yet. If you are int

### Obtaining a service's instances

To fetch the current list of instances (where `result` is `Result<[Instance], Error>`):

```swift
serviceDiscovery.lookup(service) { result in
...
}
```

To fetch the current list of instances (where `result` is `Result<[Instance], Error>`) **AND** subscribe to future changes:

```swift
let cancellationToken = serviceDiscovery.subscribe(
to: service,
onNext: { result in
// This closure gets invoked once at the beginning and subsequently each time a change occurs
...
},
onComplete: { reason in
// This closure gets invoked when the subscription completes
...
}
)

...

// Cancel the `subscribe` request
cancellationToken.cancel()
```

`subscribe` returns a ``CancellationToken`` that you can use to cancel the subscription later on. `onComplete` is a closure that
gets invoked when the subscription ends (e.g., when the service discovery instance shuts down) or gets cancelled through the
``CancellationToken``. ``CompletionReason`` can be used to distinguish what leads to the completion.

#### Async APIs

Async APIs are available for Swift 5.5 and above.

To fetch the current list of instances:

```swift
let instances: [Instance] = try await serviceDiscovery.lookup(service)
let instances: [Instance] = try await serviceDiscovery.lookup()
```

To fetch the current list of instances **AND** subscribe to future changes:

```swift
for try await instances in serviceDiscovery.subscribe(to: service) {
for try await instances in serviceDiscovery.subscribe() {
// do something with this snapshot of instances
}
```

Underlying the async `subscribe` API is an `AsyncSequence`. To end the subscription, simply break out of the `for`-loop.

### Combinators
Underlying the async `subscribe` API is an `AsyncSequence`. To end the subscription, simply break out of the `for`-loop.

ServiceDiscovery includes combinators for common requirements such as transforming and filtering instances. For example:

```swift
// Only include instances running on port 8080
let serviceDiscovery = InMemoryServiceDiscovery(configuration: configuration)
.filterInstance { [8080].contains($0.port) }
```
Note the `AsyncSequence` is of a `Result` type, wrapping either the instances discovered, or a discovery error if such occurred.
A client should decide how to best handle errors in this case, e.g. terminate the subscription or continue and handle the errors.

## Implementing a service discovery backend

Expand All @@ -111,7 +64,7 @@ let serviceDiscovery = InMemoryServiceDiscovery(configuration: configuration)
To add a dependency on the API package, you need to declare it in your `Package.swift`:

```swift
.package(url: "https://github.com/apple/swift-service-discovery.git", from: "0.1.0"),
.package(url: "https://github.com/apple/swift-service-discovery.git", from: "2.0.0"),
```

and to your library target, add `ServiceDiscovery` to your dependencies:
Expand All @@ -125,69 +78,41 @@ and to your library target, add `ServiceDiscovery` to your dependencies:
),
```

To become a compatible service discovery backend that all ServiceDiscovery consumers can use, you need to implement a type that conforms to the ``ServiceDiscovery/ServiceDiscovery`` protocol provided by ServiceDiscovery. It includes two methods, ``ServiceDiscovery/lookup(_:deadline:callback:)`` and ``ServiceDiscovery/subscribe(to:onNext:onComplete:)``.
To become a compatible service discovery backend that all ServiceDiscovery consumers can use, you need to implement a type that conforms to the ``ServiceDiscovery/ServiceDiscovery`` protocol provided by ServiceDiscovery. It includes two methods, ``ServiceDiscovery/lookup`` and ``ServiceDiscovery/subscribe``.

#### lookup

```swift
/// Performs a lookup for the given service's instances. The result will be sent to `callback`.
///
/// `defaultLookupTimeout` will be used to compute `deadline` in case one is not specified.
/// Performs async lookup for the given service's instances.
///
/// - Parameters:
/// - service: The service to lookup
/// - deadline: Lookup is considered to have timed out if it does not complete by this time
/// - callback: The closure to receive lookup result
func lookup(_ service: Service, deadline: DispatchTime?, callback: @escaping (Result<[Instance], Error>) -> Void)
/// - Returns: A listing of service discovery instances.
/// - throws when failing to lookup instances
func lookup() async throws -> [Instance]
```

`lookup` fetches the current list of instances for the given `service` and sends it to `callback`. If the service is unknown (e.g., registration is required but it has not been done for the service), then the result should be a `LookupError.unknownService` failure.

The backend implementation should impose a deadline on when the operation will complete. `deadline` should be respected if given, otherwise one should be computed using `defaultLookupTimeout`.
`lookup` fetches the current list of instances asynchronously.

#### subscribe

```swift
/// Subscribes to receive a service's instances whenever they change.
/// Subscribes to receive service discovery change notification whenever service discovery instances change.
///
/// The service's current list of instances will be sent to `nextResultHandler` when this method is first called. Subsequently,
/// `nextResultHandler` will only be invoked when the `service`'s instances change.
///
/// ### Threading
///
/// `nextResultHandler` and `completionHandler` may be invoked on arbitrary threads, as determined by implementation.
///
/// - Parameters:
/// - service: The service to subscribe to
/// - nextResultHandler: The closure to receive update result
/// - completionHandler: The closure to invoke when the subscription completes (e.g., when the `ServiceDiscovery` instance exits, etc.),
/// including cancellation requested through `CancellationToken`.
///
/// - Returns: A `CancellationToken` instance that can be used to cancel the subscription in the future.
func subscribe(to service: Service, onNext nextResultHandler: @escaping (Result<[Instance], Error>) -> Void, onComplete completionHandler: @escaping (CompletionReason) -> Void) -> CancellationToken
/// - Returns a ``ServiceDiscoverySubscription`` which produces an `AsyncSequence` of changes in the service discovery instances.
/// - throws when failing to establish subscription
func subscribe() async throws -> DiscoverySequence
```

`subscribe` "pushes" service instances to the `nextResultHandler`. The backend implementation is expected to call `nextResultHandler`:
`subscribe` returns an ``AsyncSequence`` that yields a Result type containing array of instances or error information.
The set of instances is the complete set of known instances at yield time. The backend should yield:

- When `subscribe` is first invoked, the caller should receive the current list of instances for the given service. This is essentially the `lookup` result.
- Whenever the given service's list of instances changes. The backend implementation has full control over how and when its service records get updated, but it must notify `nextResultHandler` when the instances list becomes different from the previous result.

A new ``CancellationToken`` must be created for each `subscribe` request. If the cancellation token's `isCancelled` is `true`, the subscription has been cancelled and the backend implementation should cease calling the corresponding `nextResultHandler`.
- Whenever the given service's list of instances changes. The backend implementation has full control over how and when its service records get updated, but it must yield when the instances list becomes different from the previous result.

The backend implementation must also notify via `completionHandler` when the subscription ends for any reason (e.g., the service discovery instance is shutting down or cancellation is requested through ``CancellationToken``), so that the subscriber can submit another `subscribe` request if needed.

## Topics

### Service Discovery API

- ``ServiceDiscovery/lookup(_:deadline:callback:)``
- ``ServiceDiscovery/subscribe(to:onNext:onComplete:)``
- ``ServiceDiscovery/lookup(_:deadline:)``
- ``ServiceDiscovery/subscribe(to:)``

### Combinators

- ``ServiceDiscovery/mapInstance(_:)``
- ``ServiceDiscovery/mapService(serviceType:_:)``
- ``ServiceDiscovery/filterInstance(_:)``
- ``ServiceDiscovery/lookup()``
- ``ServiceDiscovery/subscribe()``

59 changes: 0 additions & 59 deletions Sources/ServiceDiscovery/FilterInstanceServiceDiscovery.swift

This file was deleted.

28 changes: 0 additions & 28 deletions Sources/ServiceDiscovery/HostPort.swift

This file was deleted.

Loading