Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Combining modules

Module constraints can be used together in one model. The job workload combines a finite-state lifecycle with bounded resource usage.

This example combines an FSM, event attributes, and measured resource usage. A worker publishes its thread limit. A job records how many threads it requests and how many it occupies while running.

YAML model

quent: alpha
model: job_workload

entities:
  Worker:
    # Generates WorkerUsage and WorkerBounds.
    resource:
      threads:
        kind: occupancy
        known-bounds: true
    events:
      ready:
        attributes:
          name: string
          limits: { sets-resource-bounds: true }

fsms:
  Job:
    states:
      queued:
        initial: true
        attributes:
          name: string
          requested_threads: u64
        to: [running]
      running:
        attributes:
          worker: { uses: Worker }
        to: [completed]
      completed: {}

Instrumentation API

The generated API distinguishes the worker’s WorkerBounds from the job’s WorkerUsage. No event names or payload keys are assembled at runtime.

use instrumentation::{Context, Job, JobWorkload, Noop, Worker, WorkerBounds, WorkerUsage};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<JobWorkload>::try_new(Noop)?;

    let mut worker = context.observer::<Worker>().handle();
    worker.ready("worker-1".to_owned(), WorkerBounds { threads: 16 })?;

    let _job = context
        .observer::<Job>()
        .handle()
        .queued("compile".to_owned(), 4)
        .running(worker.as_entity_ref_with(WorkerUsage { threads: 4 }))
        .completed();

    Ok(())
}
#include "quent-tutorial-job-workload-cpp-bridge/gen/quent.hpp"

int main() {
  auto context = quent::Context::none();
  auto worker = context.worker_observer()->handle();
  worker.ready(quent::worker::Ready{
      .name = "worker-1",
      .limits = quent::records::WorkerBounds{.threads = 16},
  });

  auto queued = context.job_observer()->handle().queued(quent::job::Queued{
      .name = "compile",
      .requested_threads = 4,
  });
  auto running = std::move(queued).running(quent::job::Running{
      .worker = quent::refs::WorkerUsageRef{
          .target = worker.id(),
          .data = quent::records::WorkerUsage{.threads = 4},
      },
  });
  auto completed = std::move(running).completed();
  return 0;
}
import quent_tutorial_job_workload as quent


def main() -> None:
    with quent.Context() as context:
        worker = context.worker_observer().handle()
        worker.ready(name="worker-1", limits={"threads": 16})

        job = context.job_observer().handle()
        queued = job.queued(name="compile", requested_threads=4)
        running = queued.running(
            worker={
                "target": worker,
                "data": {"threads": 4},
            }
        )
        completed = running.completed()


if __name__ == "__main__":
    main()
Key point

The job records its requested thread count and its usage of a bounded worker resource.

Check yourself

What does WorkerBounds { threads: 16 } represent?

Which call records where the job runs and how many threads it occupies?

Full code