| 1 | package taskmonitor |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | // ErrStoreVersionConflict reports that a snapshot CAS lost to another writer. |
| 10 | // Callers may re-read and retry a derived update, or return a stable client |
| 11 | // conflict without parsing implementation-specific error text. |
| 12 | var ErrStoreVersionConflict = errors.New("task store version conflict") |
| 13 | |
| 14 | // Store is the read-only query surface for task monitoring. |
| 15 | type Store interface { |
| 16 | ListTasks(ctx context.Context, projectDir string) ([]TaskSnapshot, error) |
| 17 | GetTask(ctx context.Context, projectDir string, taskID string) (*TaskSnapshot, error) |
| 18 | ListEvents(ctx context.Context, projectDir string, taskID string, afterSequence int) ([]TaskEvent, error) |
| 19 | } |
| 20 | |
| 21 | // IdempotencyRecord captures the binding between an idempotency key and the |
| 22 | // operation it was used for. |
| 23 | type IdempotencyRecord struct { |
| 24 | Key string `json:"key"` |
| 25 | Op string `json:"op"` |
| 26 | TaskID string `json:"task_id"` |
| 27 | Version uint64 `json:"version"` |
| 28 | Pending bool `json:"pending,omitempty"` |
| 29 | ClaimedAt time.Time `json:"claimed_at,omitempty"` |
| 30 | } |
| 31 | |
| 32 | // IdempotencyClaimer atomically reserves a key before a control operation |
| 33 | // performs any side effect. Pending claims can be finalized or released. |
| 34 | type IdempotencyClaimer interface { |
| 35 | ClaimIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) (*IdempotencyRecord, error) |
| 36 | FinalizeIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) error |
| 37 | ReleaseIdempotency(ctx context.Context, projectDir, key string) error |
| 38 | } |
| 39 | |
| 40 | // WriteStore extends Store with atomic write operations for control |
| 41 | // commands, persistent idempotency, and event sequencing. |
| 42 | // |
| 43 | // Transaction ordering for control operations: |
| 44 | // 1. ClaimIdempotency — reserve the key before runtime/state side effects |
| 45 | // 2. SaveTask — persist state with version CAS |
| 46 | // 3. AppendAuditEvent — atomically assign sequence + write event |
| 47 | // 4. FinalizeIdempotency — mark the claim complete |
| 48 | // |
| 49 | // Steps 2-3 failures after a successful SaveTask leave the task in the new |
| 50 | // state with a potentially incomplete audit log. This is acceptable for a |
| 51 | // file-based store; a transactional store would provide stronger guarantees. |
| 52 | type WriteStore interface { |
| 53 | Store |
| 54 | |
| 55 | // SaveTask atomically persists snap with version-based CAS. |
| 56 | SaveTask(ctx context.Context, projectDir string, snap TaskSnapshot) error |
| 57 | |
| 58 | // RenewRuntimeLease extends an alive task lease only when ownerID still |
| 59 | // owns the persisted runtime generation. Implementations must read the raw |
| 60 | // stored snapshot rather than a liveness-reconciled observation. |
| 61 | RenewRuntimeLease(ctx context.Context, projectDir, taskID, ownerID string, leaseUntil time.Time) (bool, error) |
| 62 | |
| 63 | // AppendAuditEvent atomically assigns the next monotonic sequence |
| 64 | // number and appends the event to taskID's event log. Implementations |
| 65 | // must be safe for concurrent use across processes. |
| 66 | AppendAuditEvent(ctx context.Context, projectDir string, ev TaskEvent) error |
| 67 | |
| 68 | // CheckIdempotency returns the recorded key if it exists, or nil. |
| 69 | CheckIdempotency(ctx context.Context, projectDir string, key string) (*IdempotencyRecord, error) |
| 70 | |
| 71 | // RecordIdempotency atomically claims key for r. If key already exists |
| 72 | // with identical parameters, it is a no-op. If key exists with different |
| 73 | // parameters, it must return an error. Implementations must be safe |
| 74 | // across process restarts. |
| 75 | RecordIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) error |
| 76 | } |
| 77 |