[Proposal] Distributed Actor Isolation

Hello,

Splitting out the semantics of distributed actor definition and isolation makes a lot of sense to me. I have a bunch of little comments, but all of this is looking very good to me.

In the early discussion about remote and distributed actors, shouldn't the read and write operations be distributed but neither async nor throws?

distributed actor TokenRange {
  let range: (Token, Token)
  var storage: [Token: Data]
  
  init(...) { ... }
  
  distributed func read(at loc: Token) -> Data? {
    return storage[loc]
  }

  distributed func write(to loc: Token, data: Data) -> Data? {
    let prev = storage[loc]
    storage[loc] = data
    return prev
  }
}

It's probably worth a quick sentence in this section to say that one must try when calling these functions outside the actor because a communication error might describe failure by throwing, and that this will be discussed further later. It's an important part of the model to keep in one's mind when reading.

The "transport" terminology seems to have been partially replaced with "distributed actor system." This is a good change, but there are a few places where the term "transport" is still used. It's worth a quick search-and-replace.

Thus, access to a distributed actor's stored properties from outside of the actor's isolation are forbidden. In addition, computed properties cannot be nonisolated or participate in a key-path. We will discuss computed properties later on.

I don't think that latter part is true: computed properties can be nonisolated. I think what you want to say there is that stored properties cannot be accessed from outside the actor, even if they are immutable, and can not be nonisolated.

Distributed functions may be subject to additional type-checking. For example, in a future proposal we will discuss the serialization aspects of distributed method calls, where we will discuss how to statically check and enforce parameters and return values of distributed methods are either Codable , or conforming to some other marker protocol that may be used by the distributed actor runtime to serialize the messages.

I think this proposal would be more self-contained if we could bring a description of that type checking into this proposal, so we know the full set of requirements on a distributed method.

Remote actor references are not obtained via initializers, but rather through a special resolve(_:using:) function that is available on any distributed actor or DistributedActor constrained protocol. The specifics of resolving, and remote actor runtime details will be discussed in a follow up proposal shortly.

There are a number of places where the text refers to "any distributed actor or DistributedActor constrained protocol", and I think the text would benefit from defining a new term, distributed actor type, that describes any type that is known to conform to the DistributedActor protocol. Then, we can refer to distributed actor types throughout rather than this awkward phrase.

As a tiny, tiny nit, I'd drop the "shortly". Time is irrelevant on Swift Evolution :D.

Regarding the DistributedActor protocol, it is mentioned but never defined. I think it's important for this proposal to define this protocol, even if the follow-on proposal then extends it (e.g., with a distributed actor system member). I'll come back to the actual definition.

A distributed actor type, extensions of such a type, and DistributedActor inheriting protocols are the only places where distributed method declarations are allowed. This is because, in order to implement a distributed method, a transport and identity must be associated with the values carrying the method. Distributed methods can synchronously refer to any of the state isolated to the distributed actor instance.

Here's a good place to use the "distributed actor type" definition. A distributed method must be a member of a distributed actor type and its self must be isolated. There's no need to call out the transport or identity here.

There are two special properties that we'll discuss in the future that are accessible this way: the actor's identity, and the distributed actor system it belongs to. Those properties are synthesized by the compiler, and we'll soon explain them in greater depth in the runtime focused proposals detailing the distributed actor runtime design.

I don't think we need to call these out as special. First of all, the distributed actor system isn't really part of this proposal. Second, the identity is going to be part of the DistributedActor protocol, and can be a nonisolated computed property, so there's nothing special about it except that the compiler synthesizes the conformance of distributed actor to the DistributedActor protocol for you.

Distributed functions must be able to invoked from another process, by code from either the same, or a different module. As such distributed functions must be either public , internal or fileprivate . Declaring a private distributed func is not allowed, as it defeats the purpose of distributed method, it would not be possible to invoke such function using legal Swift.

I disagree with this restriction on private. You could have a nonisolated async throws method on the distributed actor that can call a private distributed method. distributed is orthogonal to access control.

It is not allowed to use rethrows with distributed functions, because it is not possible to serialize a closure and send it over the network to obtain the "usual" re-throwing behavior one would have expected.

As you note, the closure is the problem here, not the rethrows. I think we should remove this rethrows restriction and let the fact that a closure will not conform to Codable (or whatever) make this distributed method ill-formed.

Similarily, it is not allowed to declare distributed function parameters as inout .

I don't really see this as falling out from the closure/rethrows rule above. There are reasonable inout semantics we could implement in the semantic model, but the problem is really that we'll have trouble with the distributed actor system implementation. I think it's better to say that we ban inout because inout is not a first-class type in the type system, so we can't implement remote calls when there are inout parameters.

It is also worth calling out the interactions with Task and async let . Their context may be the same asynchronous context as the actor, in which case we also do not need to cause the implicit asynchronous effect. When it is known the invocation is performed on an isolated distributed actor reference, we infer the fact that it indeed is "known to be local", and do not need to apply the implicit throwing effect either:

I think this behavior isn't really about Task or async let. Rather, it falls out of the implied restriction that a local function or closure cannot be distributed. Therefore, if you are in a function that has an isolated parameter of distributed actor type, you know that the actor referenced by that parameter is local, and that functions/closures inside that method to that cannot end up ever being called remotely. I think this should come earlier in the section, to call out the "isolated", "local", "potentially remote" three states and how they are determined, so this "sometimes you don't need try" bit doesn't come as a surprise.

In the section on protocol conformances, the conformance of ExampleActor to Example requires that asyncThrows be distributed. I think that needs to be called out and explained more, because it's really important (and not as obvious as the other cases that are called out).

This proposal mentioned the DistributedActor protocol a few times, however without going into much more depth about its design.

Let's define it! I think it's something like this:

protocol DistributedActor: Sendable, Identifiable where Self.ID: Sendable {
  associatedtype SerializationRequirement = Codable
  nonisolated var id: ID { get }
}

Now, if we pull in SerializationRequirement, we have to explain it. I think that's a good thing, because it nicely ties up all of the semantics of distributed so the follow-on proposal on distributed actor systems can focus on that layer.

Doug

8 Likes