feat(tasks): implement atomic claims and leases
This commit is contained in:
@@ -32,17 +32,20 @@ type User struct {
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string
|
||||
Name string
|
||||
TokenHash string
|
||||
BoundUserID *string
|
||||
AppVersion *string
|
||||
AndroidVersion *string
|
||||
PDDVersion *string
|
||||
LastSeenAt *time.Time
|
||||
IsEnabled bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
Name string
|
||||
TokenHash string
|
||||
BoundUserID *string
|
||||
AppVersion *string
|
||||
AndroidVersion *string
|
||||
PDDVersion *string
|
||||
LastSeenAt *time.Time
|
||||
ReadinessAt *time.Time
|
||||
AccessibilityEnabled bool
|
||||
PDDInstalled bool
|
||||
IsEnabled bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type AdminSession struct {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLifecycleStatusPredicates(t *testing.T) {
|
||||
statuses := []TaskStatus{
|
||||
TaskStatusPending,
|
||||
TaskStatusClaimed,
|
||||
TaskStatusRunning,
|
||||
TaskStatusWaitingConfirmation,
|
||||
TaskStatusSucceeded,
|
||||
TaskStatusFailed,
|
||||
TaskStatusCanceled,
|
||||
}
|
||||
for _, status := range statuses {
|
||||
t.Run(string(status), func(t *testing.T) {
|
||||
active := status == TaskStatusClaimed ||
|
||||
status == TaskStatusRunning ||
|
||||
status == TaskStatusWaitingConfirmation
|
||||
if IsActiveTaskStatus(status) != active {
|
||||
t.Fatalf("IsActiveTaskStatus(%s) = %v", status, !active)
|
||||
}
|
||||
if CanStart(status) != (status == TaskStatusClaimed) {
|
||||
t.Fatalf("CanStart(%s) mismatch", status)
|
||||
}
|
||||
heartbeat := status == TaskStatusRunning ||
|
||||
status == TaskStatusWaitingConfirmation
|
||||
if CanHeartbeat(status) != heartbeat {
|
||||
t.Fatalf("CanHeartbeat(%s) mismatch", status)
|
||||
}
|
||||
if CanRelease(status) != (status == TaskStatusClaimed) {
|
||||
t.Fatalf("CanRelease(%s) mismatch", status)
|
||||
}
|
||||
immediateCancel := status == TaskStatusPending ||
|
||||
status == TaskStatusClaimed
|
||||
if CanAdminCancelImmediately(status) != immediateCancel {
|
||||
t.Fatalf("CanAdminCancelImmediately(%s) mismatch", status)
|
||||
}
|
||||
if CanRequestCancel(status) != heartbeat ||
|
||||
CanAcknowledgeCancel(status) != heartbeat {
|
||||
t.Fatalf("cancel predicates for %s mismatch", status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,38 +33,62 @@ const (
|
||||
)
|
||||
|
||||
type PurchaseTask struct {
|
||||
ID string
|
||||
CreatorSubject string
|
||||
CreatedByUserID *string
|
||||
SourceRef *string
|
||||
Title string
|
||||
Description string
|
||||
SKU string
|
||||
ImageAssetID string
|
||||
Quantity int
|
||||
MaxBudgetCents *int64
|
||||
Currency string
|
||||
Status TaskStatus
|
||||
Version int64
|
||||
CancelReason *string
|
||||
CanceledAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
CreatorSubject string
|
||||
CreatedByUserID *string
|
||||
SourceRef *string
|
||||
Title string
|
||||
Description string
|
||||
SKU string
|
||||
ImageAssetID string
|
||||
Quantity int
|
||||
MaxBudgetCents *int64
|
||||
Currency string
|
||||
Status TaskStatus
|
||||
Version int64
|
||||
ClaimedByUserID *string
|
||||
ClaimedByDeviceID *string
|
||||
ClaimGeneration int64
|
||||
ClaimTokenHash *string
|
||||
ClaimIssuedAt *time.Time
|
||||
ClaimExpiresAt *time.Time
|
||||
CancelReason *string
|
||||
CancelRequestedAt *time.Time
|
||||
CancelRequestedByUserID *string
|
||||
CanceledAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type TaskEvent struct {
|
||||
ID string
|
||||
TaskID string
|
||||
ActorUserID *string
|
||||
Type string
|
||||
Message string
|
||||
OccurredAt time.Time
|
||||
ID string
|
||||
TaskID string
|
||||
ActorUserID *string
|
||||
ActorDeviceID *string
|
||||
Type string
|
||||
Message string
|
||||
OccurredAt time.Time
|
||||
}
|
||||
|
||||
type TaskExecution struct {
|
||||
ID string
|
||||
TaskID string
|
||||
AttemptNo int64
|
||||
ClaimGeneration int64
|
||||
UserID string
|
||||
DeviceID string
|
||||
CurrentStep string
|
||||
OrderSubmitted bool
|
||||
StartedAt time.Time
|
||||
LastHeartbeatAt *time.Time
|
||||
FinishedAt *time.Time
|
||||
}
|
||||
|
||||
type TaskDetail struct {
|
||||
Task PurchaseTask
|
||||
Asset Asset
|
||||
Events []TaskEvent
|
||||
Task PurchaseTask
|
||||
Asset Asset
|
||||
Execution *TaskExecution
|
||||
Events []TaskEvent
|
||||
}
|
||||
|
||||
type TaskValidationError struct {
|
||||
@@ -192,7 +216,45 @@ func FormatOptionalCNY(cents *int64) *string {
|
||||
}
|
||||
|
||||
func CanCancel(status TaskStatus) bool {
|
||||
return status == TaskStatusPending
|
||||
return CanAdminCancelImmediately(status) || CanRequestCancel(status)
|
||||
}
|
||||
|
||||
func IsActiveTaskStatus(status TaskStatus) bool {
|
||||
switch status {
|
||||
case TaskStatusClaimed,
|
||||
TaskStatusRunning,
|
||||
TaskStatusWaitingConfirmation:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func CanStart(status TaskStatus) bool {
|
||||
return status == TaskStatusClaimed
|
||||
}
|
||||
|
||||
func CanHeartbeat(status TaskStatus) bool {
|
||||
return status == TaskStatusRunning ||
|
||||
status == TaskStatusWaitingConfirmation
|
||||
}
|
||||
|
||||
func CanRelease(status TaskStatus) bool {
|
||||
return status == TaskStatusClaimed
|
||||
}
|
||||
|
||||
func CanAdminCancelImmediately(status TaskStatus) bool {
|
||||
return status == TaskStatusPending ||
|
||||
status == TaskStatusClaimed
|
||||
}
|
||||
|
||||
func CanRequestCancel(status TaskStatus) bool {
|
||||
return status == TaskStatusRunning ||
|
||||
status == TaskStatusWaitingConfirmation
|
||||
}
|
||||
|
||||
func CanAcknowledgeCancel(status TaskStatus) bool {
|
||||
return CanRequestCancel(status)
|
||||
}
|
||||
|
||||
func IsValidTaskStatus(status TaskStatus) bool {
|
||||
|
||||
@@ -107,11 +107,8 @@ func TestParseOptionalCNYRejectsInvalidAndOverflow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanCancelOnlyPending(t *testing.T) {
|
||||
func TestCanCancelOnlyNonTerminalTask(t *testing.T) {
|
||||
for _, status := range []TaskStatus{
|
||||
TaskStatusClaimed,
|
||||
TaskStatusRunning,
|
||||
TaskStatusWaitingConfirmation,
|
||||
TaskStatusSucceeded,
|
||||
TaskStatusFailed,
|
||||
TaskStatusCanceled,
|
||||
@@ -120,8 +117,15 @@ func TestCanCancelOnlyPending(t *testing.T) {
|
||||
t.Fatalf("CanCancel(%s) = true", status)
|
||||
}
|
||||
}
|
||||
if !CanCancel(TaskStatusPending) {
|
||||
t.Fatal("CanCancel(PENDING) = false")
|
||||
for _, status := range []TaskStatus{
|
||||
TaskStatusPending,
|
||||
TaskStatusClaimed,
|
||||
TaskStatusRunning,
|
||||
TaskStatusWaitingConfirmation,
|
||||
} {
|
||||
if !CanCancel(status) {
|
||||
t.Fatalf("CanCancel(%s) = false", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user