ucan-wg / invocation

UCAN Invocation & Pipelining
Other
12 stars 5 forks source link

Racing multiple promises #7

Open Gozala opened 1 year ago

Gozala commented 1 year ago

At the moment tasks can be pipelined to run after some set of promises resolve / fail. However there is no way to race multiple tasks and run another task with a winner of the race in the input.

It may be a good idea to have it out of scope in the initial version, but it also may be a good idea how it can be added in the future as it may inform some of the current design.

Perhaps race should be it's own task which could address the use case at hand, but in that case invocation itself needs to support some OR based task submission.

expede commented 1 year ago

Yeah, I've been thinking about this kind of thing as being their own task type. It's essentially an effect on multiple pending inputs. I don't think that there's anything in the current draft that would prevent this; it just needs a Task description and implementation.

Gozala commented 1 year ago

That makes sense. However what I find curious different judgment was applied to batch task scheduling (which, to some degree, overlaps with https://github.com/ucan-wg/invocation/issues/6) E.g. why have a special batch syntax as opposed to a task (that scheduler/runtime can provide) that can take bunch of tasks (or promises with inlined tasks) to run them ?

andrewzhurov commented 1 year ago

Yeah, I've been thinking about this kind of thing as being their own task type. It's essentially an effect on multiple pending inputs. I don't think that there's anything in the current draft that would prevent this; it just needs a Task description and implementation.

From what I understand, that Task (Instruction) would mean to await multiple other Instructions, outputing the awaited result that arrived first. From my understanding of how await works, Invocation would await all of the await inputs before invoking the Instruction with them. I guess we could add to spec a "special" kind of tasks, specifying at Task level that it can start evocation having resolved only some of the awaited inputs. A sort of "await any" logic, at the Task level.

Alternatively, perhaps we could have it at the Instruction level? Extending await spec with ala js Promise any

{:input {:firstFulfilledValue {:await/any ["bafy...instruction1",
                                           "bafy...instruction2"]}}}
expede commented 1 year ago

Hmm await/any is interesting. The project that my team is working on has determinism requirements for the base case (for verifiability), so this nondeterminism makes that harder 🤔 I'll think on it a bit more though

andrewzhurov commented 1 year ago

That's a good point! Some further thoughts:

Await seems non-determenistic.

Due to the fact that there may be multiple Receipts with different outputs. E.g., a malicious one, as in example of #22

Also, even when all Receipts are truthful, there may exist different Tasks for the same Instruction, that attenuate "how" differently. E.g., one attenuates it with "max threads 5", other attenuates with "max threads 1", which may result in different output if the Instruction is not agnostic to threads amount. Same with memory, thread Hz, any other environment/"how" parameter. Not sure how much is that a problem, perhaps Instructions can be crafted to be agnostic to these parameters. Otherwise the output even for the same Tasks may vary, as Executor would not be able to control all the parameters to be exactly the same across runs, e.g., making sure there's exactly 2Ghz on each thread, and not 1.99Gz.

Assuming we have agnostic Instructions, there may be non-determenism for await/* when limiting on gas. E.g., Task1 limits gas to 500 and successfully computes, whereas Task2 limits gas to 2 and fails to compute with these resources. Then it's not clear which should be awaited, the "ok"ed Receipt or the "error"ed Receipt.

Perhaps we can achieve determenism at the level of Tasks if we were to bake-into it the exact Receipt "awaited". That however would require to await all awaits of an Instruction, collecting them at the Task level, before issuing an Invocation. So user, instead of sending Executor Invocations, would need authorize and send to Executor Tasks, and perhaps Invoke only the ones that have no "await" deps (leaf nodes). Executor then starts their execution, attenuating non-leaf Tasks with the result of their dependencies and would issue further Invocations for those that have all deps resolved. So the invocation graph gets constructed gradually as execution progresses. Pro: there won't be "hanging" invocations, deps for which never arrive, e.g., one that await/ok but the Instruction "error"ed, as Invocations are issued only when they can be executed.

andrewzhurov commented 1 year ago

Thought on naming of "await the first fulfilled result". Perhaps a better name for it is await/first. It's meant to fire once. Whereas await/any seems to mean "fire with any of these fulfilled results" and may be issued for each result as it becomes available.

E.g., Alice could issue a Task to notify her of any failed Instructions. And so it'll be invoked when instruction1 fails and when instruction2 fails.

{"rsc": "mailto:alice@example.com",
 "op": "msg/send",
 "input": {"to": "alice@example.com",
           "subject": "An Instruction failed",
           "body": {"await/any": [{"await/error": {"/": "bafy...instruction1"}},
                                  {"await/error": {"/": "bafy...instruction2"}}]}}}

Also, perhaps await/first and await/any, if we to have them, is to be ok/error agnostic, i.e., they don't specify that awaited Instructions should be ok, ok/error specified per Instruction, allowing for better flexibility, as seen in

{"await/any": [{"await/error": {"/": "bafy...instruction1"}},
               {"await/error": {"/": "bafy...instruction2"}}]}