-
Notifications
You must be signed in to change notification settings - Fork 8
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
Deterministic promise combinators #231
Conversation
005b857
to
59fcb84
Compare
TODO for me:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, +1 to this approach, with two main thoughts.
Names
Should we call it RestatePromise
rather than CombinablePromise
?
Putting myself in the shoes of a user, if I see RestatePromise
I have an idea on how to place this (it's a promise from Restate, the system for durable promises - kind of makes sense). For CombinablePromise
I don't see that immediately. Might be some node internal or library promise type?
API
The methods for all()
, any()
, etc. don't really belong to the context for me. The fact that those operations require to store some data in Restate is more of an implementation detail that should not leak into the API.
Could we do the following?
- define static methods
RestatePromise.all()
, etc. - have the context in the
CombinablePromise
(orRestatePromise
) as__restatectx
. This would act as the tag (which currently is the__combinable
property). - The static method would check that both promises indeed have that property (not just rely on compile time type checks) and that both are the same object (
===
) and then use that context to create the journal entry.
@@ -220,6 +229,10 @@ export class RestateGrpcContextImpl implements RestateGrpcContext { | |||
); | |||
} | |||
|
|||
rpcGateway(): RpcGateway { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did this sneak in as a leftover from some other change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this method was simply moved in a more logical place (close to the other call methods)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully get the logic that tags the CombinablePromise
. Maybe I am overlooking something, but it looks a bit incomplete still. Here are the core points:
-
The promise has the
__combinable
property as a tag. But that property isn't checked anywhere. Looks like the code relies purely on type safety from the TS compiler typing. -
While this probably catches most cases during development, none of this info exists at runtime, so double checking whether you actually get a
CombinablePromise
by checking that property makes sense and is good practice, as I understand it. -
The
__combinable
property is set as a property that takes the valueundefined
. This is a really special way of doing it, which means thatpromise.__combinable === undefined
no matter whether something is aCombinablePromise
or not. Just thatpromise.hasOwnProperty("__combinable")
would be false for non-CombinablePromise
types.Why not just make it a boolean flag, and you can directly check where ever you expect a
CombinablePromise
whetherpromise.__combinable === true
?This would also simplify the setter code
(promise as any).__combinable = true;
or doconst cp = promise as CombinablePromise; cp.__combinable = true; return cp;
clearTimeout(this.suspensionTimeout); | ||
this.suspensionTimeout = undefined; | ||
} | ||
this.clearSuspensionTimeout(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do the promises change the suspension behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No they don't, this was a little cleanup i did while analyzing this code.
I gave some thoughts to it. My problem with calling it just
|
@StephanEwen I updated this PR changing how we expose the combinator methods, doing as you suggested: |
This is the component that creates the promise combinators, and encapsulates the business logic to make them deterministic.
…side the other. We need this once we wire up the PromiseCombinatorTracker with the rest of the state machine.
Mark sleep and awakeable returned promises as combineable (more to follow)
… Now on `then` we either replay deterministically, or wire up the proxy promises with listeners to record the order. We need this change because we cannot establish before the await point whether we're in processing mode or not. The construction of the promise combinator might happen before it's awaited, and its await might be interleaved with other entry writes.
…mment for more details.
…ictly related to this PR, this verifies a behaviour I previously encountered while implementing the AwakeableSleepRaceInterleavedWithSideEffectGreeter test suite.
41e4a10
to
9675b81
Compare
Fix #15
Feature scope
To gradually implement this feature, I reduced the scope of this PR as follows:
RestateContext
:ctx.all
,ctx.race
,ctx.any
andctx.allSettled
. There was a discussion here Determinism of Promise.any/all #15 (comment) about overriding the globals, but for the time being the methods in the context will do fine.sleep
andawakeable
returned promises are "combineable". In particular, I left out:call
handler API: this should be straightforward to do. I expect to fix this in a follow up PR.call
gRPC API: this is a bit more complicated, as we cannot control the generated code output. This is perhaps related to Consider replacing protobuf library #209, where we discussed about replacing the protobuf library and build our own protoc plugin. I think we should fix this once we settle Consider replacing protobuf library #209.sideEffect
: this is a bit more complicated to support because of the side effect retry system, which might cause races in writing entries out. I don't plan to fix this for the time being, a user can just combine promises inside the side effect closure as they normally would do.get
: this doesn't seem useful to put in a combinator.call
seems more important.Implementation details
Given the feature scope, the idea is the following: Every time the user awaits a combinator, we record the order of which we see completions for the child promises. E.g. for ALL(P1, P2, P3) we record the order we saw the completions, e.g. C3, C1, C2. Once the combinator is completed (either fullfilled or rejected), we write in the journal what was the order in which we saw the completions. On replay, we make sure the combinator deterministically replays the order in which the completions were seen.
The business logic that records the completion order and ensures on replay the order is replayed deterministically is in the class called
PromiseCompletionTracker
.This class is then wired up to the state machine to read/write the
CombinatorEntryMessage
, that is the entry used to record the completions order, and the RestateContext APIs, to allow users to create those combinators.