Tighten activation recovery and API contracts

This commit is contained in:
ms2k 2026-07-14 19:04:00 +09:00
commit 3062bf3d75
4 changed files with 268 additions and 24 deletions

View file

@ -39,12 +39,21 @@ after durable authority changes. It never creates or restores a claim. Progress
and outcome operations require current fenced authority and return and outcome operations require current fenced authority and return
`precondition_failed` for stale or non-authoritative tuples. `precondition_failed` for stale or non-authoritative tuples.
Activation returns the live lease expiry and a recommended renewal time. Renewal
returns the corresponding updated projection. These timestamps schedule worker
behavior; the coordination store remains authoritative for whether the lease is
live.
A successful claim returns the attempt authority and the attempt's A successful claim returns the attempt authority and the attempt's
`ExecutionEnvelope`. The worker verifies that their job, attempt, fence, and `ExecutionEnvelope`. The worker verifies that their job, attempt, fence, and
worker incarnation agree before source or target I/O. Opaque resolved-source worker incarnation agree before source or target I/O. Resolved-source and
and checkpoint references identify durable streamable state; they are not checkpoint descriptors identify durable streamable state; they are not
reconstructed from coordination data. reconstructed from coordination data.
Resolved-source and checkpoint references carry their descriptor kind, schema
version, byte length, and SHA-256. The service accepts them only when their kind,
ownership, integrity, and fenced attempt authority match the mutation.
[`RemoraApiMutation`](../protocol/RemoraApiMutation.tla) models this durable [`RemoraApiMutation`](../protocol/RemoraApiMutation.tla) models this durable
operation ledger around an abstract successful `JobProtocol` effect. It does operation ledger around an abstract successful `JobProtocol` effect. It does
not redefine job phases or attempt-authority transitions. Its `Rejected` not redefine job phases or attempt-authority transitions. Its `Rejected`
@ -62,6 +71,11 @@ bound to the authenticated subject and filter.
credential values, bearer material, URL userinfo, raw upstream response bodies, credential values, bearer material, URL userinfo, raw upstream response bodies,
internal stack traces, or unredacted configured endpoints. internal stack traces, or unredacted configured endpoints.
Credential creation uses a closed material shape selected by `kind`; rotation
must use the existing kind's shape. Custom HTTP headers use lowercase canonical
names and reject connection-nominated, routing, framing, cookie, proxy, and
authentication headers.
## Jobs, pages, and results ## Jobs, pages, and results
`Job.phase` uses the exact `JobProtocol` phase vocabulary; this contract does `Job.phase` uses the exact `JobProtocol` phase vocabulary; this contract does
@ -81,6 +95,10 @@ unauthorized, absent, and never-committed results are not exposed. The hard
delete endpoint returns the durable deletion resource established by the delete endpoint returns the durable deletion resource established by the
architecture. architecture.
Before hard deletion, job reads return the complete `job` projection. Once only
the permanent minimal record remains, authorized reads and deleted-job pages use
the distinct `tombstone` projection rather than inventing discarded job fields.
A deletion whose exact remaining work cannot progress because target access or A deletion whose exact remaining work cannot progress because target access or
configuration is unavailable enters `blocked` with a redacted problem. It configuration is unavailable enters `blocked` with a redacted problem. It
retains the deletion obligation and can return to active processing when the retains the deletion obligation and can return to active processing when the

View file

@ -61,11 +61,11 @@ paths:
operationId: getJob operationId: getJob
responses: responses:
'200': '200':
description: Current authorized job projection. description: Current authorized full job or minimal hard-deletion tombstone projection.
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Job' $ref: '#/components/schemas/JobResource'
'404': '404':
$ref: '#/components/responses/NotFound' $ref: '#/components/responses/NotFound'
default: default:
@ -438,8 +438,12 @@ paths:
schema: schema:
$ref: '#/components/schemas/AttemptRenewRequest' $ref: '#/components/schemas/AttemptRenewRequest'
responses: responses:
'204': '200':
description: Existing live coordination lease renewed or duplicate operation accepted; durable authority is unchanged. description: Existing exact live coordination lease renewed or prior response replayed; durable authority is unchanged.
content:
application/json:
schema:
$ref: '#/components/schemas/LeaseProjection'
'409': '409':
$ref: '#/components/responses/PreconditionFailed' $ref: '#/components/responses/PreconditionFailed'
default: default:
@ -886,17 +890,92 @@ components:
SourceCredentialKind: SourceCredentialKind:
type: string type: string
enum: [basic_auth, bearer_token, http_headers, object_store, hugging_face] enum: [basic_auth, bearer_token, http_headers, object_store, hugging_face]
CredentialMaterial: BasicAuthMaterial:
type: object type: object
minProperties: 1 additionalProperties: false
maxProperties: 64 required: [username, password]
additionalProperties: properties:
username:
type: string
minLength: 1
maxLength: 1024
password:
type: string type: string
minLength: 1 minLength: 1
maxLength: 16384 maxLength: 16384
writeOnly: true writeOnly: true
description: Write-only adapter credential fields. Values are never returned or logged. TokenMaterial:
type: object
additionalProperties: false
required: [token]
properties:
token:
type: string
minLength: 1
maxLength: 16384
writeOnly: true
HttpHeadersMaterial:
type: object
additionalProperties: false
required: [headers]
properties:
headers:
type: object
minProperties: 1
maxProperties: 64
propertyNames:
allOf:
- pattern: '^[a-z0-9!#$%&''*+.^_`|~-]+$'
- not:
enum: [authorization, connection, content-length, cookie, host, keep-alive, proxy-authenticate, proxy-authorization, te, trailer, transfer-encoding, upgrade]
additionalProperties:
type: string
minLength: 1
maxLength: 16384
description: Canonical lowercase end-to-end headers. Connection-nominated and routing, framing, cookie, proxy, and authentication headers are rejected.
writeOnly: true
ObjectStoreCredentialMaterial:
type: object
additionalProperties: false
required: [access_key_id, secret_access_key]
properties:
access_key_id:
type: string
minLength: 1
maxLength: 1024
secret_access_key:
type: string
minLength: 1
maxLength: 16384
session_token:
type: string
minLength: 1
maxLength: 16384
writeOnly: true
CredentialMaterial:
oneOf:
- $ref: '#/components/schemas/BasicAuthMaterial'
- $ref: '#/components/schemas/TokenMaterial'
- $ref: '#/components/schemas/HttpHeadersMaterial'
- $ref: '#/components/schemas/ObjectStoreCredentialMaterial'
writeOnly: true
description: Write-only material whose closed shape must match the credential kind. Values are never returned or logged.
SourceCredentialCreate: SourceCredentialCreate:
oneOf:
- $ref: '#/components/schemas/BasicAuthCredentialCreate'
- $ref: '#/components/schemas/BearerTokenCredentialCreate'
- $ref: '#/components/schemas/HttpHeadersCredentialCreate'
- $ref: '#/components/schemas/ObjectStoreCredentialCreate'
- $ref: '#/components/schemas/HuggingFaceCredentialCreate'
discriminator:
propertyName: kind
mapping:
basic_auth: '#/components/schemas/BasicAuthCredentialCreate'
bearer_token: '#/components/schemas/BearerTokenCredentialCreate'
http_headers: '#/components/schemas/HttpHeadersCredentialCreate'
object_store: '#/components/schemas/ObjectStoreCredentialCreate'
hugging_face: '#/components/schemas/HuggingFaceCredentialCreate'
BasicAuthCredentialCreate:
type: object type: object
additionalProperties: false additionalProperties: false
required: [label, kind, material] required: [label, kind, material]
@ -906,13 +985,66 @@ components:
minLength: 1 minLength: 1
maxLength: 200 maxLength: 200
kind: kind:
$ref: '#/components/schemas/SourceCredentialKind' const: basic_auth
material: material:
$ref: '#/components/schemas/CredentialMaterial' $ref: '#/components/schemas/BasicAuthMaterial'
BearerTokenCredentialCreate:
type: object
additionalProperties: false
required: [label, kind, material]
properties:
label:
type: string
minLength: 1
maxLength: 200
kind:
const: bearer_token
material:
$ref: '#/components/schemas/TokenMaterial'
HttpHeadersCredentialCreate:
type: object
additionalProperties: false
required: [label, kind, material]
properties:
label:
type: string
minLength: 1
maxLength: 200
kind:
const: http_headers
material:
$ref: '#/components/schemas/HttpHeadersMaterial'
ObjectStoreCredentialCreate:
type: object
additionalProperties: false
required: [label, kind, material]
properties:
label:
type: string
minLength: 1
maxLength: 200
kind:
const: object_store
material:
$ref: '#/components/schemas/ObjectStoreCredentialMaterial'
HuggingFaceCredentialCreate:
type: object
additionalProperties: false
required: [label, kind, material]
properties:
label:
type: string
minLength: 1
maxLength: 200
kind:
const: hugging_face
material:
$ref: '#/components/schemas/TokenMaterial'
SourceCredentialRotate: SourceCredentialRotate:
type: object type: object
additionalProperties: false additionalProperties: false
required: [material] required: [material]
description: The material shape must match the stored credential kind; rotation never changes kind.
properties: properties:
material: material:
$ref: '#/components/schemas/CredentialMaterial' $ref: '#/components/schemas/CredentialMaterial'
@ -977,6 +1109,9 @@ components:
JobPhase: JobPhase:
type: string type: string
enum: [Queued, Running, RetryWait, Cancelling, Succeeded, Failed, Cancelled] enum: [Queued, Running, RetryWait, Cancelling, Succeeded, Failed, Cancelled]
TerminalJobPhase:
type: string
enum: [Succeeded, Failed, Cancelled]
JobLifecycle: JobLifecycle:
type: string type: string
enum: [visible, soft_deleted, hard_deleting, hard_deleted] enum: [visible, soft_deleted, hard_deleting, hard_deleted]
@ -992,6 +1127,19 @@ components:
minimum: 1 minimum: 1
worker_incarnation: worker_incarnation:
$ref: '#/components/schemas/OpaqueId' $ref: '#/components/schemas/OpaqueId'
LeaseProjection:
type: object
additionalProperties: false
required: [expires_at, renew_after]
properties:
expires_at:
type: string
format: date-time
description: Expiry of the exact coordination lease established for this authority.
renew_after:
type: string
format: date-time
description: Earliest recommended renewal time, strictly before expires_at.
Progress: Progress:
type: object type: object
additionalProperties: false additionalProperties: false
@ -1129,6 +1277,27 @@ components:
Sha256Digest: Sha256Digest:
type: string type: string
pattern: '^sha256:[a-f0-9]{64}$' pattern: '^sha256:[a-f0-9]{64}$'
DurableDescriptorRef:
type: object
additionalProperties: false
required: [id, kind, schema_version, byte_length, checksum]
properties:
id:
$ref: '#/components/schemas/OpaqueId'
kind:
type: string
enum: [resolved_source, checkpoint]
schema_version:
type: integer
minimum: 1
byte_length:
type: integer
minimum: 0
item_count:
type: integer
minimum: 0
checksum:
$ref: '#/components/schemas/Sha256Digest'
ResultMember: ResultMember:
type: object type: object
additionalProperties: false additionalProperties: false
@ -1160,8 +1329,10 @@ components:
Job: Job:
type: object type: object
additionalProperties: false additionalProperties: false
required: [id, owner, label, revision, phase, lifecycle, source, target, authoritative_attempt, progress, checkpoint, result, failure, created_at, updated_at, event_cursor] required: [resource_kind, id, owner, label, revision, phase, lifecycle, source, target, authoritative_attempt, progress, checkpoint, result, failure, created_at, updated_at, event_cursor]
properties: properties:
resource_kind:
const: job
id: id:
$ref: '#/components/schemas/OpaqueId' $ref: '#/components/schemas/OpaqueId'
owner: owner:
@ -1210,6 +1381,44 @@ components:
event_cursor: event_cursor:
type: string type: string
minLength: 16 minLength: 16
JobTombstone:
type: object
additionalProperties: false
required: [resource_kind, id, owner, target, terminal_phase, deletion_operation_id, created_at, hard_delete_started_at, updated_at, event_cursor]
properties:
resource_kind:
const: tombstone
id:
$ref: '#/components/schemas/OpaqueId'
owner:
$ref: '#/components/schemas/PrincipalId'
target:
$ref: '#/components/schemas/Target'
terminal_phase:
$ref: '#/components/schemas/TerminalJobPhase'
deletion_operation_id:
$ref: '#/components/schemas/OpaqueId'
created_at:
type: string
format: date-time
hard_delete_started_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
event_cursor:
type: string
minLength: 16
JobResource:
oneOf:
- $ref: '#/components/schemas/Job'
- $ref: '#/components/schemas/JobTombstone'
discriminator:
propertyName: resource_kind
mapping:
job: '#/components/schemas/Job'
tombstone: '#/components/schemas/JobTombstone'
JobPage: JobPage:
type: object type: object
additionalProperties: false additionalProperties: false
@ -1218,7 +1427,7 @@ components:
items: items:
type: array type: array
items: items:
$ref: '#/components/schemas/Job' $ref: '#/components/schemas/JobResource'
next_cursor: next_cursor:
type: [string, 'null'] type: [string, 'null']
event_cursor: event_cursor:
@ -1378,7 +1587,7 @@ components:
- type: 'null' - type: 'null'
resolved_source_ref: resolved_source_ref:
oneOf: oneOf:
- $ref: '#/components/schemas/OpaqueId' - $ref: '#/components/schemas/DurableDescriptorRef'
- type: 'null' - type: 'null'
target: target:
$ref: '#/components/schemas/Target' $ref: '#/components/schemas/Target'
@ -1388,7 +1597,7 @@ components:
$ref: '#/components/schemas/OpaqueId' $ref: '#/components/schemas/OpaqueId'
checkpoint_ref: checkpoint_ref:
oneOf: oneOf:
- $ref: '#/components/schemas/OpaqueId' - $ref: '#/components/schemas/DurableDescriptorRef'
- type: 'null' - type: 'null'
ClaimResponse: ClaimResponse:
type: object type: object
@ -1402,12 +1611,14 @@ components:
ActivationResponse: ActivationResponse:
type: object type: object
additionalProperties: false additionalProperties: false
required: [authority, execution] required: [authority, execution, lease]
properties: properties:
authority: authority:
$ref: '#/components/schemas/AttemptAuthority' $ref: '#/components/schemas/AttemptAuthority'
execution: execution:
$ref: '#/components/schemas/ExecutionEnvelope' $ref: '#/components/schemas/ExecutionEnvelope'
lease:
$ref: '#/components/schemas/LeaseProjection'
AttemptMutation: AttemptMutation:
type: object type: object
required: [operation_id, fence, worker_incarnation] required: [operation_id, fence, worker_incarnation]
@ -1435,7 +1646,12 @@ components:
required: [resolved_source_ref] required: [resolved_source_ref]
properties: properties:
resolved_source_ref: resolved_source_ref:
$ref: '#/components/schemas/OpaqueId' allOf:
- $ref: '#/components/schemas/DurableDescriptorRef'
- type: object
properties:
kind:
const: resolved_source
AttemptCheckpointRequest: AttemptCheckpointRequest:
unevaluatedProperties: false unevaluatedProperties: false
allOf: allOf:
@ -1444,7 +1660,12 @@ components:
required: [checkpoint_ref] required: [checkpoint_ref]
properties: properties:
checkpoint_ref: checkpoint_ref:
$ref: '#/components/schemas/OpaqueId' allOf:
- $ref: '#/components/schemas/DurableDescriptorRef'
- type: object
properties:
kind:
const: checkpoint
AttemptProgressRequest: AttemptProgressRequest:
unevaluatedProperties: false unevaluatedProperties: false
allOf: allOf:

View file

@ -22,6 +22,7 @@ INVARIANTS
ConflictIsChangedRequest ConflictIsChangedRequest
ReapedClaimHasNoActivationEffect ReapedClaimHasNoActivationEffect
ReapedCoordinationIsClean ReapedCoordinationIsClean
RecoveryOwnsInactiveClaim
PROPERTIES PROPERTIES
ActivationAppliesAtMostOnce ActivationAppliesAtMostOnce

View file

@ -163,7 +163,6 @@ RejectPrecondition(op, c, f, w) ==
ExpireReservation(c) == ExpireReservation(c) ==
/\ durableState[c] = Authorized /\ durableState[c] = Authorized
/\ reservation[c] # NoWorker /\ reservation[c] # NoWorker
/\ lease[c] = NoWorker
/\ reservation' = [reservation EXCEPT ![c] = NoWorker] /\ reservation' = [reservation EXCEPT ![c] = NoWorker]
/\ UNCHANGED <<durableState, attemptFence, attemptWorker, lease, /\ UNCHANGED <<durableState, attemptFence, attemptWorker, lease,
activationStage, durableLock, operationRequest, activationStage, durableLock, operationRequest,
@ -185,6 +184,7 @@ ExpireLease(c) ==
AcquireRecoveryLock(c) == AcquireRecoveryLock(c) ==
/\ durableState[c] = Authorized /\ durableState[c] = Authorized
/\ reservation[c] = NoWorker
/\ durableLock[c] = NoOwner /\ durableLock[c] = NoOwner
/\ durableLock' = [durableLock EXCEPT ![c] = Recovery] /\ durableLock' = [durableLock EXCEPT ![c] = Recovery]
/\ UNCHANGED <<durableState, attemptFence, attemptWorker, reservation, /\ UNCHANGED <<durableState, attemptFence, attemptWorker, reservation,
@ -291,6 +291,10 @@ ReapedCoordinationIsClean ==
/\ reservation[c] = NoWorker /\ reservation[c] = NoWorker
/\ lease[c] = NoWorker /\ lease[c] = NoWorker
RecoveryOwnsInactiveClaim ==
\A c \in Claims :
durableLock[c] = Recovery => reservation[c] = NoWorker
ActivationAppliesAtMostOnceAction == ActivationAppliesAtMostOnceAction ==
\A c \in Claims : \A c \in Claims :
activationEffectCount[c] = 1 => activationEffectCount'[c] = 1 activationEffectCount[c] = 1 => activationEffectCount'[c] = 1