fix: allow retry after definitive publish failures
Frontend CI / Frontend (push) Successful in 3m18s
Backend CI / Backend (push) Successful in 16m16s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 26s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Desktop Client Build / Build Desktop Client (push) Successful in 25m45s
Frontend CI / Frontend (push) Successful in 3m18s
Backend CI / Backend (push) Successful in 16m16s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 26s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Desktop Client Build / Build Desktop Client (push) Successful in 25m45s
This commit is contained in:
@@ -95,6 +95,7 @@ type DesktopTaskView struct {
|
||||
DedupKey *string `json:"dedup_key"`
|
||||
ActiveAttemptID *string `json:"active_attempt_id"`
|
||||
LeaseExpiresAt *time.Time `json:"lease_expires_at"`
|
||||
PublishSubmitStartedAt *time.Time `json:"publish_submit_started_at,omitempty"`
|
||||
Attempts int `json:"attempts"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error json.RawMessage `json:"error"`
|
||||
@@ -1563,6 +1564,7 @@ func buildListPublishTasksByStatusesQuery(
|
||||
t.error,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
t.publish_submit_started_at,
|
||||
j.status,
|
||||
j.compliance_blocked_record_id,
|
||||
j.compliance_blocked_at,
|
||||
@@ -1638,28 +1640,29 @@ func scanDesktopTaskRows(rows pgx.Rows) ([]DesktopTaskView, error) {
|
||||
items := make([]DesktopTaskView, 0)
|
||||
for rows.Next() {
|
||||
var (
|
||||
desktopID uuid.UUID
|
||||
jobID uuid.UUID
|
||||
tenantID int64
|
||||
workspaceID int64
|
||||
targetAccountID uuid.UUID
|
||||
targetClientID uuid.UUID
|
||||
platform string
|
||||
kind string
|
||||
payload []byte
|
||||
status string
|
||||
dedupKey pgtype.Text
|
||||
activeAttemptID pgtype.UUID
|
||||
leaseExpiresAt pgtype.Timestamptz
|
||||
attempts int32
|
||||
resultJSON []byte
|
||||
errorJSON []byte
|
||||
createdAt time.Time
|
||||
updatedAt time.Time
|
||||
jobStatus string
|
||||
blockedRecordID pgtype.Int8
|
||||
blockedAt pgtype.Timestamptz
|
||||
blockedReason pgtype.Text
|
||||
desktopID uuid.UUID
|
||||
jobID uuid.UUID
|
||||
tenantID int64
|
||||
workspaceID int64
|
||||
targetAccountID uuid.UUID
|
||||
targetClientID uuid.UUID
|
||||
platform string
|
||||
kind string
|
||||
payload []byte
|
||||
status string
|
||||
dedupKey pgtype.Text
|
||||
activeAttemptID pgtype.UUID
|
||||
leaseExpiresAt pgtype.Timestamptz
|
||||
attempts int32
|
||||
resultJSON []byte
|
||||
errorJSON []byte
|
||||
createdAt time.Time
|
||||
updatedAt time.Time
|
||||
publishSubmitStartedAt pgtype.Timestamptz
|
||||
jobStatus string
|
||||
blockedRecordID pgtype.Int8
|
||||
blockedAt pgtype.Timestamptz
|
||||
blockedReason pgtype.Text
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&desktopID,
|
||||
@@ -1680,6 +1683,7 @@ func scanDesktopTaskRows(rows pgx.Rows) ([]DesktopTaskView, error) {
|
||||
&errorJSON,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
&publishSubmitStartedAt,
|
||||
&jobStatus,
|
||||
&blockedRecordID,
|
||||
&blockedAt,
|
||||
@@ -1708,6 +1712,11 @@ func scanDesktopTaskRows(rows pgx.Rows) ([]DesktopTaskView, error) {
|
||||
value := leaseExpiresAt.Time
|
||||
leaseExpiresAtValue = &value
|
||||
}
|
||||
var publishSubmitStartedAtValue *time.Time
|
||||
if publishSubmitStartedAt.Valid {
|
||||
value := publishSubmitStartedAt.Time
|
||||
publishSubmitStartedAtValue = &value
|
||||
}
|
||||
|
||||
publishJobStatusText := strings.TrimSpace(jobStatus)
|
||||
var publishJobStatus *string
|
||||
@@ -1751,6 +1760,7 @@ func scanDesktopTaskRows(rows pgx.Rows) ([]DesktopTaskView, error) {
|
||||
DedupKey: dedupKeyText,
|
||||
ActiveAttemptID: activeAttemptIDText,
|
||||
LeaseExpiresAt: leaseExpiresAtValue,
|
||||
PublishSubmitStartedAt: publishSubmitStartedAtValue,
|
||||
Attempts: int(attempts),
|
||||
Result: json.RawMessage(resultJSON),
|
||||
Error: json.RawMessage(errorJSON),
|
||||
@@ -1833,24 +1843,25 @@ func buildDesktopTaskView(task *repository.DesktopTask) DesktopTaskView {
|
||||
}
|
||||
|
||||
return DesktopTaskView{
|
||||
ID: task.DesktopID.String(),
|
||||
JobID: task.JobID.String(),
|
||||
TenantID: task.TenantID,
|
||||
WorkspaceID: task.WorkspaceID,
|
||||
TargetAccountID: task.TargetAccountID.String(),
|
||||
TargetClientID: task.TargetClientID.String(),
|
||||
Platform: task.Platform,
|
||||
Kind: task.Kind,
|
||||
Payload: json.RawMessage(task.Payload),
|
||||
Status: normalizeDesktopTaskViewStatus(task.Kind, task.Status),
|
||||
DedupKey: task.DedupKey,
|
||||
ActiveAttemptID: activeAttemptID,
|
||||
LeaseExpiresAt: task.LeaseExpiresAt,
|
||||
Attempts: task.Attempts,
|
||||
Result: json.RawMessage(task.Result),
|
||||
Error: json.RawMessage(task.Error),
|
||||
CreatedAt: task.CreatedAt,
|
||||
UpdatedAt: task.UpdatedAt,
|
||||
ID: task.DesktopID.String(),
|
||||
JobID: task.JobID.String(),
|
||||
TenantID: task.TenantID,
|
||||
WorkspaceID: task.WorkspaceID,
|
||||
TargetAccountID: task.TargetAccountID.String(),
|
||||
TargetClientID: task.TargetClientID.String(),
|
||||
Platform: task.Platform,
|
||||
Kind: task.Kind,
|
||||
Payload: json.RawMessage(task.Payload),
|
||||
Status: normalizeDesktopTaskViewStatus(task.Kind, task.Status),
|
||||
DedupKey: task.DedupKey,
|
||||
ActiveAttemptID: activeAttemptID,
|
||||
LeaseExpiresAt: task.LeaseExpiresAt,
|
||||
PublishSubmitStartedAt: task.PublishSubmitStartedAt,
|
||||
Attempts: task.Attempts,
|
||||
Result: json.RawMessage(task.Result),
|
||||
Error: json.RawMessage(task.Error),
|
||||
CreatedAt: task.CreatedAt,
|
||||
UpdatedAt: task.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1899,8 +1910,10 @@ func normalizeDesktopTaskCompletionStatusForKind(kind string, status string) str
|
||||
|
||||
func normalizeDesktopTaskViewStatus(kind string, status string) string {
|
||||
switch strings.TrimSpace(kind) {
|
||||
case "monitor", "publish":
|
||||
case "monitor":
|
||||
return normalizeDesktopTaskTerminalStatus(status)
|
||||
case "publish":
|
||||
return strings.TrimSpace(status)
|
||||
default:
|
||||
return strings.TrimSpace(status)
|
||||
}
|
||||
|
||||
@@ -257,14 +257,14 @@ func TestClassifyLeaseErrorDefersQueuedMonitorTask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDesktopTaskViewStatusMapsMonitorAndPublishUnknownToFailed(t *testing.T) {
|
||||
func TestNormalizeDesktopTaskViewStatusMapsMonitorUnknownToFailedButKeepsPublishUnknown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := normalizeDesktopTaskViewStatus("monitor", "unknown"); got != "failed" {
|
||||
t.Fatalf("monitor unknown view status = %q, want failed", got)
|
||||
}
|
||||
if got := normalizeDesktopTaskViewStatus("publish", "unknown"); got != "failed" {
|
||||
t.Fatalf("publish unknown view status = %q, want failed", got)
|
||||
if got := normalizeDesktopTaskViewStatus("publish", "unknown"); got != "unknown" {
|
||||
t.Fatalf("publish unknown view status = %q, want unknown", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -218,6 +219,14 @@ func (s *PublishJobService) Create(ctx context.Context, actor auth.Actor, req Cr
|
||||
if err := lockDesktopPublishDedupKeys(ctx, tx, dedupKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
releasedPublishOutcomes := make([]*desktopPublishSyncOutcome, 0)
|
||||
if len(dedupKeys) > 0 {
|
||||
outcomes, releaseErr := releaseDefinitiveFailedUnknownPublishTasks(ctx, tx, actor.TenantID, actor.PrimaryWorkspaceID, dedupKeys)
|
||||
if releaseErr != nil {
|
||||
return nil, releaseErr
|
||||
}
|
||||
releasedPublishOutcomes = append(releasedPublishOutcomes, outcomes...)
|
||||
}
|
||||
existingTasks, err := loadExistingPublishRecordTargets(ctx, tx, actor.TenantID, actor.PrimaryWorkspaceID, articleID, targets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -338,6 +347,11 @@ func (s *PublishJobService) Create(ctx context.Context, actor auth.Actor, req Cr
|
||||
return nil, response.ErrInternal(50100, "desktop_publish_job_commit_failed", "failed to commit desktop publish job")
|
||||
}
|
||||
invalidateArticleCaches(ctx, s.cache, actor.TenantID, &articleID)
|
||||
for _, outcome := range releasedPublishOutcomes {
|
||||
if outcome != nil {
|
||||
invalidateArticleCaches(ctx, s.cache, outcome.TenantID, &outcome.ArticleID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, task := range createdTasks {
|
||||
dispatchEvent := DesktopDispatchEventFromTask(task, "task_available")
|
||||
@@ -514,6 +528,155 @@ func lockDesktopPublishDedupKeys(ctx context.Context, tx pgx.Tx, keys []string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func releaseDefinitiveFailedUnknownPublishTasks(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID int64,
|
||||
workspaceID int64,
|
||||
dedupKeys []string,
|
||||
) ([]*desktopPublishSyncOutcome, error) {
|
||||
if len(dedupKeys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rows, err := tx.Query(ctx, `
|
||||
SELECT desktop_id, tenant_id, workspace_id, platform_id, kind, payload, status, result, error
|
||||
FROM desktop_tasks
|
||||
WHERE tenant_id = $1
|
||||
AND workspace_id = $2
|
||||
AND kind = 'publish'
|
||||
AND status = 'unknown'
|
||||
AND dedup_key = ANY($3::text[])
|
||||
FOR UPDATE
|
||||
`, tenantID, workspaceID, dedupKeys)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50131, "desktop_publish_dedup_cleanup_failed", "failed to inspect stale publish deduplication tasks")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tasks := make([]repository.DesktopTask, 0)
|
||||
for rows.Next() {
|
||||
var task repository.DesktopTask
|
||||
if scanErr := rows.Scan(
|
||||
&task.DesktopID,
|
||||
&task.TenantID,
|
||||
&task.WorkspaceID,
|
||||
&task.Platform,
|
||||
&task.Kind,
|
||||
&task.Payload,
|
||||
&task.Status,
|
||||
&task.Result,
|
||||
&task.Error,
|
||||
); scanErr != nil {
|
||||
return nil, response.ErrInternal(50131, "desktop_publish_dedup_cleanup_failed", "failed to parse stale publish deduplication task")
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50131, "desktop_publish_dedup_cleanup_failed", "failed to iterate stale publish deduplication tasks")
|
||||
}
|
||||
|
||||
outcomes := make([]*desktopPublishSyncOutcome, 0)
|
||||
for index := range tasks {
|
||||
task := tasks[index]
|
||||
if !publishUnknownTaskHasDefinitiveFailure(&task) {
|
||||
continue
|
||||
}
|
||||
updated, updateErr := markUnknownPublishTaskDefinitiveFailed(ctx, tx, &task)
|
||||
if updateErr != nil {
|
||||
return nil, updateErr
|
||||
}
|
||||
outcome, syncErr := syncDesktopPublishTaskState(ctx, tx, updated)
|
||||
if syncErr != nil {
|
||||
return nil, syncErr
|
||||
}
|
||||
if outcome != nil {
|
||||
outcomes = append(outcomes, outcome)
|
||||
}
|
||||
}
|
||||
return outcomes, nil
|
||||
}
|
||||
|
||||
func markUnknownPublishTaskDefinitiveFailed(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
task *repository.DesktopTask,
|
||||
) (*repository.DesktopTask, error) {
|
||||
if task == nil {
|
||||
return nil, response.ErrNotFound(40482, "desktop_task_not_found", "desktop task not found")
|
||||
}
|
||||
row := tx.QueryRow(ctx, `
|
||||
UPDATE desktop_tasks
|
||||
SET status = 'failed',
|
||||
active_attempt_id = NULL,
|
||||
lease_token_hash = NULL,
|
||||
lease_expires_at = NULL,
|
||||
publish_submit_started_at = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE desktop_id = $1
|
||||
AND workspace_id = $2
|
||||
AND status = 'unknown'
|
||||
RETURNING desktop_id, tenant_id, workspace_id, platform_id, kind, payload, status, result, error
|
||||
`, task.DesktopID, task.WorkspaceID)
|
||||
var updated repository.DesktopTask
|
||||
if err := row.Scan(
|
||||
&updated.DesktopID,
|
||||
&updated.TenantID,
|
||||
&updated.WorkspaceID,
|
||||
&updated.Platform,
|
||||
&updated.Kind,
|
||||
&updated.Payload,
|
||||
&updated.Status,
|
||||
&updated.Result,
|
||||
&updated.Error,
|
||||
); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrConflict(40985, "desktop_task_reconcile_conflict", "desktop task is not waiting for reconcile")
|
||||
}
|
||||
return nil, response.ErrInternal(50131, "desktop_publish_dedup_cleanup_failed", "failed to release stale publish deduplication task")
|
||||
}
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
func publishUnknownTaskHasDefinitiveFailure(task *repository.DesktopTask) bool {
|
||||
if task == nil || task.Kind != "publish" || task.Status != "unknown" {
|
||||
return false
|
||||
}
|
||||
errorPayload := unmarshalJSONObject(task.Error)
|
||||
if boolValueFromMap(errorPayload, "publish_submit_uncertain") {
|
||||
return false
|
||||
}
|
||||
combined := strings.ToLower(strings.Join([]string{
|
||||
stringPointerValue(extractStringPointer(errorPayload, "code", "error_code")),
|
||||
stringPointerValue(extractStringPointer(errorPayload, "message", "error_msg", "detail")),
|
||||
}, " "))
|
||||
if combined == "" {
|
||||
return false
|
||||
}
|
||||
return definitivePublishFailurePattern.MatchString(combined)
|
||||
}
|
||||
|
||||
func boolValueFromMap(payload map[string]any, key string) bool {
|
||||
if payload == nil {
|
||||
return false
|
||||
}
|
||||
value, ok := payload[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
return typed
|
||||
case string:
|
||||
normalized := strings.TrimSpace(strings.ToLower(typed))
|
||||
return normalized == "true" || normalized == "1" || normalized == "yes"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var definitivePublishFailurePattern = regexp.MustCompile(`csrf|forbidden|\b403\b|not_logged_in|login_required|token_missing|cookie_missing|challenge_required|risk_control|captcha|验证码|滑块|人机验证|安全验证|风控|风险|参数错误|内容为空|image_fetch_failed|cover_fetch_failed|upload_failed|signature_failed`)
|
||||
|
||||
func loadExistingPublishRecordTargets(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
@@ -567,7 +730,7 @@ func loadExistingPublishRecordTargets(
|
||||
AND pr.article_id = $3
|
||||
AND pr.platform_account_id = ANY($4::bigint[])
|
||||
AND pr.deleted_at IS NULL
|
||||
AND pr.status IN ('queued', 'publishing', 'success')
|
||||
AND (pr.status IN ('queued', 'publishing', 'success') OR dt.status = 'unknown')
|
||||
AND dt.status IN ('queued', 'in_progress', 'succeeded', 'unknown')
|
||||
ORDER BY pr.platform_account_id,
|
||||
CASE
|
||||
@@ -785,10 +948,18 @@ func (s *PublishJobService) requeueUnknownPublishTask(
|
||||
return nil, response.ErrInternal(50094, "desktop_task_reconcile_failed", "failed to reconcile desktop task")
|
||||
}
|
||||
|
||||
publishOutcome, err := syncDesktopPublishTaskState(ctx, tx, requeued)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, response.ErrInternal(50106, "desktop_task_reconcile_commit_failed", "failed to commit desktop task reconcile")
|
||||
}
|
||||
|
||||
if publishOutcome != nil {
|
||||
invalidateArticleCaches(ctx, s.cache, publishOutcome.TenantID, &publishOutcome.ArticleID)
|
||||
}
|
||||
publishDesktopTaskLifecycleEvent(ctx, s.messaging, s.logger, requeued, "task_available")
|
||||
return createPublishJobResponseForRequeuedTask(requeued), nil
|
||||
}
|
||||
|
||||
@@ -203,6 +203,32 @@ func TestCreatePublishJobResponseForRequeuedTaskReportsRequeuedTaskID(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishUnknownTaskHasDefinitiveFailureReleasesCSRF403(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
task := &repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "unknown",
|
||||
Error: []byte(`{"error_code":403,"error_msg":"CSRF token invalid"}`),
|
||||
}
|
||||
if !publishUnknownTaskHasDefinitiveFailure(task) {
|
||||
t.Fatal("unknown publish task with CSRF 403 should be treated as definitive failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishUnknownTaskHasDefinitiveFailureKeepsSubmitUncertain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
task := &repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "unknown",
|
||||
Error: []byte(`{"publish_submit_uncertain":true,"message":"network interrupted after submit"}`),
|
||||
}
|
||||
if publishUnknownTaskHasDefinitiveFailure(task) {
|
||||
t.Fatal("submit-uncertain unknown publish task must stay in dedup set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadExistingPublishRecordTargetsDedupeUnknownTasks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -220,6 +246,9 @@ func TestLoadExistingPublishRecordTargetsDedupeUnknownTasks(t *testing.T) {
|
||||
if !strings.Contains(normalizedSQL, "dt.status IN ('queued', 'in_progress', 'succeeded', 'unknown')") {
|
||||
t.Fatalf("publish dedup lookup should include unknown task status; query:\n%s", tx.sql)
|
||||
}
|
||||
if !strings.Contains(normalizedSQL, "OR dt.status = 'unknown'") {
|
||||
t.Fatalf("publish dedup lookup should include unknown tasks even when publish record is failed; query:\n%s", tx.sql)
|
||||
}
|
||||
if !strings.Contains(normalizedSQL, "AND dt.tenant_id = $1") {
|
||||
t.Fatalf("publish dedup lookup should constrain desktop_tasks by tenant for the payload index; query:\n%s", tx.sql)
|
||||
}
|
||||
|
||||
@@ -37,37 +37,38 @@ type CreateDesktopPublishJobParams struct {
|
||||
}
|
||||
|
||||
type DesktopTask struct {
|
||||
DesktopID uuid.UUID
|
||||
JobID uuid.UUID
|
||||
TenantID int64
|
||||
WorkspaceID int64
|
||||
TargetAccountID uuid.UUID
|
||||
TargetClientID uuid.UUID
|
||||
Platform string
|
||||
Kind string
|
||||
Payload []byte
|
||||
Status string
|
||||
Priority int
|
||||
Lane string
|
||||
LaneWeight int
|
||||
Source string
|
||||
SchedulerGroupKey *string
|
||||
MonitorTaskID *int64
|
||||
SupersedesTaskID *uuid.UUID
|
||||
ControlFlags []byte
|
||||
InterruptGeneration int
|
||||
DedupKey *string
|
||||
ActiveAttemptID *uuid.UUID
|
||||
LeaseExpiresAt *time.Time
|
||||
Attempts int
|
||||
Result []byte
|
||||
Error []byte
|
||||
StartedAt *time.Time
|
||||
InterruptedAt *time.Time
|
||||
InterruptReason *string
|
||||
EnqueuedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DesktopID uuid.UUID
|
||||
JobID uuid.UUID
|
||||
TenantID int64
|
||||
WorkspaceID int64
|
||||
TargetAccountID uuid.UUID
|
||||
TargetClientID uuid.UUID
|
||||
Platform string
|
||||
Kind string
|
||||
Payload []byte
|
||||
Status string
|
||||
Priority int
|
||||
Lane string
|
||||
LaneWeight int
|
||||
Source string
|
||||
SchedulerGroupKey *string
|
||||
MonitorTaskID *int64
|
||||
SupersedesTaskID *uuid.UUID
|
||||
ControlFlags []byte
|
||||
InterruptGeneration int
|
||||
DedupKey *string
|
||||
ActiveAttemptID *uuid.UUID
|
||||
LeaseExpiresAt *time.Time
|
||||
Attempts int
|
||||
Result []byte
|
||||
Error []byte
|
||||
StartedAt *time.Time
|
||||
InterruptedAt *time.Time
|
||||
InterruptReason *string
|
||||
EnqueuedAt time.Time
|
||||
PublishSubmitStartedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CreateDesktopTaskParams struct {
|
||||
@@ -355,37 +356,38 @@ func desktopPublishJobFromGenerated(row generated.DesktopPublishJob) *DesktopPub
|
||||
|
||||
func desktopTaskFromGenerated(row generated.DesktopTask) *DesktopTask {
|
||||
return &DesktopTask{
|
||||
DesktopID: uuidFromPG(row.DesktopID),
|
||||
JobID: uuidFromPG(row.JobID),
|
||||
TenantID: row.TenantID,
|
||||
WorkspaceID: row.WorkspaceID,
|
||||
TargetAccountID: uuidFromPG(row.TargetAccountID),
|
||||
TargetClientID: uuidFromPG(row.TargetClientID),
|
||||
Platform: row.PlatformID,
|
||||
Kind: row.Kind,
|
||||
Payload: row.Payload,
|
||||
Status: row.Status,
|
||||
Priority: int(row.Priority),
|
||||
Lane: row.Lane,
|
||||
LaneWeight: int(row.LaneWeight),
|
||||
Source: row.Source,
|
||||
SchedulerGroupKey: nullableText(row.SchedulerGroupKey),
|
||||
MonitorTaskID: nullableInt64(row.MonitorTaskID),
|
||||
SupersedesTaskID: nullableUUID(row.SupersedesTaskID),
|
||||
ControlFlags: row.ControlFlags,
|
||||
InterruptGeneration: int(row.InterruptGeneration),
|
||||
DedupKey: nullableText(row.DedupKey),
|
||||
ActiveAttemptID: nullableUUID(row.ActiveAttemptID),
|
||||
LeaseExpiresAt: optionalTime(row.LeaseExpiresAt),
|
||||
Attempts: int(row.Attempts),
|
||||
Result: row.Result,
|
||||
Error: row.Error,
|
||||
StartedAt: optionalTime(row.StartedAt),
|
||||
InterruptedAt: optionalTime(row.InterruptedAt),
|
||||
InterruptReason: nullableText(row.InterruptReason),
|
||||
EnqueuedAt: timeFromTimestamp(row.EnqueuedAt),
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
DesktopID: uuidFromPG(row.DesktopID),
|
||||
JobID: uuidFromPG(row.JobID),
|
||||
TenantID: row.TenantID,
|
||||
WorkspaceID: row.WorkspaceID,
|
||||
TargetAccountID: uuidFromPG(row.TargetAccountID),
|
||||
TargetClientID: uuidFromPG(row.TargetClientID),
|
||||
Platform: row.PlatformID,
|
||||
Kind: row.Kind,
|
||||
Payload: row.Payload,
|
||||
Status: row.Status,
|
||||
Priority: int(row.Priority),
|
||||
Lane: row.Lane,
|
||||
LaneWeight: int(row.LaneWeight),
|
||||
Source: row.Source,
|
||||
SchedulerGroupKey: nullableText(row.SchedulerGroupKey),
|
||||
MonitorTaskID: nullableInt64(row.MonitorTaskID),
|
||||
SupersedesTaskID: nullableUUID(row.SupersedesTaskID),
|
||||
ControlFlags: row.ControlFlags,
|
||||
InterruptGeneration: int(row.InterruptGeneration),
|
||||
DedupKey: nullableText(row.DedupKey),
|
||||
ActiveAttemptID: nullableUUID(row.ActiveAttemptID),
|
||||
LeaseExpiresAt: optionalTime(row.LeaseExpiresAt),
|
||||
Attempts: int(row.Attempts),
|
||||
Result: row.Result,
|
||||
Error: row.Error,
|
||||
StartedAt: optionalTime(row.StartedAt),
|
||||
InterruptedAt: optionalTime(row.InterruptedAt),
|
||||
InterruptReason: nullableText(row.InterruptReason),
|
||||
EnqueuedAt: timeFromTimestamp(row.EnqueuedAt),
|
||||
PublishSubmitStartedAt: optionalTime(row.PublishSubmitStartedAt),
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -849,6 +849,10 @@ SET status = CASE
|
||||
active_attempt_id = NULL,
|
||||
lease_token_hash = NULL,
|
||||
lease_expires_at = NULL,
|
||||
publish_submit_started_at = CASE
|
||||
WHEN $1::text = 'retry' THEN NULL
|
||||
ELSE publish_submit_started_at
|
||||
END,
|
||||
attempts = CASE WHEN $1::text = 'retry' THEN 0 ELSE attempts END,
|
||||
updated_at = now()
|
||||
WHERE desktop_id = $4
|
||||
|
||||
@@ -39,6 +39,10 @@ func TestReconcileDesktopTaskRetryResetsAttempts(t *testing.T) {
|
||||
if !strings.Contains(query, "attempts = CASE WHEN $1::text = 'retry' THEN 0 ELSE attempts END") {
|
||||
t.Fatalf("retry reconcile must reset attempts so manual retry can be leased; query:\n%s", query)
|
||||
}
|
||||
if !strings.Contains(query, "publish_submit_started_at = CASE") ||
|
||||
!strings.Contains(query, "WHEN $1::text = 'retry' THEN NULL") {
|
||||
t.Fatalf("retry reconcile must clear stale publish submit marker; query:\n%s", query)
|
||||
}
|
||||
if strings.Contains(query, "attempts + CASE") {
|
||||
t.Fatalf("retry reconcile must not increment attempts; query:\n%s", query)
|
||||
}
|
||||
|
||||
@@ -225,6 +225,10 @@ SET status = CASE
|
||||
active_attempt_id = NULL,
|
||||
lease_token_hash = NULL,
|
||||
lease_expires_at = NULL,
|
||||
publish_submit_started_at = CASE
|
||||
WHEN sqlc.arg(status)::text = 'retry' THEN NULL
|
||||
ELSE publish_submit_started_at
|
||||
END,
|
||||
attempts = CASE WHEN sqlc.arg(status)::text = 'retry' THEN 0 ELSE attempts END,
|
||||
updated_at = now()
|
||||
WHERE desktop_id = sqlc.arg(desktop_id)
|
||||
|
||||
Reference in New Issue
Block a user