feat(tasks): implement atomic claims and leases
This commit is contained in:
@@ -3,6 +3,7 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -145,7 +146,10 @@ func (s *Store) ListTasks(
|
||||
query.WriteString(`SELECT
|
||||
id, creator_subject, created_by_user_id, source_ref, title, description, sku,
|
||||
image_asset_id, quantity, max_budget_cents, currency, status,
|
||||
version, cancel_reason, canceled_at, created_at, updated_at
|
||||
version, claimed_by_user_id, claimed_by_device_id, claim_generation,
|
||||
claim_token_hash, claim_issued_at, claim_expires_at, cancel_reason,
|
||||
cancel_requested_at, cancel_requested_by_user_id, canceled_at,
|
||||
created_at, updated_at
|
||||
FROM purchase_tasks
|
||||
WHERE creator_subject = ?`)
|
||||
arguments := []any{filter.CreatorSubject}
|
||||
@@ -242,7 +246,9 @@ func (s *Store) GetTaskDetail(
|
||||
}
|
||||
rows, err := tx.QueryContext(
|
||||
ctx,
|
||||
`SELECT id, task_id, actor_user_id, event_type, message, occurred_at
|
||||
`SELECT
|
||||
id, task_id, actor_user_id, actor_device_id,
|
||||
event_type, message, occurred_at
|
||||
FROM task_events
|
||||
WHERE task_id = ?
|
||||
ORDER BY occurred_at ASC, id ASC`,
|
||||
@@ -256,11 +262,13 @@ func (s *Store) GetTaskDetail(
|
||||
for rows.Next() {
|
||||
var event domain.TaskEvent
|
||||
var actorUserID sql.NullString
|
||||
var actorDeviceID sql.NullString
|
||||
var occurredAt string
|
||||
if err := rows.Scan(
|
||||
&event.ID,
|
||||
&event.TaskID,
|
||||
&actorUserID,
|
||||
&actorDeviceID,
|
||||
&event.Type,
|
||||
&event.Message,
|
||||
&occurredAt,
|
||||
@@ -270,6 +278,9 @@ func (s *Store) GetTaskDetail(
|
||||
if actorUserID.Valid {
|
||||
event.ActorUserID = &actorUserID.String
|
||||
}
|
||||
if actorDeviceID.Valid {
|
||||
event.ActorDeviceID = &actorDeviceID.String
|
||||
}
|
||||
event.OccurredAt, err = parseTimestamp(occurredAt)
|
||||
if err != nil {
|
||||
return domain.TaskDetail{}, err
|
||||
@@ -282,10 +293,31 @@ func (s *Store) GetTaskDetail(
|
||||
if err := rows.Close(); err != nil {
|
||||
return domain.TaskDetail{}, repositoryFailure(err)
|
||||
}
|
||||
execution, err := scanExecution(tx.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT
|
||||
id, task_id, attempt_no, claim_generation, user_id, device_id,
|
||||
current_step, order_submitted, started_at, last_heartbeat_at,
|
||||
finished_at
|
||||
FROM task_executions
|
||||
WHERE task_id = ?
|
||||
ORDER BY attempt_no DESC
|
||||
LIMIT 1`,
|
||||
taskID,
|
||||
))
|
||||
var executionPointer *domain.TaskExecution
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
executionPointer = nil
|
||||
} else if err != nil {
|
||||
return domain.TaskDetail{}, repositoryFailure(err)
|
||||
} else {
|
||||
executionPointer = &execution
|
||||
}
|
||||
detail := domain.TaskDetail{
|
||||
Task: task,
|
||||
Asset: asset,
|
||||
Events: events,
|
||||
Task: task,
|
||||
Asset: asset,
|
||||
Execution: executionPointer,
|
||||
Events: events,
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.TaskDetail{}, repositoryFailure(err)
|
||||
@@ -293,7 +325,7 @@ func (s *Store) GetTaskDetail(
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (s *Store) CancelPendingTask(
|
||||
func (s *Store) CancelTask(
|
||||
ctx context.Context,
|
||||
creatorSubject string,
|
||||
taskID string,
|
||||
@@ -313,25 +345,66 @@ func (s *Store) CancelPendingTask(
|
||||
if !domain.CanCancel(task.Status) {
|
||||
return domain.PurchaseTask{}, usecase.ErrTaskStateConflict
|
||||
}
|
||||
result, err := tx.ExecContext(
|
||||
ctx,
|
||||
`UPDATE purchase_tasks
|
||||
SET status = 'CANCELED',
|
||||
version = version + 1,
|
||||
cancel_reason = NULLIF(?, ''),
|
||||
canceled_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND creator_subject = ?
|
||||
AND status = 'PENDING'
|
||||
AND version = ?`,
|
||||
reason,
|
||||
formatTimestamp(canceledAt),
|
||||
formatTimestamp(canceledAt),
|
||||
taskID,
|
||||
creatorSubject,
|
||||
task.Version,
|
||||
)
|
||||
if domain.CanRequestCancel(task.Status) &&
|
||||
task.CancelRequestedAt != nil {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.PurchaseTask{}, repositoryFailure(err)
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
var result sql.Result
|
||||
if domain.CanAdminCancelImmediately(task.Status) {
|
||||
result, err = tx.ExecContext(
|
||||
ctx,
|
||||
`UPDATE purchase_tasks
|
||||
SET status = 'CANCELED',
|
||||
version = version + 1,
|
||||
claimed_by_user_id = NULL,
|
||||
claimed_by_device_id = NULL,
|
||||
claim_token_hash = NULL,
|
||||
claim_issued_at = NULL,
|
||||
claim_expires_at = NULL,
|
||||
cancel_reason = NULLIF(?, ''),
|
||||
cancel_requested_at = NULL,
|
||||
cancel_requested_by_user_id = NULL,
|
||||
canceled_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND creator_subject = ?
|
||||
AND status IN ('PENDING', 'CLAIMED')
|
||||
AND version = ?`,
|
||||
reason,
|
||||
formatTimestamp(canceledAt),
|
||||
formatTimestamp(canceledAt),
|
||||
taskID,
|
||||
creatorSubject,
|
||||
task.Version,
|
||||
)
|
||||
} else {
|
||||
event.Type = "TASK_CANCEL_REQUESTED"
|
||||
event.Message = "task cancellation requested"
|
||||
result, err = tx.ExecContext(
|
||||
ctx,
|
||||
`UPDATE purchase_tasks
|
||||
SET version = version + 1,
|
||||
cancel_reason = NULLIF(?, ''),
|
||||
cancel_requested_at = ?,
|
||||
cancel_requested_by_user_id = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND creator_subject = ?
|
||||
AND status IN ('RUNNING', 'WAITING_CONFIRMATION')
|
||||
AND cancel_requested_at IS NULL
|
||||
AND version = ?`,
|
||||
reason,
|
||||
formatTimestamp(canceledAt),
|
||||
nullableString(event.ActorUserID),
|
||||
formatTimestamp(canceledAt),
|
||||
taskID,
|
||||
creatorSubject,
|
||||
task.Version,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return domain.PurchaseTask{}, repositoryFailure(err)
|
||||
}
|
||||
@@ -363,11 +436,13 @@ func insertTaskEvent(
|
||||
_, err := tx.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO task_events (
|
||||
id, task_id, actor_user_id, event_type, message, occurred_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
id, task_id, actor_user_id, actor_device_id,
|
||||
event_type, message, occurred_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
event.ID,
|
||||
event.TaskID,
|
||||
nullableString(event.ActorUserID),
|
||||
nullableString(event.ActorDeviceID),
|
||||
event.Type,
|
||||
event.Message,
|
||||
formatTimestamp(event.OccurredAt),
|
||||
|
||||
Reference in New Issue
Block a user