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
Open

2.0 API proposal #45

wants to merge 15 commits into from

Conversation

tomerd
Copy link
Member

@tomerd tomerd commented Jun 1, 2023

Proposal for 2.0 version of the API, based on Swift concurrency

@tomerd tomerd force-pushed the 2.0 branch 2 times, most recently from 6a29135 to 41e70d0 Compare June 16, 2023 00:34
@tomerd
Copy link
Member Author

tomerd commented Jul 12, 2023

@yim-lee @ktoso updated subscribe to return [Instance]. ptal

@tomerd
Copy link
Member Author

tomerd commented Jul 12, 2023

cc @fabianfett

@tomerd tomerd requested review from ktoso and yim-lee July 12, 2023 19:58
@tomerd tomerd force-pushed the 2.0 branch 2 times, most recently from 588a098 to d012743 Compare July 12, 2023 20:06
@tomerd tomerd changed the title [wip] 2.0 API suggestion [wip] 2.0 API proposal Jul 12, 2023
@fabianfett
Copy link
Member

fabianfett commented Jul 12, 2023

I think my biggest feature request for a 2.0 would be to drop the associated Service type on ServiceDiscovery. The Service types makes using ServiceDiscovery more complicated then it needs to be.

Currently proposed protocol:

public protocol ServiceDiscovery {
    associatedtype Service
    associatedtype Instance

    func lookup(_ service: Service, deadline: ContinuousClock.Instant?) async throws -> [Instance]
    func subscribe(_ service: Service) async throws -> any ServiceDiscoveryInstancesSequence<Instance>
}

This means a type that want's to use ServiceDiscovery needs to be generic over three types. Let's assume we want to build a RedisClient that uses ServiceDiscovery:

protocol RedisInstance: Hashable {}

final class RedisClient<Discovery: ServiceDiscovery, Service: Hashable, Instance: RedisInstance > {
  init(
    discovery: Discovery,
    service: Service
  )

  func run() {
    for try await instances in self.discovery.subscribe(self.service) {
      // do something with the results
    }
  }
}

Personally I think that users should inject the Service into their ServiceDiscovery object on creation. For most usecases you are only interested in one service. And for instances where you are interested in more Services you are always free to use multiple ServiceDiscovery instances.

Changed protocol:

public protocol ServiceDiscovery {
    associatedtype Instance
    associatedtype Subscription: AsyncSequence where Subscription.Element == [Instance]

    func lookup() async throws -> [Instance]
    func subscribe() async throws -> Subscription
}

Other changes:

  1. I removed the deadline form the lookup call. I think users should enforce deadlines in their code and express a hit deadline through cancelling the task. Libraries should not implement this over and over again. We might want to push for a stdlib addition for this eventually. withDeadline {}
  2. I changed the protocol to be fully generic.

The really cool thing now is the following: Library authors might not need to depend on ServiceDiscovery at all. They can express their requirement completely through AsyncSequences:

protocol RedisInstance {}

final class RedisClient<DiscoverySubscription: AsyncSequence> where DiscoverySubscription.Element: Sequence<some RedisInstance> {
  init(subscription: DiscoverySubscription)

  func run() {
    for try await instances in self.subscription {
      // do something with the results
    }
  }
}

@tomerd wdyt?

@fabianfett
Copy link
Member

HostPort should live in Tests only.

@tomerd
Copy link
Member Author

tomerd commented Jul 12, 2023

@tomerd wdyt?

I like the idea of clients not needing to depend on it explicitly -- not that we have AsyncSequence the API is really light.

@yim-lee wdyt?

@yim-lee
Copy link
Member

yim-lee commented Jul 12, 2023

@fabianfett @tomerd +1

@tomerd
Copy link
Member Author

tomerd commented Jul 13, 2023

the one thing I am less sure about is removing Service since it makes for a less flexible API for more complex use cases. @yim-lee what use cases did you have in mind for it originally?

@yim-lee
Copy link
Member

yim-lee commented Jul 13, 2023

For most usecases you are only interested in one service.

This is true.

Personally I think that users should inject the Service into their ServiceDiscovery object on creation....And for instances where you are interested in more Services you are always free to use multiple ServiceDiscovery instances.

In v1 ServiceDiscovery knows about instances of all services. What this comment suggests is that in v2 each ServiceDiscovery would know about instances for one service only, in which case it would make sense to drop Service from ServiceDiscovery since neither lookup nor subscribe requires service param anymore.

I think we can drop lookup as well, because in most if not all cases people use subscribe only.

what use cases did you have in mind for it originally?

As described above, the proposed model is very different from that of v1. If we agree on the new model, then yes, I think we can drop Service from ServiceDiscovery.

@tomerd
Copy link
Member Author

tomerd commented Jul 13, 2023

okay update the proposal @yim-lee @fabianfett. did end up keeping the ServiceDiscoveryInstancesSequence protocol instead of a associated type as suggested since without it I found I would need to make the internal implementation of the async sequence public which is not desirable IMO. thoughts?

@tomerd tomerd changed the title [wip] 2.0 API proposal 2.0 API proposal Aug 3, 2023
@tomerd
Copy link
Member Author

tomerd commented Aug 3, 2023

thanks for the ideas and PR @fabianfett, applied!

@tomerd tomerd requested a review from fabianfett August 3, 2023 23:14
Copy link
Member

@fabianfett fabianfett left a comment

Choose a reason for hiding this comment

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

Another round of reviews

return nil
}
/// Internal use only
public struct _DiscoverySequence: AsyncSequence {
Copy link
Member

Choose a reason for hiding this comment

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

I guess this one is Sendable?

/// Service discovery instance type
associatedtype Instance: Sendable
/// AsyncSequence of Service discovery instances
associatedtype DiscoverySequence: AsyncSequence where DiscoverySequence.Element == [Instance]
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to require that the DiscoverySequence is Sendable? This will make usage sites much simpler.


/// Default timeout for lookup.
var defaultLookupTimeout: DispatchTimeInterval { get }
public protocol ServiceDiscovery: Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

Protocol has the same name as module. ‼️🚨 For using namespaces? Module rename to ServiceDiscoveryModule?

Copy link
Member

Choose a reason for hiding this comment

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

Or maybe rename one of them to just Discovery?

Copy link

Choose a reason for hiding this comment

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

Protocols that describe a capability should be named using the suffixes able, ible, or ing (e.g. Equatable, ProgressReporting).

So ServiceDiscovering?

Copy link
Member

Choose a reason for hiding this comment

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

This is a thing that can discover services, maybe ServiceDiscoverer?

Sources/ServiceDiscovery/ServiceDiscovery.swift Outdated Show resolved Hide resolved
Sources/ServiceDiscovery/InMemoryServiceDiscovery.swift Outdated Show resolved Hide resolved
@tomerd tomerd mentioned this pull request Sep 14, 2023
@ser-0xff
Copy link

ser-0xff commented Sep 15, 2023

Hi!
Had a look at suggested new API for service discovery.
It is not clear to me how is it supposed to cancel a subscription?
For example user called subscribe(), got a sequence and then iterate it...
And then by some external event it should cancel the iterating loop, which is just waiting in the iterator.next() call.
What options to do it with a suggested API?

@fabianfett
Copy link
Member

Hi!
Had a look at suggested new API for service discovery.
It is not clear to me how is it supposed to cancel a subscription?
For example user called subscribe(), got a sequence and then iterate it...
And then by some external event it should cancel the iterating loop, which is just waiting in the iterator.next() call.
What options to do it with a suggested API?

@ser-0xff the iterator.next() call must support task cancellation. This means that those iterators will throw a CancellationError or return nil from the iterator.next() call. Most services will implement using withTaskCancellationHandler. Is that explanation sufficient?

@ser-0xff
Copy link

Thank you for the reply.

tomerd and others added 11 commits November 2, 2023 17:41
motivation: adopt swift concurrency

changes:
* replace callback based APIs with swift concurrency and async sequence
* remove Mapping wrappers since they are not longer needed and can easy be achieved using conventional functional composition on top of asyn sequence
* re-implement in-memory service discover to match the new API
* add and adjust tests
@tomerd
Copy link
Member Author

tomerd commented Nov 3, 2023

@yim-lee @fabianfett this has been updated per our discussion.

the one remaining issue I am aware of is #45 (comment). I dont have a good name atm

README.md Outdated Show resolved Hide resolved
Sources/ServiceDiscovery/Docs.docc/index.md Outdated Show resolved Hide resolved
Sources/ServiceDiscovery/Docs.docc/index.md Outdated Show resolved Hide resolved
Sources/ServiceDiscovery/Docs.docc/index.md Outdated Show resolved Hide resolved

/// Default timeout for lookup.
var defaultLookupTimeout: DispatchTimeInterval { get }
public protocol ServiceDiscovery: Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

Or maybe rename one of them to just Discovery?

Sources/ServiceDiscovery/ServiceDiscovery.swift Outdated Show resolved Hide resolved
Sources/ServiceDiscovery/ServiceDiscovery.swift Outdated Show resolved Hide resolved
Sources/ServiceDiscovery/ServiceDiscovery.swift Outdated Show resolved Hide resolved
Sources/ServiceDiscovery/ServiceDiscovery.swift Outdated Show resolved Hide resolved
tomerd and others added 2 commits November 3, 2023 11:12
Comment on lines +40 to +47
public protocol ServiceDiscoverySubscription: Sendable {
/// Service discovery instance type
associatedtype Instance: Sendable
/// AsyncSequence of Service discovery instances
associatedtype DiscoverySequence: Sendable, AsyncSequence where DiscoverySequence.Element == Result<[Instance], Error>

/// - Returns a ``DiscoverySequence``, which is an ``AsyncSequence`` where each of its items is a snapshot listing of service instances.
func next() async -> DiscoverySequence
Copy link
Member

Choose a reason for hiding this comment

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

This protocol is a bit weird. I assume your intention is much closer to this:

tomerd#2

Let me know what you think :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants