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

[WIP] Add Distributed module documentation #326

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ktoso
Copy link
Contributor

@ktoso ktoso commented Aug 7, 2024

This is a very early version of Distributed module documentation; It is not polished up, may have stray sentences and sections etc.

We'll figure out how to best present the information through the pitch posted on the forums over here:

I'll also work on improving tone and voice still, right now it is not the most consistent.

For now, we can discuss what needs to be explained in this documentation and I started a forums thread for this purpose: TSPL Pitch: Distributed.

Copy link

@martialln martialln left a comment

Choose a reason for hiding this comment

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

Mainly puts comments on the last section around Emulating callbacks as I found some errors in code examples

distributed func call(later: InfoListener) async -> String {
Task.detached {
// do some asynchronous processing AFTER returning from the 'call' method...
later("Here's the info you asked for!")

Choose a reason for hiding this comment

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

later is not a closure anymore and is async throws

Suggested change
later("Here's the info you asked for!")
try await later.additionalInfo("Here's the info you asked for!")

In distributed actors, the same can be achieved using distributed method calls rather than closures, like this:

```swift
protocol InfoListener: Codable {

Choose a reason for hiding this comment

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

Suggested change
protocol InfoListener: Codable {
protocol InfoListener: DistributedActor, Codable {


```swift
protocol InfoListener: Codable {
func additionalInfo(_ info: String) async throws

Choose a reason for hiding this comment

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

Suggested change
func additionalInfo(_ info: String) async throws
distributed func additionalInfo(_ info: String)

}

distributed actor Alice {
distributed func call(later: InfoListener) async -> String {

Choose a reason for hiding this comment

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

Suggested change
distributed func call(later: InfoListener) async -> String {
distributed func call(later: some InfoListener) async -> String {

In this case Later concrete type must be known on Alice side.

this protocol:

```swift
distributed actor Bob {

Choose a reason for hiding this comment

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

Suggested change
distributed actor Bob {
distributed actor Bob: Codable {

we registered a closure to be called at some later point in time when some additional information was looked up by the
actor. In local-only actors, a common way of modeling this is passing a closure to the actor.

In distributed actors, the same can be achieved using distributed method calls rather than closures, like this:

Choose a reason for hiding this comment

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

We should explain that Alice and Bob must share the same ActorSystem. Also there are two cases: is Bob type known on Alice side or not


extension Bob: InfoListener {
distributed func additionalInfo(_ info: String) {
print("later: \(laterReply)")

Choose a reason for hiding this comment

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

Suggested change
print("later: \(laterReply)")
print("later: \(info)")

print("later: \(laterReply)")
}
}

Choose a reason for hiding this comment

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

Using resolvable protocol we could have something like that:

@Resolvable
protocol InfoListener: DistributedActor, Codable {
    distributed func additionalInfo(_ info: String)
}

distributed actor Alice {
    distributed func call(later: $InfoListener) async -> String {
        Task.detached {
            // do some asynchronous processing AFTER returning from the 'call' method...
            try await later.additionalInfo("Here's the info you asked for!")
        }
        
        return "Thanks for your call!"
    }
}

distributed actor Bob: DefaultDistributedActorSystem.SerializationRequirement {
    func test(alice: Alice) async throws {
        let infoListener = try $InfoListener.resolve(id: self.id, using: self.actorSystem)
        let immediateReply = try await alice.call(later: infoListener)
        
        print("immediately: \(immediateReply)")
    }
}

extension Bob: InfoListener {
    distributed func additionalInfo(_ info: String) {
        print("later: \(info)")
    }
}

```swift
distributed actor Bob {
func test(alice: Alice) async {
let immediateReply = try await alice.call(self)

Choose a reason for hiding this comment

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

Suggested change
let immediateReply = try await alice.call(self)
// We have to make sure, in some way, that the `DistributedCallback` instance stays alive to receive the callback
let immediateReply = try await alice.call(later: DistributedCallback(name: "Bob", actorSystem: self.actorSystem))

Copy link

@akbashev akbashev left a comment

Choose a reason for hiding this comment

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

Overall like the page, gives a quick recap of the module!

Want to reread it a bit later with a fresh mind, but at the moment have two small comments. Added them as suggestions, but take them with a grain of salt.

  1. Overall I think would be nice to add some small sentence on why exactly one should use distributed module.
  2. Think memory management could also be highlighted—it's not quite obvious at a first glance. Though not sure about placing, it's quite specific. Maybe even be placed in Implementing Your Own DistributedActorSystem section as it's mostly for systems to care about?


## Thinking in Distributed Actors

In order to build distributed systems successfully you will need to get into the right mindset.
Copy link

Choose a reason for hiding this comment

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

Suggested change
In order to build distributed systems successfully you will need to get into the right mindset.
Building distributed systems is a complex task that requires careful consideration of reliability, maintainability, and scalability. There are various approaches to build such systems, and Swift, as a general-purpose language, offers tools for this, such as actors. However, in order to build distributed systems successfully with this tools, you will need to get into the right mindset.

The actor system may return a local or remote reference, however it should not perform asynchronous work such as
trying to confirm if the actor exists remotely or not. The resolve method should quickly return either a known
local actor identified by the passed `id` or a remote reference if the actor may exist remotely.

Copy link

@akbashev akbashev Aug 8, 2024

Choose a reason for hiding this comment

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

Suggested change
> Note: Be careful with memory management. Local actors already stored on the node, so it might be better to keep references to them as weak. However, for remote actors reference is generated, which could potentially be cleaned if not kept strongly. // TODO: Link to Automatic Reference Counting page?

As we can see, an cross-actor call to an actor function caused an implicit `async` effect,
while the same type of call to a `distributed func` caused an additional `throws` effect that needed to be handled
with a `try`. Since the method `increment(by:)` itself is not declared throwing by itself, we know that the only
way this method can fail, is by the underlying transport mechanism failing in sme way.
Copy link

Choose a reason for hiding this comment

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

Suggested change
way this method can fail, is by the underlying transport mechanism failing in sme way.
way this method can fail, is by the underlying transport mechanism failing in some way.

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.

None yet

3 participants