Skip to content

Monitoring ​

Ratchet provides two monitoring channels: Micrometer metrics for quantitative dashboards and alerts, and the event system for real-time programmatic observation.

Micrometer integration ​

Setup ​

Add the Micrometer module:

xml
<dependency>
  <groupId>run.ratchet</groupId>
  <artifactId>ratchet-micrometer</artifactId>
  <version>0.5.0</version>
</dependency>

ratchet-micrometer ships a default SimpleMeterRegistry, so a registry is always present. To send metrics to a real backend such as Prometheus, override the default with an @Alternative producer:

java
@Produces
@Alternative
@Priority(2000)
@Singleton // @Singleton avoids a Weld proxy on the abstract MeterRegistry (WELD-001435)
public MeterRegistry meterRegistry() {
    return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}

The MicrometerMetricsCollector is annotated @Alternative @Priority(1000), so it automatically overrides the default NoOpMetricsCollector when the module is on the classpath.

You can install ratchet-otel beside ratchet-micrometer. In that combination, OpenTelemetry's priority-1100 adapter handles tracing while Micrometer continues to publish metrics. If both modules are present, Ratchet deliberately chooses direct OpenTelemetry tracing instead of the Micrometer tracing bridge. A custom tracing collector with a priority above 1100 overrides both.

Published metrics ​

MetricTypeTagsDescription
ratchet.jobs.startedCountertype, priorityJob execution attempts started
ratchet.jobs.completedCountertypeJob execution attempts completed successfully
ratchet.jobs.failedCountertype, familyFailed attempts, grouped by bounded exception family
ratchet.jobs.durationTimertypeWall-clock duration of successful attempts
ratchet.store.finalization.retriesCountertypeTransient conflicts while persisting a successful result
ratchet.store.finalization.minimal_successCountertypeFull-result persistence was exhausted and Ratchet used the minimal success write
ratchet.store.finalization.stuckCountertypeBoth finalization paths were exhausted; the job remains RUNNING for recovery
ratchet.store.claim.transient_failuresCounterexecution_typeTransient store conflicts on the claim path
ratchet.poller.claimed.jobsCounterexecution_typeNumber of jobs claimed
ratchet.submission.gate.rejectionsCounterexecution_type, gate_statusClaimed jobs blocked by a local submission gate
ratchet.wakeup.localCountersourceDirect local poller wakeups after new work
ratchet.execution.target.fallbackCounterrequested_target, effective_targetRequested executor target was unavailable and Ratchet used a fallback pool
ratchet.wakeup.cluster.publishCountertransport, outcomeCluster wakeup publish attempts
ratchet.wakeup.cluster.receiveCountertransport, outcomeCluster wakeup messages observed by a receiver
ratchet.callbacks.failedCountertype, familyonSuccess or onFailure callback failures; these do not fail the parent job
ratchet.signal.waitingCountertype, signal_keyJobs created in WAITING for a signal
ratchet.signal.deliveredCountertype, signal_key, outcomeSignal deliveries that moved a job to PENDING
ratchet.signal.timed_outCountertype, signal_keySignal waits that expired
ratchet.signal.cancelledCountertype, signal_keySignal waits cancelled before delivery
ratchet.store.operationTimerstore, operation, outcomeTimed store operations on claim and execution hot paths
ratchet.poller.breaker.stateGaugebreakerPoller claim-breaker state: 0 closed/unknown, 1 half-open, 2 open
ratchet.circuit.breaker.stateGaugeservice, profileApplication circuit-breaker state: 0 closed/unknown, 1 half-open, 2 open
ratchet.queue.jobsGaugestatusCurrent jobs by status: PENDING, RUNNING, WAITING, PAUSED, FAILED, SUCCEEDED, CANCELED
ratchet.queue.readyGauge—Pending jobs whose scheduled run time has passed
ratchet.queue.stuckGauge—Running jobs whose pickup timestamp is more than five minutes old
ratchet.queue.pending.typeGaugetypePending jobs by job type; zero when a type has no pending jobs
ratchet.queue.pending.priorityGaugepriorityPending jobs by priority; zero when a priority has no pending jobs
ratchet.queue.wait.p95Gauge—95th-percentile queue wait time in milliseconds
ratchet.queue.oldest.pending.ageGauge—Seconds since the oldest pending job's scheduled run time; zero when none exists or that time is in the future
ratchet.encryption.integrity.violationsCountersurfaceA row marked as encrypted contained plaintext. The read succeeds, but the writer or rollout needs investigation.
ratchet.encryption.envelope.version_skewCounterversion_gapA newer envelope reached this node. next, multiple_versions_ahead, and not_newer keep the diagnostic bounded; Ratchet releases valid newer jobs for an upgraded peer.

Queue-health gauges read JobQueryService.getQueueHealth(), which runs several aggregate store queries. The snapshot is cached for 15 seconds per node, and values are zero when the store lacks the JobAnalyticsStore capability. A failed refresh keeps the previous snapshot and waits 15 seconds before retrying; values are NaN until the first successful fetch. The oldest pending age is calculated at each read. To disable these gauges and their queries, add this filter before the scheduler starts:

java
registry.config().meterFilter(
    MeterFilter.deny(id -> id.getName().startsWith("ratchet.queue")));

The type tag corresponds to JobType (SINGLE, RECURRING, BATCH, CHAIN, WORKFLOW, SYSTEM). The priority tag corresponds to JobPriority (LOWEST, LOW, NORMAL, HIGH, CRITICAL). The family tag corresponds to ExceptionFamily, a coarse classification of the failure cause rather than the raw exception class name.

MicrometerMetricTagPolicy keeps string-valued tags bounded. Framework values pass through, blanks become UNKNOWN, and unrecognized values become OTHER. Signal keys are application strings and collapse to OTHER unless you explicitly allowlist selected keys. See Metrics Collection for the tag-policy details.

Grafana dashboard queries ​

Job throughput (Prometheus):

text
rate(ratchet_jobs_completed_total[5m])

Failure rate:

text
rate(ratchet_jobs_failed_total[5m])
  / (rate(ratchet_jobs_completed_total[5m]) + rate(ratchet_jobs_failed_total[5m]))

P95 execution time:

text
histogram_quantile(0.95, rate(ratchet_jobs_duration_seconds_bucket[5m]))

Failures by exception family:

text
topk(5, sum by (family) (rate(ratchet_jobs_failed_total[5m])))

Custom MetricsCollector ​

If you need different metric names, additional tags, or a non-Micrometer backend, implement the MetricsCollector SPI. Its callbacks cover job outcomes, success finalization, claims and gates, wakeups and executor routing, callback and signal events, store timing, poller and application circuit breakers, and encryption integrity/version signals.

The example below is intentionally partial: it records two basic job outcomes and drops every other signal. Extending NoOpMetricsCollector is convenient when that is what you want. For a production replacement, review every callback in Metrics Collection and either export it or delegate it to a complete collector.

java
public class MyMetricsCollector extends NoOpMetricsCollector {

    @Override
    public void jobStarted(UUID jobId, JobType type, JobPriority priority) {
        // record your metric
    }

    @Override
    public void jobFailed(UUID jobId, JobType type, Throwable cause, int attempt) {
        // record your metric
    }
}

Register your implementation as a CDI alternative with a higher priority than 1000 to override the Micrometer collector.

Event-based monitoring ​

The event system provides fine-grained lifecycle notifications. Unlike metrics (which are aggregated counters/timers), events carry full context about individual job executions.

CDI observers ​

java
@ApplicationScoped
public class JobMonitor {

    public void onStarted(@Observes JobStartedEvent event) {
        log.info("Job {} started on node {}", event.getJobId(), event.getNodeId());
    }

    public void onCompleted(@Observes JobCompletedEvent event) {
        log.info("Job {} completed in {}ms",
            event.getJobId(), event.getExecutionTimeMs());
    }

    public void onFailed(@Observes JobFailedEvent event) {
        log.error("Job {} failed (attempt {}): {}",
            event.getJobId(), event.getRetryAttempt(), event.getErrorMessage());
    }

    public void onRetrying(@Observes JobRetryingEvent event) {
        log.warn("Job {} retrying (attempt {}): {}",
            event.getJobId(), event.getRetryAttempt(), event.getErrorMessage());
    }

    public void onExecutionTimeout(@Observes JobExecutionTimedOutEvent event) {
        // Dispatch external alerts on an application-managed executor: observers are synchronous.
        timeoutAlertExecutor.execute(() -> alertOps(
            "Job " + event.getJobId() + " exceeded " + event.getExecutionTimeout()));
    }

    public void onDlq(@Observes JobDlqEvent event) {
        // Alert: job exhausted all retries
        alertOps("Job " + event.getJobId() + " moved to DLQ: " + event.getErrorMessage());
    }
}

Programmatic listeners ​

For dynamic registration (useful in frameworks or libraries that can't use CDI observers):

java
scheduler.addEventListener(event -> {
    if (event instanceof JobFailedEvent failed) {
        log.error("Job {} failed (attempt {}): {}",
            failed.getJobId(), failed.getRetryAttempt(), failed.getErrorMessage());
    }
});

For aggregate poll-cycle and throughput metrics, use the MetricsCollector SPI (or the Micrometer module) rather than the event system. Events carry per-job context, not cycle-level summaries.

Event types ​

EventWhen fired
JobStartedEventWorker begins executing a job
JobCompletedEventJob finished successfully
JobFailedEventJob reached terminal FAILED state
JobRetryingEventJob failed but will be retried
JobDlqEventJob entered terminal dead-letter/FAILED handling
JobExecutionTimedOutEventRunning job exceeded its configured execution timeout
JobCancelledEventJob was cancelled via API
BatchCompletingEventAll children in a batch have finished
ChainStartedEventA chained job was triggered by its parent
WorkflowBranchTriggeredEventA conditional branch was activated

Health checks ​

Node heartbeat check ​

Query the scheduler_node table to verify all expected nodes are alive (MySQL syntax):

sql
SELECT node_id, heartbeat_ts,
       TIMESTAMPDIFF(SECOND, heartbeat_ts, NOW()) AS seconds_stale
FROM scheduler_node
WHERE heartbeat_ts < NOW() - INTERVAL 2 MINUTE;

Any rows returned indicate stale nodes. For programmatic health checks:

java
@ApplicationScoped
public class RatchetHealthCheck {

    @Inject
    NodeStore nodeStore;

    public boolean isHealthy() {
        Instant cutoff = Instant.now().minus(Duration.ofMinutes(2));
        return nodeStore.findInactiveNodesSince(cutoff).isEmpty();
    }
}

Queue depth check ​

Monitor the number of pending jobs to detect backlog:

sql
-- PENDING is live state on scheduler_job_queue (priority is denormalized there).
SELECT priority, COUNT(*) as pending_count
FROM scheduler_job_queue
WHERE status = 'PENDING'
GROUP BY priority;

Alert if CRITICAL or HIGH jobs have been pending for more than a few seconds.

DLQ check ​

Jobs in the dead-letter queue need human attention:

sql
SELECT COUNT(*)
FROM scheduler_job
WHERE terminal_status = 'FAILED';

Wire this into your alerting system. The durable scheduler_job row remains the source of truth after its live scheduler_job_queue row is removed.

Alerting recommendations ​

ConditionSeverityAction
DLQ count > 0CriticalInvestigate and retry or archive
Failure rate > 5% (5min window)WarningCheck logs for systematic errors
P95 duration > 2x normalWarningCheck for resource contention or slow dependencies
Node heartbeat stale > 2minCriticalNode may be down; check process health
Pending CRITICAL jobs > 30sCriticalCluster may be overloaded
Pending queue depth growingWarningScale up nodes or increase thread pool
ratchet.queue.stuck > 0WarningCheck long-running jobs and worker health
ratchet.queue.oldest.pending.age keeps risingWarningCheck poller health, available workers, and queue capacity
ratchet.store.finalization.stuck > 0CriticalCheck store health; successful work is waiting for recovery
ratchet.poller.breaker.state remains 2CriticalInvestigate claim-path store failures
ratchet.encryption.integrity.violations > 0CriticalInvestigate a downgrade, lagging writer, or encryption bug
ratchet.encryption.envelope.version_skew persists after rolloutWarningFind and upgrade the lagging node

See also ​