HLC Troubleshooting Runbook
Version: 1.2.0
Sprint: SPRINT_20260105_002_004_BE
Last Updated: 2026-05-31
Audience: On-call SREs and the Scheduler team troubleshooting Hybrid Logical Clock (HLC) based queue ordering in the Stella Ops JobEngine scheduler. This runbook covers chain-verification failures, clock skew, air-gap merge conflicts, and node-identity collisions.
Status note (reconciled 2026-05-31 against
src/JobEngineandsrc/__Libraries/StellaOps.HybridLogicalClock): HLC ordering is implemented as a library inStellaOps.Scheduler.Queue(seeHlcSchedulerOptionsinsrc/JobEngine/StellaOps.Scheduler.__Libraries/StellaOps.Scheduler.Queue/Options/HlcSchedulerOptions.csand the002_hlc_queue_chain.sqlmigration). However, several monitoring hooks this runbook originally assumed do not exist yet in code and are flagged inline below:
- The HLC queue is not wired into any running host. Neither DI entry point —
ServiceCollectionExtensions.AddHlcSchedulerQueue()(registers theServices/*implementations) norHlcSchedulerServiceCollectionExtensions.AddHlcSchedulerServices()(registers theHlc/*implementations) — is called anywhere outside the library itself and its test project. The Scheduler WebService (StellaOps.Scheduler.WebService/Program.cs) does not register HLC ordering, andEnableHlcOrderingdefaults tofalse. Treat this entire runbook as forward-looking / roadmap for a feature whose enqueue/dequeue/verify code paths are not exercised at runtime in the shipped Scheduler. The schema (migration002_hlc_queue_chain.sql) and the core HLC library are present.- No Prometheus alert rules are shipped for any of the
Hlc*alert names referenced here (no rule appears underdevops/telemetry/). Treat alert names as descriptive labels for the condition, not as wired alerts.- No
/health/hlcor/configHTTP endpoint exists. The Scheduler WebService maps only/healthzand/readyz(plus a build-info endpoint and the domain job/schedule/run endpoints). There is no/health/livenessroute on the Scheduler service.- The HLC/scheduler metric instruments below are declared but never recorded to: no code path calls the
RecordEnqueue/RecordDequeue/RecordChainVerification/RecordSnapshotCreated(etc.) methods, so the counters/histograms emit nothing at runtime even when the feature would be enabled. Of the air-gap sync metrics, onlyairgap_bundle_size_byteshas a live call site (RecordBundleSize); the rest (includingairgap_merge_conflicts_totalandairgap_sync_duration_seconds) are also declared-but-unrecorded. See the Metrics reference for the full declared-vs-emitted breakdown.- Several legacy metric names referenced by older docs (
hlc_clock_skew_rejections_total,hlc_physical_time_offset_seconds,hlc_ticks_total,scheduler_jobs_received_total) do not exist at all insrc/.
Table of Contents
- Chain Verification Failure
- Clock Skew Issues
- Time Offset Drift
- Merge Conflicts
- Slow Air-Gap Sync
- No Enqueues
- Batch Snapshot Failures
- Duplicate Node ID
1. Chain Verification Failure
Symptoms
- Condition label:
HlcChainVerificationFailure(NOT a shipped Prometheus alert — see status note) - Metric:
scheduler_chain_verification_failures_total(declared on theStellaOps.Scheduler.HlcQueuemeter but not recorded to by current code — see status note and the Metrics reference; do not expect this counter to move at runtime today) - Log (per-issue): the exact strings depend on which
SchedulerChainVerifieris wired — there are two implementations with different messages:Hlc/SchedulerChainVerifier(registered byAddHlcSchedulerServices):Stored link doesn't match computed. Stored=..., Computed=...(LinkMismatch) orExpected ..., got ...(PrevLinkMismatch), followed by the summary lineChain verification complete. TenantId=..., ..., IsValid=false, IssueCount=N.Services/SchedulerChainVerifier(registered byAddHlcSchedulerQueue):Stored link doesn't match computed link(LinkMismatch),PrevLink doesn't match previous entry's link(PrevLinkMismatch), plus anInvalidPayloadHash/InvalidLinkLength/HlcOrderViolationissue set the other does not raise; summary lineChain verification for tenant {TenantId}: VALID|INVALID, {N} entries, {M} issues.- This duplication is a known code smell, not a documented contract. Confirm which type the deployment registers before pattern-matching log strings.
Severity
Critical - Indicates potential data tampering or corruption.
Investigation Steps
Identify the affected chain segment:
SELECT job_id, t_hlc, encode(prev_link, 'hex') as prev_link, encode(link, 'hex') as link, created_at FROM scheduler.scheduler_log WHERE tenant_id = '<tenant_id>' ORDER BY t_hlc DESC LIMIT 100;Find the break point:
WITH chain AS ( SELECT job_id, t_hlc, prev_link, link, LAG(link) OVER (ORDER BY t_hlc) as expected_prev FROM scheduler.scheduler_log WHERE tenant_id = '<tenant_id>' ) SELECT * FROM chain WHERE prev_link IS DISTINCT FROM expected_prev ORDER BY t_hlc;Check for unauthorized modifications:
- Review database audit logs
- Check for direct SQL updates bypassing the application
Verify chain head consistency:
SELECT * FROM scheduler.chain_heads WHERE tenant_id = '<tenant_id>';
Resolution
If corruption is isolated:
- Mark affected jobs for re-processing
- Rebuild chain from the last valid point
- Update chain head
If tampering is suspected:
- Escalate to Security team immediately
- Preserve all logs and database state
- Initiate incident response procedure
2. Clock Skew Issues
Symptoms
- Condition label:
HlcClockSkewExceedsTolerance(NOT a shipped Prometheus alert — see status note) - Metric:
hlc_clock_skew_rejections_totalis NOT IMPLEMENTED. There is no dedicated skew-rejection counter; clock-skew rejection currently surfaces only as an error log and anHlcClockSkewExceptionthrown fromHybridLogicalClock.Receive(). - Log (from
StellaOps.HybridLogicalClock.HybridLogicalClock.Receive):Clock skew of {Skew} from node {RemoteNode} exceeds threshold {MaxSkew}(logged at Error;{Skew}/{MaxSkew}are formatted asTimeSpanvalues, not raw milliseconds)
Severity
Critical - Can cause job ordering inconsistencies.
Investigation Steps
Check NTP synchronization:
# On affected node timedatectl status ntpq -p chronyc tracking # if using chronyVerify time sources:
ntpq -pnCheck for leap second issues:
dmesg | grep -i leapCompare with other nodes:
for node in node-1 node-2 node-3; do echo "$node: $(ssh $node date +%s.%N)" done
Resolution
Restart NTP client:
sudo systemctl restart chronyd # or ntpdForce time sync:
sudo chronyc makestepTemporarily increase tolerance (emergency only):
Scheduler: HlcOrdering: MaxClockSkewMs: 60000 # Increase from default 1000 (max allowed: 60000)Config section is
Scheduler:HlcOrdering(constantHlcSchedulerOptions.SectionName), the key isMaxClockSkewMs(default 1000, range[0, 60000]). The skew threshold actually enforced at runtime is themaxClockSkewpassed when constructingHybridLogicalClock(library default 1 minute); confirm both are aligned for the deployment.Restart affected service to reset HLC state.
3. Time Offset Drift
Symptoms
- Condition label:
HlcPhysicalTimeOffset(NOT a shipped Prometheus alert — see status note) - Metric:
hlc_physical_time_offset_secondsis NOT IMPLEMENTED. No physical-time-offset gauge is emitted by current code (the archived design docdocs-archive/modules/scheduler/hlc-ordering.mdlists it ashlc_physical_offset_seconds, but neither name exists insrc/). Detect drift indirectly: a persistently high HLC logical counter (the trailing-NNNNNNsegment oft_hlc) indicates the wall clock is lagging and the logical counter is absorbing the gap.
Severity
Warning - May cause timestamp anomalies in diagnostics.
Investigation Steps
Inspect recent HLC timestamps directly (the logical counter is the last
--delimited segment oft_hlc):SELECT job_id, t_hlc, created_at FROM scheduler.scheduler_log WHERE tenant_id = '<tenant_id>' ORDER BY t_hlc DESC LIMIT 50;Review HLC state: there is no
/health/hlcendpoint. The Scheduler WebService maps only/healthzand/readyz(there is no/health/livenessroute on this service). Inspect thescheduler.scheduler_log/scheduler.chain_headstables (above and in §1) for current ordering state.Check for high logical counter: If the logical counter (the
t_hlctail segment) is very high, it indicates frequent same-millisecond events or a wall clock that is not advancing.
Resolution
Usually self-correcting as wall clock advances. If persistent:
- Review job submission rate
- Consider horizontal scaling to distribute load
4. Merge Conflicts
Symptoms
- Condition label:
HlcMergeConflictRateHigh(NOT a shipped Prometheus alert — see status note) - Metric:
airgap_merge_conflicts_totalis declared on theStellaOps.AirGap.Syncmeter (taggedconflict_type) but its recorder (RecordMergeConflict) currently has no call site, so the counter does not move at runtime today (see status note). When wired, tag value would beDuplicateTimestamporPayloadMismatch. - Conflict types are
DuplicateTimestampandPayloadMismatch(theConflictTypeenum inStellaOps.AirGap.Sync.Models.ConflictResolution)
Severity
Warning - May indicate duplicate job submissions or clock issues on offline nodes.
Investigation Steps
Identify conflict types (tag value is
DuplicateTimestamporPayloadMismatch):sum by (conflict_type) (airgap_merge_conflicts_total)Review merge logs:
grep "merge conflict" /var/log/stellaops/scheduler.log | tail -100Check offline node clocks:
- Were offline nodes synchronized before disconnection?
- How long were nodes offline?
Resolution
- For duplicate jobs: Use idempotency keys to prevent duplicates
- For payload conflicts: Review job submission logic
- For ordering conflicts: Verify NTP on all nodes before disconnection
5. Slow Air-Gap Sync
Symptoms
- Condition label:
HlcSyncDurationHigh(NOT a shipped Prometheus alert — see status note) - Metric:
airgap_sync_duration_seconds(histogram declared on theStellaOps.AirGap.Syncmeter) p95 > 30s — but its recorder (RecordSyncDuration) has no call site in current code, so no samples are produced today (see status note).airgap_bundle_size_bytes(used in the investigation step below) is recorded (RecordBundleSize) and is the one air-gap sync histogram with a live emission path.
Severity
Warning - Delays job processing.
Investigation Steps
Check bundle sizes:
histogram_quantile(0.95, airgap_bundle_size_bytes_bucket)Check database performance:
SELECT * FROM pg_stat_activity WHERE state = 'active' AND query LIKE '%scheduler_log%';Review index usage:
EXPLAIN ANALYZE SELECT * FROM scheduler.scheduler_log WHERE tenant_id = '<tenant>' ORDER BY t_hlc LIMIT 1000;
Resolution
- Chunk large bundles: Split bundles > 10K entries
- Optimize database: Ensure indexes are used
- Increase resources: Scale up database if needed
6. No Enqueues
Symptoms
- Condition label:
HlcEnqueueRateZero(NOT a shipped Prometheus alert — see status note) - No jobs appearing in HLC queue. Note:
scheduler_hlc_enqueues_totalis declared but never recorded (see status note), so it is always flat today regardless of activity — it cannot distinguish “no enqueues” from “feature not wired”. Verify enqueue activity by queryingscheduler.scheduler_logrow counts instead.
Severity
Info - May be expected or indicate misconfiguration.
Investigation Steps
Check if HLC ordering is enabled: there is no
/configHTTP endpoint. Confirm the effective configuration from the service’s boundScheduler:HlcOrderingsection (appsettings / environment overrides) — the relevant flag isEnableHlcOrdering(defaultfalse).Verify enqueue activity (there is no
scheduler_jobs_received_totalmetric, andscheduler_hlc_enqueues_totalis declared-but-unrecorded — see status note). Until the enqueue path records metrics, confirm activity directly from the table:SELECT COUNT(*) AS recent_enqueues FROM scheduler.scheduler_log WHERE tenant_id = '<tenant_id>' AND created_at > NOW() - INTERVAL '5 minutes';If/when the metric is wired, the equivalent PromQL would be
rate(scheduler_hlc_enqueues_total[5m]).Check for errors:
grep -i "hlc\|enqueue" /var/log/stellaops/scheduler.log | grep -i error
Resolution
If HLC should be enabled:
Scheduler: HlcOrdering: EnableHlcOrdering: trueIf dual-write mode is needed (the option is named
EnableDualWrite, notDualWriteMode):Scheduler: HlcOrdering: EnableDualWrite: trueMigration phases (per
HlcSchedulerOptionsand the queueMIGRATION_GUIDE.md): Phase 1EnableDualWrite=true, EnableHlcOrdering=false; Phase 2EnableDualWrite=true, EnableHlcOrdering=true; Phase 3EnableDualWrite=false, EnableHlcOrdering=true.
7. Batch Snapshot Failures
Symptoms
- Condition label:
HlcBatchSnapshotFailures(NOT a shipped Prometheus alert — see status note) - Missing DSSE-signed batch proofs. The
scheduler_batch_snapshots_totalcounter is declared-but-unrecorded (see status note), so it is always flat today; the reliable signal is the table itself — rows inscheduler.batch_snapshotwith NULLsigned_by/signaturewhen signing was expected. (DB constraintchk_signature_requires_signerenforces thatsigned_byandsignatureare either both NULL or both set, so an unsigned row has both NULL.)
Severity
Warning - Audit proofs may be incomplete.
Implementation note: Batch-snapshot signing is HMAC-based, not certificate-based, and there is no
stella signerCLI for it. Signing is performed in-process byBatchSnapshotDsseSignerand configured viaBatchSnapshotDsseOptions, which are bound from the configuration sectionScheduler:Queue:Hlc:DsseSigning(note: this is a different section fromScheduler:HlcOrderingthat holds the other HLC options, and onlyAddHlcSchedulerServicesWithDsseSigning(configuration)binds it). The options are:Mode("hmac"or"none", default"none"),SecretBase64(the HMAC secret, required whenMode="hmac"),KeyId(defaultscheduler-batch-snapshot), andPayloadType(defaultapplication/vnd.stellaops.scheduler.batch-snapshot+json). Signing is enabled iffModeequals"hmac"(case-insensitive). The MAC routes through the centralICryptoHmacabstraction, so the algorithm follows the active compliance profile (HMAC-SHA256 onworld/fips/kcmvp/eidas, HMAC-GOST3411 ongost, HMAC-SM3 onsm). Whether snapshots are taken/signed at all is gated byHlcSchedulerOptions.SignBatchSnapshotsandBatchSnapshotIntervalSeconds(inScheduler:HlcOrdering).
Investigation Steps
Check signing configuration: verify
Scheduler:Queue:Hlc:DsseSigning:Modeis"hmac"andScheduler:Queue:Hlc:DsseSigning:SecretBase64is set (a missing secret throwsInvalidOperationException: "HMAC signing mode requires SecretBase64 to be configured", and an invalid Base64 secret throwsInvalidOperationException: "SecretBase64 is not valid Base64"at sign time). There is nostella signer statuscommand for this signer.Verify the feature is enabled: there is no
/configHTTP endpoint. ConfirmScheduler:HlcOrdering:SignBatchSnapshots=trueand a non-zeroBatchSnapshotIntervalSecondsin the bound configuration.Check database connectivity:
SELECT 1; -- Simple connectivity test
Resolution
- Verify
SecretBase64is configured and is valid Base64 (an invalid value is logged and treated as a verification/signing failure). - Confirm the active compliance profile matches the algorithm used to produce earlier signatures (a profile switch changes the HMAC algorithm).
- Verify database permissions for the
scheduler.batch_snapshottable (note: the table is singular —scheduler.batch_snapshot, notbatch_snapshots).
8. Duplicate Node ID
Symptoms
- Condition label:
HlcDuplicateNodeId(NOT a shipped Prometheus alert — see status note) - Multiple instances with the same
NodeId
Severity
Critical - Will cause chain corruption.
Investigation Steps
Identify affected instances: the
hlc_ticks_totalmetric referenced previously is NOT IMPLEMENTED — no per-node tick counter is emitted. Detect collisions another way:- Compare the
NodeIdembedded int_hlc(physicalTime13-NodeId-counter6) across nodes by queryingscheduler.scheduler_log. Caveat: theNodeIdsegment may itself contain hyphens (the parser is^(\d{13})-(.+)-(\d{6})$, so e.g.scheduler-east-1is a valid NodeId), sosplit_part(t_hlc,'-',2)returns only the first token of a multi-hyphen NodeId. Recover the full NodeId by stripping the leading 13-digit physical time and trailing 6-digit counter instead:SELECT substring(t_hlc FROM 15 FOR length(t_hlc) - 21) AS node_id, -- drop 13-digit time + '-' prefix and '-' + 6-digit counter suffix COUNT(*) FROM scheduler.scheduler_log WHERE tenant_id = '<tenant_id>' GROUP BY 1 ORDER BY 2 DESC; - Cross-check the configured
NodeIdon each instance (step 2).NodeIddefaults toEnvironment.MachineNamewhen unset.
- Compare the
Check node ID configuration (option
Scheduler:HlcOrdering:NodeId):# On each instance, inspect the bound Scheduler:HlcOrdering section grep -ri "NodeId" /etc/stellaops/
Resolution
Immediate action required:
- Stop one of the duplicate instances
- Reconfigure with unique node ID
- Restart and verify
- Check chain integrity for affected time period
Metrics reference
The metric names below are declared by current code, with their owning meter. Important: declared ≠ emitted. A meter instrument only produces samples when something calls its Record* method. As of this reconciliation, almost none of the HLC/scheduler instruments have a recording call site (the HLC queue services do not invoke either metrics class), and on the air-gap side only airgap_bundle_size_bytes is recorded. The “Emitted?” column reflects whether a live call site exists today. The legacy names called out as “NOT IMPLEMENTED” do not exist at all.
Meter StellaOps.Scheduler.HlcQueue (HlcSchedulerMetrics in StellaOps.Scheduler.Queue/Metrics)
| Metric | Type | Emitted? | Notes |
|---|---|---|---|
scheduler_hlc_enqueues_total | Counter | No (no caller) | Would tag tenant_id, job_type |
scheduler_hlc_enqueues_duplicates_total | Counter | No (no caller) | Idempotency hits (tag tenant_id) |
scheduler_hlc_dequeues_total | Counter | No (no caller) | Tagged tenant_id |
scheduler_chain_verifications_total | Counter | No (no caller) | Tagged tenant_id, result (success/failure) |
scheduler_chain_verification_failures_total | Counter | No (no caller) | Tagged tenant_id |
scheduler_batch_snapshots_total | Counter | No (no caller) | Tagged tenant_id, signed |
scheduler_hlc_enqueue_latency_ms | Histogram | No (no caller) | ms |
scheduler_chain_link_compute_latency_ms | Histogram | No (no caller) | ms |
scheduler_chain_verification_latency_ms | Histogram | No (no caller) | ms |
A second, separately-registered metrics type also exists at
StellaOps.Scheduler.Queue.Hlc.HlcSchedulerMetrics(astaticclass, meterStellaOps.Scheduler.Hlc) declaring overlapping and divergent names:scheduler_hlc_enqueues_total,scheduler_hlc_enqueue_deduplicated_total,scheduler_hlc_enqueue_duration_seconds,scheduler_hlc_dequeues_total,scheduler_hlc_dequeued_entries_total,scheduler_chain_verifications_total(tagresult=valid/invalid, notsuccess/failure),scheduler_chain_verification_issues_total,scheduler_chain_entries_verified_total,scheduler_batch_snapshots_created_total,scheduler_batch_snapshots_signed_total, andscheduler_batch_snapshot_verifications_total. These instruments are likewise not recorded to by any caller. Confirm which type (if any) the deployed Scheduler wires before building dashboards. This duplication is a known code smell, not a documented contract.
Meter StellaOps.AirGap.Sync (AirGapSyncMetrics)
| Metric | Type | Emitted? | Notes |
|---|---|---|---|
airgap_bundle_size_bytes | Histogram | Yes (RecordBundleSize) | bytes; tag node_id |
airgap_merge_conflicts_total | Counter | No (no caller) | Would tag conflict_type (DuplicateTimestamp/PayloadMismatch) |
airgap_duplicates_dropped_total | Counter | No (no caller) | |
airgap_bundles_exported_total / airgap_bundles_imported_total | Counter | No (no caller) | |
airgap_jobs_synced_total / airgap_offline_enqueues_total | Counter | No (no caller) | |
airgap_sync_duration_seconds | Histogram | No (no caller) | seconds |
airgap_merge_entries_count | Histogram | No (no caller) | entries |
Not implemented (referenced by older docs but absent from src/)
hlc_ticks_total,hlc_clock_skew_rejections_total,hlc_physical_time_offset_seconds(a.k.a.hlc_physical_offset_seconds),scheduler_chain_entries_total,scheduler_jobs_received_total— none of these exact names exist anywhere insrc/. (Note: a different name,scheduler_chain_entries_verified_total, is declared by theStellaOps.Scheduler.Hlcmeter — see the duplication note above — but it too has no recording call site.)- The Grafana dashboard
devops/observability/grafana/hlc-queue-metrics.jsoncited bydocs-archive/modules/scheduler/hlc-ordering.mddoes not exist at that path in this repo (the only files underdevops/observability/grafana/arecanary.jsonandprovisioning/).
Escalation Matrix
| Issue | First Responder | Escalation L2 | Escalation L3 |
|---|---|---|---|
| Chain verification failure | On-call SRE | Scheduler team | Security team |
| Clock skew | On-call SRE | Infrastructure | Architecture |
| Merge conflicts | On-call SRE | Scheduler team | - |
| Performance issues | On-call SRE | Database team | - |
| Duplicate node ID | On-call SRE | Scheduler team | - |
Revision History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2026-01-07 | Agent | Initial release |
| 1.1.0 | 2026-05-30 | Agent | Deep doc↔code reconciliation against src/JobEngine and src/__Libraries/StellaOps.HybridLogicalClock. Corrected config section (Scheduler:HlcOrdering, key EnableDualWrite, MaxClockSkewMs default 1000), corrected log messages, fixed batch_snapshot table name, replaced certificate/stella signer signing assumptions with the HMAC BatchSnapshotDsseOptions reality. Flagged non-existent endpoints (/health/hlc, /config), non-existent metrics (hlc_ticks_total, hlc_clock_skew_rejections_total, hlc_physical_time_offset_seconds, scheduler_jobs_received_total), absence of any shipped Hlc* Prometheus alert rules, and the missing Grafana dashboard. Added a Metrics reference of actually-emitted metric names. |
| 1.2.0 | 2026-05-31 | Agent | Second-pass reconciliation. Corrected: removed the false /health/liveness endpoint claim (Scheduler WebService maps only /healthz + /readyz); reframed the entire Metrics reference from “actually emitted” to declared-but-not-emitted after confirming the HLC scheduler metrics have zero recording call sites and only airgap_bundle_size_bytes is recorded on the AirGap meter. Added (completeness): status note that the HLC queue is not wired into any running host (neither DI entry point is called outside the library/tests, EnableHlcOrdering default false → runbook is forward-looking); flagged the two divergent SchedulerChainVerifier implementations (different log strings / issue sets) in §1; documented the DSSE config section path Scheduler:Queue:Hlc:DsseSigning and exact InvalidOperationException messages in §7; added the chk_signature_requires_signer constraint note; replaced the unreliable split_part(t_hlc,'-',2) query in §8 with a hyphen-safe substring (NodeId may contain hyphens); switched §6 enqueue-activity check from a flat metric to a scheduler_log row-count query. Verified config defaults, t_hlc format, table/column names, ConflictType enum, and HybridLogicalClock.Receive skew log/exception against source. |
