flow
Compare

Qoqi Flow vs DBOS

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.

Try it in five minutes See the code

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.

The compiler holds the line

A requests.post written straight in a DBOS workflow runs again on every recovery, unrecorded. In Qoqi it does not compile.

Deploy any day

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.

Recovery means running it again

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 · rules.py9 lines
@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)
Qoqi Flow · plain.qi11 lines
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);    }}

The stray call, caught

A request written straight in the workflow. DBOS runs it again on every recovery; Qoqi does not compile it.

DBOS · stray.py6 lines
@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 Flow · f0001.console8 lines
$ 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

Shipping a change under running workflows

DBOS patches branch on a name, then get deprecated, then removed. A Qoqi instance moves at a stage.

DBOS · version.py14 lines
# 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)
Qoqi Flow · stage.qi16 lines
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

Side by side

DBOSQoqi Flow
How a workflow comes backCalled again from the top, checkpointed steps skippedIts state thawed, at the line it was at
Workflow codeSame steps, same inputs, same order, or DBOSStepNondeterminismErrorAnything
A call outside a stepRuns again on every recovery, silentlyRefused at compile time (F0001)
New code under running workflowsRecovered by the same application version only; patchesInstances move at a stage, checked against flow.lock
A step that must never run twiceSteps run at least onceSettled<T>: never run again; the flow is told Unknown and decides
Crash testingNot providedeveryCrash: the test again, stopped after every write, same ending required
Steps in your own servicesInside your Python, TypeScript, Go or Java processAny server that speaks the protocol: a signed HTTP POST. SDKs for Go, PHP, TypeScript
ConsoleDBOS Conductor, proprietaryBuilt into qoqi serve, open source

Questions

Is Qoqi Flow ready for production?

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.

DBOS is a library. Isn't a daemon heavier?

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.

How do DBOS's ideas map?

@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.

Where does it run?

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

Write the process. Let it wait.

$ curl -fsSL https://qoqi.dev/install.sh | sh

Quickstart GitHub