Task · SFL-0040

ha: object-store lease coordinator (s3_lease / gcs_lease) for stateless platforms

Status
To Do
Labels
followup, phase-3, roadmap
Milestone
Feature roadmap — operator ergonomics & platform capabilities
Updated
2026-08-14

Description

ha: object-store lease coordinator (s3_lease / gcs_lease) for stateless platforms

What

CoordinateConfig.type accepts exactly noop | file_lease | k8s_lease (src/sf2loki/config.py:1184), and src/sf2loki/coordinate/ holds only base.py, file_lease.py, k8s_lease.py. Neither real coordinator fits stateless non-Kubernetes compute:

The docs steer operators straight into this hole. docs/architecture.md:272-273 states “S3 and GCS need no mounted volume, so they’re the fit for stateless compute (Fargate, Cloud Run, ECS with ephemeral storage)”; README.md:433-437 and docs/index.md:30 repeat it. But docs/deployment/high-availability.md:86-91 maps coordinators to state backends as only:

There is no row for “object-store state, no Kubernetes”. That combination — the recommended stateless setup — has no coordinator at all, so HA is unreachable there.

The arbitration primitive already exists in-repo. S3CheckpointStore performs ETag compare-and-swap: IfNoneMatch: "*" for the first write and IfMatch: <etag> for updates (src/sf2loki/state/s3_store.py:284-287, src/sf2loki/state/s3_store.py:335-338), raising StateStoreConflictError on a 412. GcsCheckpointStore does the generation-precondition equivalent — ifGenerationMatch: "0" for the first write, the current generation for an update (src/sf2loki/state/gcs_store.py:236-239, src/sf2loki/state/gcs_store.py:286-289). Both extras are already declared (pyproject.toml:29-30: s3 = ["aiobotocore>=2.21"], gcs = ["gcloud-aio-storage>=9.0"]). FileLeaseCoordinator (src/sf2loki/coordinate/file_lease.py, 387 lines) already implements acquire/hold/renew/takeover over an atomically-replaced {holder, expires_at, epoch} JSON document, and K8sLeaseCoordinator (src/sf2loki/coordinate/k8s_lease.py, 536 lines) already implements the variant where a lost CAS is the contention signal, so no pause-then-reread step is needed (docs/deployment/high-availability.md:69-71). An object with conditional writes gives the same document semantics as both.

Why it matters

An operator deploys the documented stateless configuration — ECS Fargate or Cloud Run with state.store: s3 — and wants automatic failover. No coordinate.type value serves them. The options are: bolt EFS onto a Fargate task purely to host a 100-byte lease file (reintroducing the volume the object-store checkpoint path removed), migrate to Kubernetes, or run a single task and absorb the full platform reschedule time on every host failure. That reschedule is minutes of ingestion gap. For Pub/Sub streaming sources the gap is bounded by Salesforce’s retention window rather than lost outright, but EventLogFile and SOQL-polled lag accrues for the whole outage and the connector is dark to alerting.

The asymmetry is the point: the checkpoint document already lives in an object store with conditional writes strong enough to arbitrate leadership, and the lease is a strictly smaller problem than the checkpoint document already solved there.

Proposed approach

Config (src/sf2loki/config.py)

Coordinators (src/sf2loki/coordinate/s3_lease.py, src/sf2loki/coordinate/gcs_lease.py)

Reuse the run_acquire_hold_pause loop shape and injected utcnow/sleep seams from src/sf2loki/coordinate/k8s_lease.py, storing {holder, expires_at, epoch} as a small JSON object:

Wiring (src/sf2loki/app.py)

Doctor (src/sf2loki/doctor.py)

_check_coordinator currently falls through to the Kubernetes probe for any type that is not noop or file_lease (src/sf2loki/doctor.py:595-606), so without a new branch an s3_lease deployment would silently probe Kubernetes and report a misleading result. Add explicit branches that do a HEAD plus a conditional-PUT/delete round-trip against a probe key (never the live lease object — same discipline as _COORDINATOR_LEASE_PROBE_NAME at src/sf2loki/doctor.py:81), reporting FAIL on a permissions error and distinguishing “conditional writes unsupported by this endpoint” for non-AWS S3-compatible endpoints.

Docs


Imported from GitHub issue #124 on 2026-08-14, when this repo migrated from GitHub Issues to Backlog.md. The original issue has been deleted; its verbatim body, labels and comments are preserved in archive/issues-dump.json (jq '.[] | select(.number == 124)' archive/issues-dump.json).

Filed from the 2026-07-30 full-repo audit (11 finder lanes + adversarial verification per finding).

Acceptance Criteria

Definition of Done

References

View the source file on GitHub