6.5 KiB
HLC Job Sync Offline Operations
Sprint: SPRINT_20260105_002_003_ROUTER
This document describes the offline job synchronization mechanism using Hybrid Logical Clock (HLC) ordering for air-gap scenarios.
Overview
When nodes operate in disconnected/offline mode, scheduled jobs are enqueued locally with HLC timestamps. Upon reconnection or air-gap transfer, these job logs are merged deterministically to maintain global ordering.
Key features:
- Deterministic ordering: Jobs merge by HLC total order
(T_hlc.PhysicalTime, T_hlc.LogicalCounter, NodeId, JobId) - Chain integrity: Each entry links to the previous via
link = Hash(prev_link || job_id || t_hlc || payload_hash) - Conflict-free: Same payload = same JobId (deterministic), so duplicates are safely dropped
- Audit trail: Source node ID and original links preserved for traceability
CLI Commands
Export Job Logs
Export offline job logs to a sync bundle for air-gap transfer:
# Export job logs for a tenant
stella airgap jobs export --tenant my-tenant -o job-sync-bundle.json
# Export with verbose output
stella airgap jobs export --tenant my-tenant -o bundle.json --verbose
# Export as JSON for automation
stella airgap jobs export --tenant my-tenant --json
Options:
--tenant, -t- Tenant ID (defaults to "default")--output, -o- Output file path--node- Export specific node only (default: current node)--sign- Sign bundle with DSSE--json- Output result as JSON--verbose- Enable verbose logging
Import Job Logs
Import a job sync bundle from air-gap transfer:
# Verify bundle without importing
stella airgap jobs import bundle.json --verify-only
# Import bundle
stella airgap jobs import bundle.json
# Force import despite validation issues
stella airgap jobs import bundle.json --force
# Import with JSON output for automation
stella airgap jobs import bundle.json --json
Options:
bundle- Path to job sync bundle file (required)--verify-only- Only verify the bundle without importing--force- Force import even if validation fails--json- Output result as JSON--verbose- Enable verbose logging
List Available Bundles
List job sync bundles in a directory:
# List bundles in current directory
stella airgap jobs list
# List bundles in specific directory
stella airgap jobs list --source /path/to/bundles
# Output as JSON
stella airgap jobs list --json
Options:
--source, -s- Source directory (default: current directory)--json- Output result as JSON--verbose- Enable verbose logging
Bundle Format
Job sync bundles are JSON files with the following structure:
{
"bundleId": "guid",
"tenantId": "string",
"createdAt": "ISO8601",
"createdByNodeId": "string",
"manifestDigest": "sha256:hex",
"signature": "base64 (optional)",
"signedBy": "keyId (optional)",
"jobLogs": [
{
"nodeId": "string",
"lastHlc": "HLC timestamp string",
"chainHead": "base64",
"entries": [
{
"nodeId": "string",
"tHlc": "HLC timestamp string",
"jobId": "guid",
"partitionKey": "string (optional)",
"payload": "JSON string",
"payloadHash": "base64",
"prevLink": "base64 (null for first)",
"link": "base64",
"enqueuedAt": "ISO8601"
}
]
}
]
}
Validation
Bundle validation checks:
- Manifest digest: Recomputes digest from job logs and compares
- Chain integrity: Verifies each entry's prev_link matches expected
- Link verification: Recomputes links and verifies against stored values
- Chain head: Verifies last entry link matches node's chain head
Merge Algorithm
When importing bundles from multiple nodes:
- Collect: Gather all entries from all node logs
- Sort: Order by HLC total order
(PhysicalTime, LogicalCounter, NodeId, JobId) - Deduplicate: Same JobId = same payload (drop later duplicates)
- Recompute chain: Build unified chain from merged entries
This produces a deterministic ordering regardless of import sequence.
Conflict Resolution
| Scenario | Resolution |
|---|---|
| Same JobId, same payload, different HLC | Take earliest HLC, drop duplicates |
| Same JobId, different payloads | Error - indicates bug in deterministic ID computation |
Metrics
The following metrics are emitted:
| Metric | Type | Description |
|---|---|---|
airgap_bundles_exported_total |
Counter | Total bundles exported |
airgap_bundles_imported_total |
Counter | Total bundles imported |
airgap_jobs_synced_total |
Counter | Total jobs synced |
airgap_duplicates_dropped_total |
Counter | Duplicates dropped during merge |
airgap_merge_conflicts_total |
Counter | Merge conflicts by type |
airgap_offline_enqueues_total |
Counter | Offline enqueue operations |
airgap_bundle_size_bytes |
Histogram | Bundle size distribution |
airgap_sync_duration_seconds |
Histogram | Sync operation duration |
airgap_merge_entries_count |
Histogram | Entries per merge operation |
Service Registration
To use job sync in your application:
// Register core services
services.AddAirGapSyncServices(nodeId: "my-node-id");
// Register file-based transport (for air-gap)
services.AddFileBasedJobSyncTransport();
// Or router-based transport (for connected scenarios)
services.AddRouterJobSyncTransport();
// Register sync service (requires ISyncSchedulerLogRepository)
services.AddAirGapSyncImportService();
Operational Runbook
Pre-Export Checklist
- Node has offline job logs to export
- Target path is writable
- Signing key available (if --sign used)
Pre-Import Checklist
- Bundle file accessible
- Bundle signature verified (if signed)
- Scheduler database accessible
- Sufficient disk space
Recovery Procedures
Chain validation failure:
- Identify which entry has chain break
- Check for data corruption in bundle
- Re-export from source node if possible
- Use
--forceonly if data loss is acceptable
Duplicate conflict:
- This is expected - duplicates are safely dropped
- Check duplicate count in output
- Verify merged jobs match expected count
Payload mismatch (same JobId, different payloads):
- This indicates a bug - same idempotency key should produce same payload
- Review job generation logic
- Do not force import - fix root cause