unisonweb / unison

A friendly programming language from the future
https://unison-lang.org
Other
5.73k stars 266 forks source link

RFC: Unified APIs for distributed programming, durable state, fault-tolerance and live redeployments (v2) #142

Closed pchiusano closed 7 years ago

pchiusano commented 7 years ago

I put together a new version of the API, based on the discussion in #141 and some conversations in the Gitter room. Thanks to everyone who participated in that discussion!

I've checked in the latest version here, so we can track revisions to it:

https://github.com/unisonweb/unison/blob/4b4da116cb10d35e93e2f6d46c01d738072ae6af/docs/distributed-programming-rfc.markdown

The history section of that doc has info on changes I incorporated. Things got simpler, and API got smaller.

atacratic commented 7 years ago

Looks really good! :smiley:

Have put some comments/questions inline here: https://github.com/unisonweb/unison/commit/f0d0d9f9206f45b0b8ae968725a9d548f3c19f14

pchiusano commented 7 years ago

@atacratic once again appreciate your detailed comments and questions, I'm going to try to get to these today or else Real Soon Now.

atacratic commented 7 years ago

@pchiusano OK cool no rush! :-) Am particularly keen to hear what you think on the Durable comments - really want to get my head round that side of things.

pchiusano commented 7 years ago

Hey @atacratic just fyi I didn't get through all your comments yet... will pick up either this weekend sometime or early next week.

pchiusano commented 7 years ago

Been pondering this a bit more, not liking the complexity around heartbeats. Might have an idea for how to just eliminate them from the API without losing expressiveness! Will do writeup this week.

atacratic commented 7 years ago

Ha, I've been pondering as well but only just saw your note above! Looking forward to writeup :smiley:

pchiusano commented 7 years ago

Okay, here's a quick writeup that I'm becoming fond of (I like deleting stuff!!!). Get rid of Heartbeat entirely, instead have:

-- Fork a computation, returning a `Task` handle that can be used for supervision
Remote.fork : Remote a -> Remote Task

-- Stop a task with the given cause
Task.stop : Cause -> Task -> Remote Unit

-- Be notified when the task completes (possibly call this `Task.await`?)
Task.supervise : Task -> Remote (Remote Cause)

-- Ensure if either task stops, other task is stopped as well, with the same cause
-- Exercise: implement in terms of `Task.stop` and `Task.supervise`!  
Task.link : Task -> Task -> Remote Unit

-- Pause the current thread for the given duration, then return
Remote.sleep : Duration -> Remote Unit

(Another possible name for Task: Process. I kinda liked vague analogy with Erlang, but they are different concepts as an Erlang process is an actor with a mailbox, whereas Task is more like a handle to a lightweight thread, think like ThreadId in Haskell)

That's it. Some notes:

Something like Heartbeat, if useful, could still be an ordinary pure Unison library.

atacratic commented 7 years ago

I like it :+1:

The reason this is easier to deal with than Heartbeats is that a Heartbeat came with a set of forks to supervise, which (I think) is inherently a distributed consensus sort of problem. The new proposal dodges this by cutting out any 'supervise a set of things' API. You just supervise a single Task at a time, so your task-chasing polling mechanism works a treat - each poll is just chasing one thing around.

Questions/issues:

I still have a nagging feeling that we are butting up against problems which are fundamentally distributed computing problems, and will need some distributed computing machinery (e.g. a distributed hash table, or a raft cluster, or a commutative event log, or whatever) in order to robustly solve. E.g. supervision only working if done from a node on a Task's path history; and path node failures breaking task-chase polling. Unison is a distributed computing platform, so it would not be so shocking if it needed some machinery of the sort I mentioned under the covers. But, all the same, it's completely right to want to avoid adding that machinery unless and until it's proven completely necessary. I do like the idea of the Unison primitives being built on simple soft-state foundations, and that being enough to build robust distributed systems out of in userland code. If it's possible...!

Slight diversion: are you thinking that each Task will occupy an OS thread in the Unison node OS process 100% of the time, even when blocked or sleeping? Am hoping you're going to say that the answer is somehow 'no'. It's very much a first-order property of the Task API, whether they are cheap enough not to have to ration. I have a suspicion that Go has nailed this question with its segmented stacks but I guess we might be limited to what Haskell can easily offer the interpreter implementation?

pchiusano commented 7 years ago

Task chasing is not resilient to loss of nodes along the path, right?

Only before starting supervising. Once you've started supervising, you will follow the task as it hops around, you don't go all the way back to start node. You are almost always going to start supervising right away or not at all, so I think this is fine. Also node failures are not THAT common. Don't want to overengineer this.

Task.supervise will only work on nodes that are on the target task's visited path.

No that is not correct. It can be called from any node. The Task value itself can contain the starting node of the forked computation, and then supervisors can chase that to the task current location. Does that make sense?

Unless I'm missing something, the Task.link implementation also needs to use Remote.fork

I think so. I'm not too concerned about the node implementing the linkage going down. I mean, if you are concerned about that, that is a good reason to establish the linkage on two different nodes on different machines. That seems like something that is a user decision. I'm not sure I fully understood your concern here...

Is supervision still all poll and no push

That's an implementation detail, but I'd say status updates not caused by an asteroid strike can be pushed to supervisors as promptly as possible. Asteroid strikes will eventually be discovered also, but will need to wait for timeouts.

I still think your task chaser poll messages need a TTL.

I don't think so. Maybe I should try to produce that proof and then it will be more clear what assumptions go into it or if my reasoning is wrong. 😀

Is the canonical usage example that a parent does a Task.link whenever it forks a child, to link itself to it?

It probably depends, but I think common case would be to fork then supervise.

I don't have the same nagging feeling... yet... 😀

Re: threads, yea they should be super lightweight, just like Haskell, not actually blocking OS threads.

On Sun, Feb 19, 2017 at 7:57 PM Chris Gibbs notifications@github.com wrote:

I like it 👍

  • The parent/child setup is nice and familiar from UNIX processes and erlang.
  • fork and supervise are familiar from pthreads (create and join).
  • It means the Unison concurrency primitives are basically Box and Task, which are both fairly well-trodden ground in other languages/systems. This is a good thing!

The reason this is easier to deal with than Heartbeats is that a Heartbeat came with a set of forks to supervise, which (I think) is inherently a distributed consensus sort of problem. The new proposal dodges this by cutting out any 'supervise a set of things' API. You just supervise a single Task at a time, so your task-chasing polling mechanism works a treat

  • each poll is just chasing one thing around.

Questions/issues:

  • Task chasing is not resilient to loss of nodes along the path, right? If a computation has been careering round some path from node to node, then the poll path (from the original starting node say) is going to follow the task path (with loops snipped out). Until it hits a node which has gone down. Isn't this a problem? Tasks might briefly visit a node on their way to some longer term home, and not want to die days later if the briefly-visited node goes down. (Maybe each transfer could send an update to a few carefully chosen previously-visited nodes in order to add some resilience - gossiping back along the path. This could be tricky to make resilient to cases of partition or where a bunch of nodes fail together.)
  • Task.supervise will only work on nodes that are on the target task's visited path. Is this OK? You nod to this in your 'something unusual' point but I think it actually needs to be mentioned in the API spec. It's a consequence of not having a single registry (DHT...) of running tasks. Or there is another fix: special-casing serialization of Task IDs so that when they are transferred some forwarding map info gets transferred too. Anyone who can refer to a given Task in order to call supervise on it is then definitely running on a node which has an entry (/some entries) for that Task in the Task map.
  • Unless I'm missing something, the Task.link implementation also needs to use Remote.fork (in order to start some supervisors without blocking the main thread that Task.link was run on.) But then those supervisor threads are going to be separated from the main thread if the main thread later decides to transfer. What if both the supervisors are left alone on a node that then fails? The linked tasks (off on some other nodes) are then no longer actually linked. This is where I was going with my note "These forked computations are guaranteed to stay up and watching as long as the node the parent is on is up. They move nodes with the parent." I was thinking that the forked supervision helpers need to trail around after the Tasks they are watching, as part of the Remote.transfer process. No, I don't particularly like that idea. Hopefully you have a better one, or maybe I've just missed the right impl of Task.link?
  • Is supervision still all poll and no push? Can we make it so that non-asteroid Task failure sends a push message to trigger supervisor completion? Otherwise people will have to roll their own supervision mechanisms to get timely failure handling in the non-asteroid case.
  • I still think your task chaser poll messages need a TTL. A poll could chase a Task for ever in a huge loop (around a massive parallel processing cluster say - imagine someone's written a token-ring task to collect stats or something), never catch up, and never see a status that was less than 10ms old.
  • Is the canonical usage example that a parent does a Task.link whenever it forks a child, to link itself to it? Unless it is explicitly a supervisor in which case it does a Task.supervise instead?

I still have a nagging feeling that we are butting up against problems which are fundamentally distributed computing problems, and will need some distributed computing machinery (e.g. a distributed hash table, or a raft cluster, or a commutative event log, or whatever) in order to robustly solve. E.g. supervision only working if done from a node on a Task's path history; and path node failures breaking task-chase polling. Unison is a distributed computing platform, so it would not be so shocking if it needed some machinery of the sort I mentioned under the covers. But, all the same, it's completely right to want to avoid adding that machinery unless and until it's proven completely necessary. I do like the idea of the Unison primitives being built on simple soft-state foundations, and that being enough to build robust distributed systems out of in userland code. If it's possible...!

Slight diversion: are you thinking that each Task will occupy an OS thread in the Unison node OS process 100% of the time, even when blocked or sleeping? Am hoping you're going to say that the answer is somehow 'no'. It's very much a first-order property of the Task API, whether they are cheap enough not to have to ration. I have a suspicion that Go has nailed this question with its segmented stacks https://blog.cloudflare.com/how-stacks-are-handled-in-go/ but I guess we might be limited to what Haskell can easily offer the interpreter implementation?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/unisonweb/unison/issues/142#issuecomment-280964657, or mute the thread https://github.com/notifications/unsubscribe-auth/AAArQkEtizf9j7RdUYTgi5s3C3tAgAsPks5reOTigaJpZM4MCFjn .

atacratic commented 7 years ago

The Task value itself can contain the starting node of the forked computation

Cunning!

[re Task/link] I'm not too concerned about the node implementing the linkage going down. I mean, if you are concerned about that, that is a good reason to establish the linkage on two different nodes on different machines.

Ah yes, and if you want to move a supervisor, you can start a new one and then kill the old one.

I was fretting about the parent starting a supervisor and then being split up from it, because I saw the supervision as something the parent was doing. But that's not the case.

I should probably stop talking about parent/child, because that calls to mind supervision trees, but here we're not being forced into a supervision tree model and we're certainly not being forced to make supervision follow parent/child (who-forked-who) relationships. The parent/child setup created by forking is only relevant when killing tasks.

People are naturally going to gravitate towards the assumption that parents supervise children, so I think I won't be the last person slightly confused by this.

Do we talk about parent tasks and child tasks? What's the terminology? Ah you used 'subtasks'.

BTW I think your comment above Task.stop should say "Stop a task (and its subtasks)".

TTL

Probably the extra assumption you're making is that task chaser poll messages move faster on average than the top speed of the tasks they're chasing - but probably that's a fair assumption.

I think common case would be to fork then supervise

Or maybe it's actually to fork and then fork a supervisor.

Other stuff:

pchiusano commented 7 years ago

Closed for #144, progress continues, thanks everyone!!