ADR-221: Skill synchronization holds a durable lease
- Status
-
Accepted
- Date
-
2026-10-10
- Authors
-
Netresearch DTT GmbH
Context
ADR-035 namespaces imported skills by their persisted
source. The backend saves a source before offering synchronization, and
Skill loads that saved row. There is no
documented consumer contract for synchronization without a source row.
Skill currently checks the status and heartbeat on its
loaded entity and then persists syncing. Two requests can hold stale
entities: the second can enter while the first row is already syncing.
Reclaim, renewal and completion also write ordinary entity updates. A
late worker can therefore overwrite a successor's state. A timestamp
alone cannot identify ownership when two claims happen in one second.
Decision
Synchronize only a persisted source that is neither physically absent
nor soft-deleted by TYPO3. Missing or soft-deleted rows fail
closed before remote contact or skill changes. Detached test fixtures
must become real persisted sources; tests cannot define an unsafe
production exception. The internal service and source repository are
outside the frozen @api surface.
Add an internal database lease with a fresh, opaque random token for
each successful claim. Claim is one conditional update of the source
row, using its actual status and heartbeat. A fresh syncing row
refuses another claim. An interrupted or sufficiently skewed heartbeat
remains reclaimable under the existing 180-second window. Tokens are
internal bookkeeping and are absent from FormEngine and responses.
Renewal requires the matching token and a live lease. Reclaim checks the current persisted expiry condition in the same write that changes the state. Terminal release matches the owner token; losing ownership cannot clear, renew or overwrite a successor's state or pinned SHA.
Remote collection runs outside a transaction. After collection, a conditional renewal confirms ownership before materialization. Hold the source-row write lock in a short transaction through skill upserts, orphaning, their persistence and conditional terminal release. This closes the gap between checking a lease and publishing imported data. No remote request occurs while that publication transaction is held.
Source, skill and skill-audit writes must use the same database connection for this transaction. A configuration that splits these tables across connections fails closed before synchronization; an ordinary local transaction cannot promise atomic publication across independent databases. Failed publication rolls back its database changes and leaves a retryable diagnostic when the worker still owns the lease. Its published-change counters are all zero after rollback. An expired or replaced worker publishes no collected data.
Consequences
The implementation follows this decision in a separate dependent PR.
specs/ defines the acceptance
oracles. Existing token handling, immutable upstream revisions,
fingerprint verification, review, bounds and orphaning rules remain.
The schema gains internal ownership bookkeeping. There is no guessed
backfill: an existing syncing row keeps its heartbeat and can be
reclaimed by the expiry rule. Long remote calls may outlive the window;
their worker must discard the result and retry after losing its lease.
Lease expiry is not cancellation of an already running HTTP request.
Real database interleavings prove claim, renewal, reclaim, publication and release. Mutation counterexamples remove ownership or expiry conditions and must fail on state or imported-content assertions. These tests establish local persistence behavior, not a live GitHub availability guarantee.
Alternatives considered
Refreshing the entity before checking is insufficient: another worker
can claim between the read and the write. Comparing only
last_ admits a same-second ownership ambiguity. An atomic
entry claim followed by unconditional renewal or release still lets
an old worker disturb its successor. Holding a transaction across
remote collection would serialize slow network calls and is avoided.