/examples/fleet

Two binaries, one law

The smallest honest fleet: an intake binary and a worker binary that share one topic over a unix socket, plus the plan that names both instances, routes the topic between them, and states a claim across the pair. Each binary's build emits a topology artifact; `hale fleet check` composes the artifacts and verifies the plan against what the binaries actually are — not what a diagram says they are.

23 lines · verified by hale 0.19.2 at build time · download intake.hl

The first binary: an intake that queues three tasks onto `Work` and connects the topic to a unix socket. Its whole contract with the outside world is the topic declaration and the binding.

intake.hl
type Task { kind: Int; body: String; }
topic Work { payload: Task; }

locus Intake {
    params { queued: Int = 0; }
    bus { publish Work; }
    run() {
        let mut i = 0;
        while i < 3 {
            Work <- Task { kind: i % 2, body: "job" };
            self.queued = self.queued + 1;
            i = i + 1;
        }
        println("intake queued ", self.queued);
    }
}

main locus IntakeApp {
    params { in_: Intake = Intake { }; }
    bindings { Work: unix("/tmp/hale-fleet-demo.sock", role: connect); }
    run() { }
}
fn main() { IntakeApp { }; }

The second binary: a supervised executor listening on the same socket. The two programs share nothing but the `Work` payload shape and the wire — separate builds, separate processes, separate failures.

worker.hl
type Task { kind: Int; body: String; }
topic Work { payload: Task; }

@supervised locus Executor {
    params { done: Int = 0; }
    bus { subscribe Work as perform; }
    fn perform(t: Task) {
        self.done = self.done + 1;
        println("executor did ", t.body, " kind ", t.kind);
    }
}

main locus WorkerApp {
    params { ex: Executor = Executor { }; }
    bindings { Work: unix("/tmp/hale-fleet-demo.sock", role: listen); }
    on_failure(e: Executor, err: ClosureViolation) { restart (e); }
    run() { }
}
fn main() { WorkerApp { }; }

The fleet plan is deployment describing itself: exact instances, each pointing at the topology artifact its build produced, the route that connects them, and a fleet-level claim — every task must have a subscribed executor. `hale fleet check` composes the real artifacts and holds the plan to them:

plan.json
{
  "schema": "1.1",
  "name": "demo",
  "instances": [
    {
      "id": "intake-0",
      "artifact": "intake.topology.json"
    },
    {
      "id": "worker-0",
      "artifact": "worker.topology.json"
    }
  ],
  "routes": [
    {
      "id": "work",
      "publishers": [
        {
          "instance": "intake-0",
          "topic": "Work"
        }
      ],
      "subscribers": [
        {
          "instance": "worker-0",
          "topic": "Work"
        }
      ],
      "transport": "unix:///tmp/hale-fleet-demo.sock"
    }
  ],
  "groups": {
    "executors": {
      "instances": [
        "worker-0"
      ]
    }
  },
  "claims": [
    {
      "name": "every_task_has_a_worker",
      "require_subscribes": {
        "group": "executors",
        "subject": "Work"
      }
    }
  ]
}
$ hale check --dump-topology …  &&  hale fleet check plan.json
ok: fleet `demo` composed — 2 instance(s), 1 route(s), fleet_shape_hash 8839828fa54935a3

Now the reason this layer exists. Ship a worker build whose executor stopped subscribing — the binary still typechecks, both artifacts are individually valid, and only the composition can see the hole:

A worker build that quietly stopped listening. (worker.hl)
- bus { subscribe Work as perform; }
+ bus { }
$ hale fleet check plan.json
route `work`: instance `worker-0` is named as a subscriber of `Work` (subject `Work`), but nothing in that component subscribes it — declaring a topic is not using it