---
title: "Sync background jobs"
description: "Analysis of every background goroutine/timer running in internal/planes/sync."
---

*Generated from `SYNC_BACKGROUND_JOBS.md` in the [Sarvex repository](https://github.com/sarvexlab/sarvex) — edit the source there, not this page.*

Analysis of every background goroutine/timer running in `internal/planes/sync`.

---

## 1. SweepScheduler — `sweep_scheduler.go`

**What:** Leader-elected loop that fires a full per-store reconciliation sweep on a configurable interval (`defaultSweepInterval`).

**Pattern:** Single goroutine with two tickers — `electTicker` (renews the distributed lease) and `sweepTicker` (fires sweep cycles). `runSweepCycle` is a no-op unless this instance currently holds the `"scheduler:leader"` lease.

**Multi-instance safe:** Yes. `concurrency.Lease.TryAcquire` is a distributed compare-and-set; only one instance holds the lease at a time. Non-leaders keep their election loop running for fast failover (gap bounded by `leaseTTL`, not just `renewInterval`).

---

## 2. CatalogImportHandler goroutines — `catalog_import.go`

**What:** On-demand goroutines, one per in-flight catalog import job. Spawned in two places:
- `HandleCommand` — when a `control.catalog.import` event arrives.
- `ResumeIncomplete` — at startup, to resume any jobs left `running` by a prior crashed instance.

**Pattern:** `wg.Add(1); go func()` per job, all cancelled via a shared `runCtx`. `Stop()` cancels `runCtx` and blocks on `wg.Wait()`.

**Multi-instance safe:** Mostly. Before spawning, `HandleCommand` checks the DB for an existing `running` job and refuses to start a second concurrent import for the same store. However, `ResumeIncomplete` runs at startup on every instance without a distributed guard — if two instances start simultaneously and both read the same `running` jobs before either has progressed, both may spawn goroutines for the same store.

---

## 3. Surface artifact runner — `surface/runner.go`

**What:** Regenerates each enrolled store's surface artifact: per-store `time.AfterFunc` debounce timers (20 s after the last catalog change) plus an hourly safety-net ticker (`RunSafetyNet`) that backstops missed events. Only does work for surfaces that register a `Generator` in `surface/all`; the bundled `direct_agent` surface registers none.

**Pattern:** `sync.Mutex`-guarded `map[string]*time.Timer`; raw `time.NewTicker(1h)` goroutine.

**Multi-instance safe: No.** Every instance keeps its own timers and ticker, so with N instances each artifact is regenerated up to N times per burst/hour. Regeneration is idempotent (stable token, deterministic output), so this costs work, not correctness.

---

## Multi-instance summary

| Job | Leader-elected / sharded | Safe with N instances |
|---|---|---|
| Sweep scheduler | Yes — distributed lease | Yes |
| Surface artifact runner (debounce + safety net) | No | Correct but N × regenerations |
| Catalog import goroutines | DB-guarded (partial) | Mostly; startup race edge case |

### Key gap

The surface artifact runner has the same N-instance multiplication the sweep scheduler's comment calls out and solves with a lease. If a surface with an expensive generator is added, bring the safety-net ticker under a lease (as `"scheduler:leader"` does) and route catalog-change events through a single-consumer subscription.
