Queue and Job Discovery
Queue and Job Discovery
The package keeps two discovery lists: the queues it has seen jobs run on, and the job classes it has seen run on each queue. Several calculations iterate over these lists instead of scanning the whole keyspace. This page covers how entries get onto the lists, which features read them, and how an entry for a job class or queue you no longer use ages out.
How Entries Are Recorded
Discovery is push-based. Every time a worker starts a job, the JobProcessing
listener calls JobMetricsRepository::recordStart(), which marks two entries as
seen:
- the queue, as
{connection}:{queue}(for exampleredis:emails) - the job class on that queue, as
{connection}:{queue}:{jobClass}, with each segment escaped as described in Escaped Segments (for exampleredis:reports~cnightly:App\Jobs\Report)
You can register an entry yourself, for example to list a queue before its first job has run:
use Cbox\LaravelQueueMetrics\Repositories\Contracts\JobMetricsRepository;
use Cbox\LaravelQueueMetrics\Repositories\Contracts\QueueMetricsRepository;
app(QueueMetricsRepository::class)->markQueueDiscovered('redis', 'emails');
app(JobMetricsRepository::class)->markJobDiscovered('redis', 'emails', App\Jobs\SendEmail::class);
An entry registered this way ages out like any other: register it again within the retention period to keep it.
Dispatching a job does not mark anything as seen; only a job starting does. A queue that receives jobs but has no worker running them for longer than the retention period drops out of discovery until a worker starts one of its jobs.
What Reads Discovery
| Reader | Uses |
|---|---|
QueueMetricsRepository::listQueues() |
the queue list |
JobMetricsRepository::listJobs() |
the job class list |
queue-metrics:calculate (CalculateQueueMetricsAction) |
the queue list to choose which queues to calculate, the job class list to aggregate each queue |
queue-metrics:calculate-baselines (CalculateBaselinesAction) |
the queue list |
| Adaptive baseline scheduling | the queue list, to decide whether the baseline command can skip a run |
GET /queue-metrics/jobs/{jobClass} (getAggregatedJobMetrics()) |
the job class list, to find every queue the class runs on |
QueueMetrics::getAllQueuesWithMetrics() and QueueMetrics::getAllJobsWithMetrics()
do not read discovery. They find queues and job classes by scanning the
per-job-class jobs: and queued: metric keys, which expire on the raw TTL
(1 hour by default) once the job class stops running.
Retention and Ageing
Each entry carries the time it was last seen, and is dropped once it has not
been seen for queue-metrics.storage.ttl.aggregated seconds (7 days by
default). Entries age out individually: a job class that was renamed or deleted,
or a queue nothing dispatches to any more, drops off the lists after the
retention period no matter how busy the rest of the application is.
// config/queue-metrics.php
'storage' => [
'ttl' => [
// Also how long a queue or job class stays discovered
// after it last ran.
'aggregated' => 604800, // 7 days
],
],
The boundary is the same on both drivers: an entry last seen exactly one retention period ago has expired. Seeing an entry again resets its clock.
A shorter aggregated TTL lets a retired queue or job class drop out sooner. But
a job that runs less often than the TTL then drops out between runs, and is only
listed again when it next starts.
Redis Driver
Since v3.5.0 each list is a sorted set scored by the Unix time the entry was last seen:
| Key | Type | Members |
|---|---|---|
{prefix}:discovery:queues:seen |
sorted set | {connection}:{queue} |
{prefix}:discovery:jobs:seen |
sorted set | {connection}:{queue}:{jobClass} (escaped) |
{prefix} is queue-metrics.storage.prefix (queue_metrics by default), and
Laravel's Redis connection prefix comes before it on the server.
- On a job start the entry is added with
ZADDand the current time as its score. This replaces theSADDearlier versions ran, inside the same transaction, so a job start sends the same number of commands as before. The key also gets theaggregatedTTL withEXPIRE. That is only a safety net: if nothing has been seen for a whole retention period, every entry has expired anyway. - On a read entries last seen one retention period ago or earlier are
removed with
ZREMRANGEBYSCORE, then the rest are read withZRANGE. Expired entries are removed only when something reads the list, such as the scheduledqueue-metrics:calculate. Reading therefore writes, soqueue-metrics.storage.connectionmust point at a writable Redis, not a read-only replica.
Every command touches a single key, so discovery works on a Redis Cluster where the keys hash to different slots.
To see what is discovered and when each entry was last seen:
redis-cli ZRANGE laravel_database_queue_metrics:discovery:queues:seen 0 -1 WITHSCORES
redis-cli ZRANGE laravel_database_queue_metrics:discovery:jobs:seen 0 -1 WITHSCORES
Replace laravel_database_ with your connection's prefix (database.redis.options.prefix).
Database Driver
The database driver stores discovery entries as rows in the
queue_metrics_sets table (with the default prefix) under the keys discovery:queues and
discovery:jobs. Since v3.4.2 each row has its own expires_at, which every
job start pushes forward by the aggregated TTL. Expired rows are hidden from
reads and removed by queue-metrics:cleanup-database.
Upgrading to v3.5.0
Before v3.5.0 the Redis driver kept discovery in plain sets,
{prefix}:discovery:queues and {prefix}:discovery:jobs. Every job start
refreshed the TTL of the whole set, so an entry never expired while any other
job ran. The sorted sets use new keys because a key that holds a set cannot hold
a sorted set; Redis rejects sorted-set commands on it with WRONGTYPE.
No action is needed to upgrade. During a rolling deploy:
- Workers on v3.5.0 write only the new sorted sets. They never write the old plain sets.
- Readers on v3.5.0 read the new sorted sets and merge the old plain sets, so entries that workers still on v3.4 write are listed too. Nothing is lost while old and new workers run side by side.
- Readers still on v3.4, such as a scheduler host upgraded last, read only
the old plain sets. Until they are upgraded they do not see a queue or job
class that has only run on v3.5.0 workers. Upgrade the host that runs
queue-metrics:calculatetogether with, or before, the workers. The old sets are read only if the key holds a plain set, so a key of any other type is ignored rather than causing an error. - The old plain sets expire on their own. Only v3.4 workers refresh their
TTL, so each one disappears at most one
aggregatedTTL (7 days by default) after the last v3.4 worker started its last job. Until then, an entry that exists only in an old set is still listed, as it was in v3.4. Versions before v2.0 wrote the old sets without a TTL; since v3.5.1 a reader gives such a set theaggregatedTTL the first time it reads it, so it expires too. - Job class entries written before v3.4.1, which cannot be read back with the right queue name, are removed from both the old set and the sorted set the first time they are read, as in v3.4.2.
To drop the old sets right away once every worker runs v3.5.0, delete them:
redis-cli DEL laravel_database_queue_metrics:discovery:queues laravel_database_queue_metrics:discovery:jobs
That drops entries which exist only in the old sets, typically queues and job classes that have not run since the upgrade. Each one is listed again the next time one of its jobs starts.
On a Redis Cluster the two keys may sit on different slots, so delete them one at a time.
Rolling back
v3.4 reads only the old plain sets, which v3.5.0 does not write. After a rollback to v3.4, discovery lists only what is still in those sets until each queue and job class runs again on v3.4 and is re-registered. Snapshots and baselines for a queue that is missing in the meantime are not calculated until it reappears.