[Pitch] Distributed Actors

I saw that too, and that's why I'm puzzled by the fact that this proposal depends on an underscored feature that could change without warning or disappear completely in future versions of the compiler.

1 Like

The proposal has a comment about this:

NOTE: The @_dynamicReplacement(for:) will be replaced with a distributed actor specific attribute. The proposal and use of dynamic member replacement reflects the current state of the implementation.

We are likely going to propose some form of @distributedRemoteImplementation(for:greet(_:)) in order to completely hide the existence of the _remote functions from users. The implementation mechanism would be the same, by making use of dynamic member replacement. This change will be introduced in an upcoming revision of the proposal.

4 Likes

Sorry, I somehow missed that. My worry about underscored attributes still applies to the use of @_marker though.

Since @_marker isn't (I believe) intended for use in user code, I think the situation is somewhat different. Implicit in the "underscored language features may be removed at any time" policy is that we won't break the source compatibility promise, so to the extent that any language/standard library features depend on a given attribute, a suitable replacement would have to be developed.

3 Likes

Thanks for the early replies and feedback everyone! I'll go "topic by topic":

Typed throws

On a personal note, and coming from years of work both with checked exceptions as well as actor runtimes, I really don't really think typed throws are as great as forums threads often make them out to be. They look nice in small examples, but in long running, evolving projects, they're more of a trap than help to be honest IMHO since they become part of the API and can never change; while error conditions or underlying transports and implementations do change all the time.

Ok, but typed-errors rant aside: you are correct, if Swift had typed errors this indeed would be possible to enforce that errors thrown by distributed functions must satisfy the Codable (or similar) requirement.

Do note however that signatures would become a union of YourError | ActorTransportError and be may be somewhat annoying to deal with...

Having that said, there are currently no plans for typed throws in Swift so this design cannot assume them.

Codable

Being an avid protocol buffers user for many years myself... we do have to realize though that the two are at odds here -- since both protocols (or someday maybe "service definitions") and protobufs Interface Definition Language serve the same purpose: to be the source of truth for the types involved. We are interested in making Swift protocols be the interface definitions, rather than using external languages to do so.

Related note: The reason for SwiftProtobuf not adopting Codable isn't just performance, it is because the entire model of working with those types is completely different - they're IDL driven and have their own quirks.

This is what the future direction Future Direction: Ability to customize parameter/return type requirements is all about. It's a relatively simple future feature and it may even just happen during the initial work on this feature.

It would enable specifying any other type requirement, other than Codable that parameters and returns are checked against. The example uses a "SuperSafe" enum of trusted values, but equally well one could imagine using ProtobufMessage there if one really wanted to.

ActorIdentities with multiple replicas

To answer your question about "actor is a service, has multiple replicas" -- they'd have different identities, because an identity must uniquely identity a specific actor.

Think of ActorIdentity similar to ObjectIdentifier however that can persist over time and process boundaries. This matters for example, a "long lived" and "well known" distributed actors which can be resolved a well-known identity such as /virtual/game/lobby and the system ensures there is always one such actor and it can always be resolved, regardless where it is physically located.

Typically though actor identities are more specific, for example following URL-ish patterns like this: sact://127.0.0.1:8888/user/worker#123545 where the last bit is the unique identity of it.

How specifically identities are used it up to the transports though -- some may choose to just use an integer (process IDs perhaps?) and perform mapping of those integers to actual instances.

This is why the identity is a protocol.

Polyglot communication

So while it is absolutely possible: you can easily implement a _remote function in a way that a remote other-language process may understand, this is not a goal of this work. We are aiming for simple and native feeling Swift actor systems that inter-operate the best with each-other.

In a server world it is best to think about this as best used within a single clustered service. You wouldn't really mix languages within the same system; going across languages can of course continue to be done with gRPC and HTTP and other tools. A similar approach has been taken by Akka and other actor runtimes in the past.

In a multi process world, we are interested in making xpc services and other cross process work on Apple platforms "feel great". Think how NSXPC works great in objective-c but feels pretty weird when used with Swift; this is a similar situation here, we intend to provide a solution that feels best for Swift, and it may be accessible to other languages, but it is not a primary goal of this work.

6 Likes

The use of _dynamicReplacement

As mentioned in the proposal, this will be replaced by some non-underscored attribute of similar capabilities.

We do not consider having to stabilize the dynamic member replacement feature in order to land this feature. Note that the existence of any dynamic functions is completely hidden from users already; it only leaks through this @_dynamicReplacement attribute; Our goal would be to not have users worry about how the remote function is "getting there", it may be the _dynamicMember replacement or some other underlying mechanism, it doesn't really matter as far as transport implementors are concerned.

Another future direction which impacts this is Synthesis of _remote and well-defined Envelope<Message> functions because if we were able to get away with not having to rely on the source generation to fill in _remote functions, there would not be any need for those replacements, as a transport could completely implement it all within library code.

This is a very tricky problem though, and it'd likely both need Variadic Generics as well as some more general and powerful meta-programming tools to make this a reality. There are also concerns about forming a "general message type" as we don't know yet about some of the specific requirements of a few transports we are investigating. We don't want to lock in this part of the design into the language unless we are 100% confident it would support all future transport implementations - we are working on figuring this out though.

4 Likes

I am still digesting the pitch and hope to come with more feedback later, but one short comment:

Being an avid protocol buffers user for many years myself... we do have to realize though that the two are at odds here -- since both protocols (or someday maybe "service definitions") and protobufs Interface Definition Language serve the same purpose: to be the source of truth for the types involved. We are interested in making Swift protocols be the interface definitions, rather than using external languages to do so.

This is super nice if realised, not having to keep a separate interface definition and all that entails in codegen and interface would be very convenient.

I do share the concern of @gwendal.roue about performance though, I'd echo it'd be nicer to formulate a more generic approach where Codable is a convenience. Looking at the future direction of customised parameter/return types, maybe it should be surfaced such that Codable needs to be specified also in the common case?

Also, a related concern - have there been thoughts of how many copies/transformations that will be needed from the raw transport data read until it's accessible by the distributed actor? For some use cases (e.g. distribution of small(ish) structs with a handful of plain data types, or small vectors of such) this will be compared to non serialisable format that one can access similar to Cap'n'proto, SBE or Flatbuffers - where it is possible to have fundamentally zero copies/transformations if done right. It'd be great to consider such use cases too.

Overall, I'm very glad to see this work in progress, just want to ensure we'll be able to use it in practice :-) .

3 Likes

One thing that strikes me as odd is the ActorTransport parameter to the actor initializer. Wouldn’t it be better to also require a specific parameter name? That would be consistent with property wrappers where wrappedValue and projectedValue parameters? That would also enable some syntax sugar (just like property wrappers) to make it more idiomatic.

3 Likes

I think the Actor protocol should be renamed LocalActor if this is to be accepted. Having Actor inherit from AnyActor is confusing, and none of these names have been formally released yet anyway.

Swift 5.5 likely ships by the end of the month, so it's pretty well locked now.

I feel like this is going to be a common programming error forever if it isn’t changed now, though.

Then again, I suppose StringProtocol already has this problem. Maybe just name it ActorProtocol?

I don't feel like it's necessary to use up a name here. All that matters is that there is an ActorTransport provided, we don't really care how you name it, it could be useful for you to use different names:

let devCluster = Cluster(...) // : ActorTransport
Worker(cluster: devCluster)

etc.

Can you provide an example of what you mean? It has to be passed at initialization time, there's no real way around the fact unless you're hardcoding a specific transport.


There is a potential future direction in which we may want to allow declaring the property as

distributed actor DA {
  nonisolated let transport = HardcodedDontDoThisPleaseThough() 
}

that is a bad-idea™ though as it makes it hard to swap the transport and make use of a different one in testing, so I don't think this is such a great thing in reality. It also is difficult to share a nicely configured instance this way without having to use globals. So this isn't really that good of an idea, even if we'd allow it in the future.

2 Likes

That's another reason we rely on the accompanying source-gen's today -- you can go wild there, literarily zero copy if you really wanted to -- but then your actors would need to accept whatever serialization buffers or types you'd need to achieve that. You'd be sidestepping Codable entirely as well.

If/when we pursue the Envelope<> synthesis without source generation, a zero copying approach might become more difficult. But you can trust that we're very interested in allowing this to be high performance solution :slight_smile:

I think it's worth keeping in mind that while Codable has performance limitations, we can and will keep improving it. It is a "good default" because it is convenient and good enough for most cases. People with more specific requirements should be able to do more specialized things if they need to (which is why opening up that SerializationRequirement typealias perhaps), but Codable is totally fine for most applications.

2 Likes

Could distributed actors be used to build a Swift-first successor to Core Data (or any other object graph)? It seems like you could implement a damn good NSManagedObject replacement this way.

After all, Core Data was always meant to be backend-agnostic, and almost every problem it has seems to have a superior solution in Swift at this point. Migration aside, anyway.

2 Likes

Is it really necessary to prohibit distributed computed properties? I understand the reasoning, since it might be very expensive to access, but I’d still rather avoid an explosion of method getters and setters. Computed properties combining the two makes APIs feel far less cluttered.

Swift’s current approach to deceptively expensive computed properties is simply cautioning against them and encouraging scrupulous documentation. I think that should be sufficient for distributed actors as well. Frankly, it’s bad practice to use properties without checking time complexity anyway.

4 Likes

Why doesn’t DistributedActor refine Identifiable? It obviously meets the requirements.

1 Like

It does.

public protocol DistributedActor: Sendable, Codable, Identifiable, Hashable {

See: https://github.com/ktoso/swift-evolution/blob/distributed-revised/proposals/nnnn-distributed-actors.md#initialization

3 Likes

Ah, never mind then. I missed that somehow, sorry.

1 Like

Putting aside attempts at constructive criticism for a moment, I must say this is one of the most impressive proposed features I’ve ever seen. It’s the product of an immense amount of work since Swift’s inception, and it seems to rule out a staggering array of logic and coupling errors without sacrificing versatility or clarity.

11 Likes

I'm incredibly impressed by this pitch – I saw the development of distributed actors throughout the past couple of months by peeking at some of your git branches, but this is truly impressive, so congratulations.

I'm curious about this:

Once we are confident in the semantics and language features required to get rid of the source generation step, we will be able to synthesize the appropriate Envelope<Message> and represent every distributed func as a well-defined codable Message type, which transports then would simply use their configured coders on.

What language features are required? IIUC, code synthesis is nothing new to Swift, as Codable relies on it extensively. Why do we need to wait for more metaprogramming-related language features to land? Can't this synthesis behaviour be baked into the compiler, like so much of the distributed actors feature already is?

3 Likes