No re-execution
A workflow that read the clock or drew a random number takes another path on recovery, and DBOS raises DBOSStepNondeterminismError. A Qoqi flow is never run again.
Recovery that does not run your code twice.
DBOS recovers a workflow by calling it again and skipping the steps it checkpointed, so the code has to take the same path every time. Qoqi Flow thaws the frozen state at the line it was at: no re-execution, no determinism, and a compiler that checks what DBOS leaves to discipline.
A workflow that read the clock or drew a random number takes another path on recovery, and DBOS raises DBOSStepNondeterminismError. A Qoqi flow is never run again.
A requests.post written straight in a DBOS workflow runs again on every recovery, unrecorded. In Qoqi it does not compile.
DBOS recovers a workflow only on processes of the same application version, or behind a patch. Qoqi instances move to new code at a stage.
Everything outside a step runs once more when DBOS recovers the workflow, and has to take the same path. In Qoqi, the clock and chance are plain code.
@DBOS.workflow()def renewal(user: User) -> None: # Recovery calls this again: another hour, another path, # and DBOSStepNondeterminismError if datetime.now() < user.trial_ends: DBOS.sleep(2 * 24 * 3600) tip = random.choice(user.tips) # another tip on recovery charge(user.id, tip)
impl Flow<Void> for Renewal { fn run() { // The clock and chance are plain code: read once, never again if time.now() < self.user.trialEnds { wait time.after(2.days()); } let tip = self.user.tips[random.below(self.user.tips.len())]; charge(self.user.id, tip); }}
A request written straight in the workflow. DBOS runs it again on every recovery; Qoqi does not compile it.
@DBOS.workflow()def renewal(user: User) -> None: # Not a step: runs again on every recovery. # Nothing records it, nothing warns. requests.post("https://hooks.example.com/renewals", data=user.id) charge(user.id)
$ qoqi check renewal.qi[ERROR] F0001: a flow reaches the network outside a step --> renewal.qi:26:67 in function 'Renewal.run' | 26 | http.to(Method.Post, "https://hooks.example.com/renewals").body(self.user.id).send(); | ^ | = Suggestion: Move the call into a step fn, and call the step here
DBOS patches branch on a name, then get deprecated, then removed. A Qoqi instance moves at a stage.
# dbos-config: "enable_patching": True@DBOS.workflow()def expense(expense_id: str, employee: str, cents: int) -> str: notify_manager(expense_id) answer = DBOS.recv("answers", timeout_seconds=7 * 24 * 3600) if DBOS.patch("notify-accounting"): # True for new workflows only notify_accounting(expense_id) # Later DBOS.deprecate_patch("notify-accounting"), then remove it # once the old workflows are done. Without a patch, a workflow is # recovered only by processes of its own application version. return pay(employee, cents)
impl Flow<Outcome> for Expense { fn run() -> Outcome { notifyManager(self.id); let answer = wait self.answers.receive(); // Running instances move to new code here, and only here stage paying; notifyAccounting(self.id); // new in version 2 return pay(self.employee, self.cents); }}// $ qoqi deploy expense.qi// expense deployed, version 7b02e1c4 in place of 43381189:// 12 instances move to it at their next stage
| DBOS | Qoqi Flow | |
|---|---|---|
| How a workflow comes back | Called again from the top, checkpointed steps skipped | Its state thawed, at the line it was at |
| Workflow code | Same steps, same inputs, same order, or DBOSStepNondeterminismError | Anything |
| A call outside a step | Runs again on every recovery, silently | Refused at compile time (F0001) |
| New code under running workflows | Recovered by the same application version only; patches | Instances move at a stage, checked against flow.lock |
| A step that must never run twice | Steps run at least once | Settled<T>: never run again; the flow is told Unknown and decides |
| Crash testing | Not provided | everyCrash: the test again, stopped after every write, same ending required |
| Steps in your own services | Inside your Python, TypeScript, Go or Java process | Any server that speaks the protocol: a signed HTTP POST. SDKs for Go, PHP, TypeScript |
| Console | DBOS Conductor, proprietary | Built into qoqi serve, open source |
It is at v0.1.0. The language, the runtime and the daemon work end to end, and they are tested by crashing them on purpose after every write. What it does not have yet is years in production. Start with a flow that matters, not the one that holds the company up.
It is one binary on a SQLite file to start with. In exchange your application stays in its own language, the flow is readable in one place, and the daemon's console shows every instance at the line it is at.
@DBOS.workflow: impl Flow. @DBOS.step: step fn. DBOS.recv and DBOS.send: an Inbox field and qoqi send. DBOS.sleep: wait time.after(...). SetWorkflowID: the instance's name, qoqi start Flow name. DBOS.patch: stage.
Anywhere one binary runs. SQLite on one machine; PostgreSQL or MySQL when several daemons share the work. There is no managed cloud yet.
Also compared
curl -fsSL https://qoqi.dev/install.sh | sh