feat(tasks): implement atomic claims and leases
This commit is contained in:
@@ -14,7 +14,7 @@ import (
|
||||
sqlite3 "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
const timestampLayout = time.RFC3339Nano
|
||||
const storageTimestampLayout = "2006-01-02T15:04:05.000000000Z"
|
||||
|
||||
type queryRower interface {
|
||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||
@@ -52,7 +52,14 @@ func scanTask(scanner rowScanner) (domain.PurchaseTask, error) {
|
||||
var createdByUserID sql.NullString
|
||||
var sourceRef sql.NullString
|
||||
var maxBudget sql.NullInt64
|
||||
var claimedByUserID sql.NullString
|
||||
var claimedByDeviceID sql.NullString
|
||||
var claimTokenHash sql.NullString
|
||||
var claimIssuedAt sql.NullString
|
||||
var claimExpiresAt sql.NullString
|
||||
var cancelReason sql.NullString
|
||||
var cancelRequestedAt sql.NullString
|
||||
var cancelRequestedByUserID sql.NullString
|
||||
var canceledAt sql.NullString
|
||||
var createdAt string
|
||||
var updatedAt string
|
||||
@@ -70,7 +77,15 @@ func scanTask(scanner rowScanner) (domain.PurchaseTask, error) {
|
||||
&task.Currency,
|
||||
&task.Status,
|
||||
&task.Version,
|
||||
&claimedByUserID,
|
||||
&claimedByDeviceID,
|
||||
&task.ClaimGeneration,
|
||||
&claimTokenHash,
|
||||
&claimIssuedAt,
|
||||
&claimExpiresAt,
|
||||
&cancelReason,
|
||||
&cancelRequestedAt,
|
||||
&cancelRequestedByUserID,
|
||||
&canceledAt,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
@@ -87,15 +102,36 @@ func scanTask(scanner rowScanner) (domain.PurchaseTask, error) {
|
||||
if maxBudget.Valid {
|
||||
task.MaxBudgetCents = &maxBudget.Int64
|
||||
}
|
||||
if claimedByUserID.Valid {
|
||||
task.ClaimedByUserID = &claimedByUserID.String
|
||||
}
|
||||
if claimedByDeviceID.Valid {
|
||||
task.ClaimedByDeviceID = &claimedByDeviceID.String
|
||||
}
|
||||
if claimTokenHash.Valid {
|
||||
task.ClaimTokenHash = &claimTokenHash.String
|
||||
}
|
||||
task.ClaimIssuedAt, err = parseNullableTimestamp(claimIssuedAt)
|
||||
if err != nil {
|
||||
return domain.PurchaseTask{}, err
|
||||
}
|
||||
task.ClaimExpiresAt, err = parseNullableTimestamp(claimExpiresAt)
|
||||
if err != nil {
|
||||
return domain.PurchaseTask{}, err
|
||||
}
|
||||
if cancelReason.Valid {
|
||||
task.CancelReason = &cancelReason.String
|
||||
}
|
||||
if canceledAt.Valid {
|
||||
value, err := parseTimestamp(canceledAt.String)
|
||||
if err != nil {
|
||||
return domain.PurchaseTask{}, err
|
||||
}
|
||||
task.CanceledAt = &value
|
||||
task.CancelRequestedAt, err = parseNullableTimestamp(cancelRequestedAt)
|
||||
if err != nil {
|
||||
return domain.PurchaseTask{}, err
|
||||
}
|
||||
if cancelRequestedByUserID.Valid {
|
||||
task.CancelRequestedByUserID = &cancelRequestedByUserID.String
|
||||
}
|
||||
task.CanceledAt, err = parseNullableTimestamp(canceledAt)
|
||||
if err != nil {
|
||||
return domain.PurchaseTask{}, err
|
||||
}
|
||||
task.CreatedAt, err = parseTimestamp(createdAt)
|
||||
if err != nil {
|
||||
@@ -108,6 +144,42 @@ func scanTask(scanner rowScanner) (domain.PurchaseTask, error) {
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func scanExecution(scanner rowScanner) (domain.TaskExecution, error) {
|
||||
var execution domain.TaskExecution
|
||||
var lastHeartbeatAt sql.NullString
|
||||
var finishedAt sql.NullString
|
||||
var startedAt string
|
||||
err := scanner.Scan(
|
||||
&execution.ID,
|
||||
&execution.TaskID,
|
||||
&execution.AttemptNo,
|
||||
&execution.ClaimGeneration,
|
||||
&execution.UserID,
|
||||
&execution.DeviceID,
|
||||
&execution.CurrentStep,
|
||||
&execution.OrderSubmitted,
|
||||
&startedAt,
|
||||
&lastHeartbeatAt,
|
||||
&finishedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.TaskExecution{}, err
|
||||
}
|
||||
execution.StartedAt, err = parseTimestamp(startedAt)
|
||||
if err != nil {
|
||||
return domain.TaskExecution{}, err
|
||||
}
|
||||
execution.LastHeartbeatAt, err = parseNullableTimestamp(lastHeartbeatAt)
|
||||
if err != nil {
|
||||
return domain.TaskExecution{}, err
|
||||
}
|
||||
execution.FinishedAt, err = parseNullableTimestamp(finishedAt)
|
||||
if err != nil {
|
||||
return domain.TaskExecution{}, err
|
||||
}
|
||||
return execution, nil
|
||||
}
|
||||
|
||||
func getAssetByID(
|
||||
ctx context.Context,
|
||||
queryer queryRower,
|
||||
@@ -144,7 +216,10 @@ func getTaskByID(
|
||||
`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 = ? AND id = ?`,
|
||||
creatorSubject,
|
||||
@@ -218,11 +293,11 @@ func insertIdempotency(
|
||||
}
|
||||
|
||||
func formatTimestamp(value time.Time) string {
|
||||
return value.UTC().Format(timestampLayout)
|
||||
return value.UTC().Format(storageTimestampLayout)
|
||||
}
|
||||
|
||||
func parseTimestamp(value string) (time.Time, error) {
|
||||
parsed, err := time.Parse(timestampLayout, value)
|
||||
parsed, err := time.Parse(time.RFC3339Nano, value)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf(
|
||||
"%w: invalid stored timestamp",
|
||||
@@ -232,6 +307,17 @@ func parseTimestamp(value string) (time.Time, error) {
|
||||
return parsed.UTC(), nil
|
||||
}
|
||||
|
||||
func parseNullableTimestamp(value sql.NullString) (*time.Time, error) {
|
||||
if !value.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := parseTimestamp(value.String)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func nullableString(value *string) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user