The design session in casehubio/parent that produced the Desired State Management research
also surfaced the need for a proper workers collection. Claudony and OpenClaw are
integration platforms — they own their repos. But thin SPI implementations like HTTP
endpoints, Camel routes, and shell scripts belong together. Seven review rounds later,
the Camel worker spec was approved and I had a clear picture of what to build.
The implementation splits into workers-common (shared async infrastructure) and
workers-camel (the Camel adapter). The split exists because every future worker type —
HTTP, Script, MCP, K8s Job — needs the same completion registry, callback endpoint, and
event bus publishing. Building it into the Camel module would mean extracting it later.
The interesting design decision was fault bus isolation. The engine’s
QuartzWorkerExecutionJobListener handles WORKFLOW_EXECUTION_FAILED with no worker-type
filter — it processes everything on that address. If Camel fires on the same address, two
things break: Quartz schedules a Quartz job for a Camel worker (wrong execution type), and
the failure count increments twice per fault (one write per handler), throwing the retry
comparison off by one.
The fix: Camel faults fire on CAMEL_WORKER_FAULT, a completely separate event bus address.
CamelWorkerFaultEventHandler handles it with retry logic that mirrors the Quartz
implementation exactly — same failureCount < maxAttempts comparison (strict less-than),
same default policy (3 attempts, 10s fixed), same backoff computation. Future worker types
will get their own fault addresses. The workerType discriminator on PendingCompletion
extends this pattern to CDI async events — CompletionExpiredEvent and FaultCallbackEvent
are broadcast to all observers, but each observer filters by worker type before acting.
The retry path after a Vert.x timer had a threading trap. The timer fires on the event
loop. runSubscriptionOn moves the subscription lambda to the worker pool, but item
delivery still happens on whoever calls emitter.complete() — the timer callback, which
is on the event loop. emitOn intercepts between emitter and downstream and re-dispatches
to the pool. The same issue appeared in submitAsync — producerTemplate.send() is a
blocking Camel call, and wrapping it in .invoke() without runSubscriptionOn would block
the event loop thread.
workers-common provides AsyncWorkerCompletionRegistry (ConcurrentHashMap-based, with
@Scheduled expiry), WorkflowCompletionPublisher (fires on WORKER_EXECUTION_FINISHED
via eventBus.publish(), not request() — two consumers need it), WorkerCallbackResource
(POST /workers/complete/{dispatchId} with constant-time token validation), and the record
types that carry dispatch context through the async completion lifecycle.
workers-camel implements both engine SPIs — ReactiveWorkerProvisioner for capability
probe, WorkerExecutionManager for dispatch. CamelCapabilityResolver does three-tier
resolution: CDI-registered CamelWorkerRoute beans first, then config properties, then
convention (route ID = capability tag with from: direct:{tag}). The casehub:complete
Camel component lets routes signal completion from within the Camel route itself.
The co-deployment constraint is documented but not solved: workers-camel and
scheduler-quartz on the same classpath causes CDI ambiguity on WorkerExecutionManager.
A composite routing manager in the engine is the right answer. That’s a separate issue.
47 tests across both modules. The POM structure follows the casehub family pattern —
casehub-parent as parent, Quarkus Platform Camel BOM imported alongside the core BOM,
Jandex indexes on both library modules.