flow
Compare

Qoqi Flow vs Temporal

Temporal's durability, without the determinism rules.

Temporal replays your workflow from its event history, so your code has to play back identically, forever. Qoqi Flow freezes the state of execution and thaws it at the line it was at. Nothing replays, so nothing has to be deterministic.

Try it in five minutes See the code

No determinism rules

In Temporal, time.Now, rand, goroutines and ranging over a map are bugs waiting for a replay. A Qoqi flow never runs twice: it is just code.

Mistakes caught at compile time

A network call outside a step does not compile, and the error shows the path of calls that leads to it. Temporal finds out on replay, in production, or never.

One binary, not a cluster

No Temporal Service, no Cassandra, no Elasticsearch, no worker fleet. qoqi serve and a SQLite file; PostgreSQL or MySQL to share the work.

Wait for an answer, or seven days

The same wait, in Temporal's Go SDK and in Qoqi.

Temporal · wait.golang16 lines
var answer Answeranswered := falsetimerCtx, cancelTimer := workflow.WithCancel(ctx)sel := workflow.NewSelector(ctx)sel.AddReceive(workflow.GetSignalChannel(ctx, "answers"),	func(c workflow.ReceiveChannel, more bool) {		c.Receive(ctx, &answer)		answered = true		cancelTimer()	})sel.AddFuture(workflow.NewTimer(timerCtx, 7*24*time.Hour),	func(f workflow.Future) {})sel.Select(ctx)if !answered {	return "expired", nil}
Qoqi Flow · wait.qi7 lines
let answer = wait match {    case let a = self.answers.receive(): a;    case time.after(7.days()): null;};if answer == null {    return Outcome.Expired;}

Plain code, because nothing replays

Temporal has a replacement for the clock, for sleep, for randomness, for goroutines. Qoqi needs none.

Temporal · rules.golang16 lines
func Renewal(ctx workflow.Context, user User) error {	// Replayed from its history: no time.Now(), it's workflow.Now(ctx)	if workflow.Now(ctx).Before(user.TrialEnds) {		// no time.Sleep: workflow.Sleep		_ = workflow.Sleep(ctx, 48*time.Hour)	}	// no rand: wrap it in a SideEffect, or it changes on replay	var pick int	_ = workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {		return rand.Intn(len(user.Tips))	}).Get(&pick)	// no goroutines, channels, select, or ranging over a map	return workflow.ExecuteActivity(ctx, Charge, user.ID, user.Tips[pick]).Get(ctx, nil)}
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);    }}

Shipping a change under running workflows

Temporal branches on a version marker, kept for as long as old executions live. A Qoqi instance moves to new code at a stage, its state checked against flow.lock.

Temporal · version.golang17 lines
func Expense(ctx workflow.Context, exp Expense) (string, error) {	// ... notify, wait for the answer ...	v := workflow.GetVersion(ctx, "notify-accounting", workflow.DefaultVersion, 1)	if v != workflow.DefaultVersion {		err := workflow.ExecuteActivity(ctx, NotifyAccounting, exp.ID).Get(ctx, nil)		if err != nil {			return "", err		}	}	// Keep this branch until every execution started before it	// has left retention. One GetVersion per change, for good.	var payout string	err := workflow.ExecuteActivity(ctx, Pay, exp).Get(ctx, &payout)	return payout, err}
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

TemporalQoqi Flow
Workflow codeMust replay identically: no time.Now, rand, goroutines, map rangesPlain code: the clock, chance, any order
A call outside a stepAllowed; runs again on every replayRefused at compile time (F0001)
Waiting for a message or a deadlineSelector, signal channel, timer, cancelwait match
New code under running workflowsGetVersion branches kept for good, or old workers kept until runs endInstances move at a stage, checked against flow.lock
A step that must never run twiceActivities run at least onceSettled<T>: never run again; the flow is told Unknown and decides
Long-lived workflowsEvent history capped at 51,200 events or 50 MB: Continue-As-NewNo event history to replay
Crash testingWrite it yourselfeveryCrash: the test again, stopped after every write, same ending required
Steps in your own servicesAn SDK in the worker's languageAny server that speaks the protocol: a signed HTTP POST. SDKs for Go, PHP, TypeScript
To run itTemporal Service, a database, Elasticsearch, your workersOne binary

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.

How do Temporal's ideas map?

Workflow: impl Flow. Activity: step fn. Signal: an Inbox field. Timer: wait time.after(...). Selector: wait match. Patching: stage, and migrate fn when the state changes shape. Worker: qoqi serve.

Do I have to rewrite my services in Qoqi?

No. Only the flow is Qoqi. A step that needs your code stays in your service: any server that answers Qoqi's protocol, a signed HTTP POST. qoqi gen writes the typed interfaces for Go, PHP and TypeScript; any other language implements the protocol in a page of code.

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