Skip to main content

ACD Worker

The ACD Worker is a Java 21 / Vert.x 5 service that runs the state of Call Queues. It keeps the real-time queue state in Redis. Laravel owns everything else: the voice responses (CXML), persistence in MySQL, and all APIs.

Purpose​

The worker handles:

  • FIFO order per queue (enqueue, poll, dequeue, requeue)
  • Agent states: AVAILABLE, BUSY, WRAP_UP, LOGGED_OUT
  • Agent selection strategies: ring_all, round_robin, least_talk_time, fewest_calls
  • Overflow when a caller waits longer than the queue's max wait
  • Live queue snapshots for the queue dashboards

How It Fits the Call Flow​

  1. An inbound call routes to a queue. Laravel enqueues the caller in the worker and answers with hold CXML (music on hold, or an announcement and a pause).
  2. Cloudonix calls Laravel's /api/callbacks/voice/queue-poll after each hold segment.
  3. Laravel asks the worker for a decision: wait (with the position), dial (with the selected agents) or overflow.
  4. On dial, Laravel returns <Dial> CXML to the agents' extensions directly, never to follow-me chains. Agents whose extension is already on a call are skipped (see Presence below).
  5. Dial results, session updates and CDRs become lifecycle events (answered, ended, abandoned, overflow, dial_failed, requeue) that Laravel sends to the worker, so agent states and queue positions stay accurate.
Hard rule

Queue offers always dial the agent's extension directly. Follow-me / forward chaining is never used for queue calls.

Agent States​

StateMeaningOffered calls
AVAILABLELogged in and freeYes
BUSYOn a queue callNo
WRAP_UPAfter a call, or pausedNo
LOGGED_OUTNot in the queue (no agent record in Redis)No
  • After a queue call ends, a BUSY agent goes to WRAP_UP for the queue's wrap-up time, then back to AVAILABLE. Wrap-up expiry is checked lazily, on the next poll or live snapshot.
  • A paused agent is WRAP_UP with no end time (sticky) until the state is changed again. Laravel sets it when the agent state API (POST /api/v1/call-queues/{id}/agents/me/state) receives wrap_up. The *45 feature code and the queue widget toggle only between AVAILABLE and LOGGED_OUT. A call answered by an agent who paused while it rang is still counted, and the agent stays paused.
  • Logging in seeds the agent's all-time talk time and call count from MySQL, so least_talk_time and fewest_calls stay correct after a worker restart.

Consistency Rules​

  • Atomic transitions. Agent state changes on call events run as Lua scripts in Redis and are conditional: an event never recreates a logged-out agent and never overrides a pause.
  • Idempotent events. Each lifecycle event carries an event ID, remembered for 24 hours, and replays from Laravel (its retry window is about 6.5 minutes) are ignored.
  • Requeue keeps the place. If the agent who answered never connects, a requeue event releases the agent and puts the call back at its original position with its original enqueue time. A requeued call already past the max wait overflows on its next poll.
  • Tombstones. A dequeued call leaves a 10-minute marker, so a late poll cannot add it back.
  • Recovery. A poll for a call the worker does not know (for example after a restart) enqueues it again.

Presence​

The worker only sees queue calls. Laravel tracks whether each extension is on any call (for example a direct call) from Cloudonix session updates, in the Redis key acd:presence:{organization_id}:{extension}. Out-of-order updates are ignored, and the key expires after an hour as a guard against missed hang-ups. An agent whose extension is busy is skipped when an offer is made.

Redis Keys​

KeyTypeContent
acd:queue:{org}:{queue}listCall IDs in FIFO order
acd:call:{org}:{queue}:{callId}hashqueuedAt (epoch ms)
acd:agent:{org}:{queue}:{userId}hashstate, wrapUpUntil, talkSecondsTotal, callsHandledTotal
acd:rr:{org}:{queue}stringRound-robin cursor (the user offered last)
acd:done:{org}:{queue}:{callId}stringDequeued-call tombstone (10 minutes)
acd:evt:{org}:{eventId}stringSeen lifecycle event (dedupe)
acd:presence:{org}:{extension}stringExtension presence (written by Laravel)

API​

All routes except health require Authorization: Bearer {ACD_WORKER_API_TOKEN}. Request bodies are read only after authentication and are capped at 64 KB.

MethodPathPurpose
POST/queue/enqueueAdd a call to a queue
POST/queue/pollGet a wait, dial or overflow decision
POST/queue/liveLive snapshot (callers and agent states)
POST/eventsCall lifecycle event
POST/agents/stateSet an agent's state
GET/health (port 8085)200 while Redis answers a ping, 503 otherwise

Configuration​

VariableDefaultDescription
ACD_WORKER_URLhttp://acd-worker:8084Base URL Laravel uses to reach the worker
ACD_WORKER_API_TOKEN—Bearer token for Laravel → worker requests. Required: the worker refuses to start when it is empty or a shipped placeholder, and Laravel refuses to boot in production without it
ACD_HTTP_PORT8084API port
ACD_HEALTH_PORT8085Health port
ACD_REDIS_HOST / ACD_REDIS_PORTredis / 6379Redis server
ACD_REDIS_PASSWORD—Redis password. The compose file passes REDIS_PASSWORD. It is given to the client directly, never in a URI
ACD_LOG_LEVELinfoLog level

Laravel talks to the worker fail-soft: if the worker is unreachable, queue calls degrade instead of failing hard, and lost ended, abandoned and dial_failed events are replayed from the Laravel queue.

Container​

  • Image: eclipse-temurin:21-jre, runs as a non-root user (uid 10001).
  • No published ports; Laravel reaches it on the internal network.
  • Docker healthcheck: GET http://localhost:8085/health every 30 seconds. Because health fails when Redis is unreachable, a worker that lost Redis is marked unhealthy. Plain Docker Compose does not restart an unhealthy container by itself (restart: unless-stopped acts only when the process exits); restart it with docker compose restart acd-worker or use an orchestrator that acts on health.
  • The Platform Status page probes the same address every minute (PLATFORM_STATUS_ACD_URL, default http://acd-worker:8085/health).

Common Commands​

# Build and test (requires JDK 21; or build inside Docker)
docker run --rm -v "$PWD/acd-worker":/build -w /build maven:3.9-eclipse-temurin-21 mvn -B test

# View logs
docker compose logs -f acd-worker

# Restart
docker compose restart acd-worker
tip

With the centralized logging stack enabled, worker events (offers, answers, wrap-up transitions) are searchable by Cloudonix session token.