Files
worker/app/models/task.go
Gleb Tv 2c7a0236da feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
2026-07-13 17:55:14 +03:00

146 строки
5.7 KiB
Go

package models
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm/clause"
"rsgit.ru/rsmon/rsmon/app/models/concerns"
)
// Task kinds stored in the tasks.kind column. The plan (docs/plans/worker-notifier-mvp.md
// section 4.1) reserves the same enum for checks and notifications; this phase only
// emits notification rows but the enum is shared so the selector can stay a single
// function.
const (
TaskKindNotification = "notification"
TaskKindCheck = "check"
TaskKindServerMetric = "server_metric"
)
// Task states for the durable task envelope.
const (
TaskStateQueued = "queued"
TaskStateLeased = "leased"
TaskStateSucceeded = "succeeded"
TaskStateFailedRetry = "failed_retry"
TaskStateFailedPerm = "failed_perm"
TaskStateDead = "dead"
)
// Notification result statuses reported by the worker (mirrors the wire enum so the
// result handler can decode without re-typing the constants).
const (
NotificationResultDelivered = "delivered"
NotificationResultRetryable = "retryable"
NotificationResultPermanent = "permanent"
NotificationResultPartial = "partial"
)
// SkipLockedClause is the SELECT ... FOR UPDATE SKIP LOCKED clause used by
// every worker-pool selector (checks in check_jobs.ChecksForWorker and
// tasks in task_selector.TasksForWorker / TasksForWorkerNotification).
// Sharing the value keeps the SQL identical across selectors so goconst
// does not flag the literal, and a future change (e.g. NOWAIT) only has
// to touch one place.
var SkipLockedClause = clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}
// Task is the durable envelope for both check and notification work executed by the
// distributed worker pool. Selection uses FOR UPDATE SKIP LOCKED per worker poll so
// a single primary key or sequence never becomes the bottleneck.
//
// Phase 1 (this commit) only emits notification tasks. The `kind` discriminator and
// capability filters are designed to accept checks in phase 2 without a schema change.
type Task struct {
concerns.Model
JobID string `gorm:"uniqueIndex;size:64" json:"job_id"`
Kind string `gorm:"size:32;index" json:"kind"`
State string `gorm:"size:32;index" json:"state"`
LastError string `gorm:"type:text" json:"last_error"`
// Tenancy + audit anchor. AccountID is required for the capability match in
// TasksForWorker; monitor_id / message_id / contact_id are denormalized for
// fast admin queries.
AccountID int64 `gorm:"index" json:"account_id"`
MonitorID *int64 `gorm:"index" json:"monitor_id,omitempty"`
CheckID *int64 `json:"check_id,omitempty"`
MessageID *int64 `json:"message_id,omitempty"`
ContactID *int64 `json:"contact_id,omitempty"`
// Payload is the kind-specific blob the worker needs to execute. For
// notifications the producer pre-renders subject/body so the worker does not
// need templating context (see RenderNotificationContent in internal/notifier).
Payload datatypes.JSON `gorm:"type:jsonb" json:"payload"`
// Scheduling + retry envelope. NotBefore is set to NOW() by the producer and
// bumped by the result handler on retryable failures. Deadline is a soft cap
// the selector can use to skip stale tasks.
NotBefore time.Time `json:"not_before"`
Deadline *time.Time `json:"deadline,omitempty"`
// LeaseOwner + LeaseExpiresAt are owned by the selector while the task is
// in state=leased. The reaper clears them when the lease expires.
LeaseOwner string `gorm:"size:128" json:"lease_owner"`
LeaseToken string `gorm:"size:64" json:"-"`
LeaseExpiresAt *time.Time `json:"lease_expires_at,omitempty"`
Attempts int `json:"attempts"`
MaxAttempts int `json:"max_attempts"`
// IdempotencyKey is unique per logical event so a retry of the producer's
// enqueue never produces a second Task row. See EnqueueNotificationTask.
IdempotencyKey string `gorm:"uniqueIndex;size:255" json:"idempotency_key"`
// Result holds the most recent worker result body (NotificationResultReport or
// CheckResultReport shape, depending on Kind). It is JSONB so the admin UI can
// pretty-print without a separate result table for transient lookups.
Result datatypes.JSON `gorm:"type:jsonb" json:"result"`
SucceededAt *time.Time `json:"succeeded_at,omitempty"`
concerns.Timestamped
}
// TaskReplay records the single operator-initiated replay of a dead task.
// OriginalTaskID is unique, making repeated clicks/API retries idempotent.
type TaskReplay struct {
concerns.Model
OriginalTaskID int64 `gorm:"uniqueIndex" json:"original_task_id"`
RequeuedTaskID int64 `gorm:"uniqueIndex" json:"requeued_task_id"`
OperatorUserID int64 `gorm:"index" json:"operator_user_id"`
concerns.Timestamped
}
// TableName overrides the default table name so pluralization stays consistent
// with the rest of the schema (tasks, not "task" or "taskses").
func (Task) TableName() string {
return "tasks"
}
// NotificationDelivery is the per-attempt audit row required by section 7.5 of the
// plan ("Audit rows: each successful or failed delivery writes a row in a new
// notification_deliveries table"). The result handler appends one row per result
// frame, which lets support answer "did the customer ever get this alert" without
// scanning application logs.
type NotificationDelivery struct {
concerns.Model
MessageID int64 `gorm:"index" json:"message_id"`
WorkerID string `gorm:"size:128;index" json:"worker_id"`
TaskID int64 `gorm:"index" json:"task_id"`
Status string `gorm:"size:32" json:"status"`
Error string `gorm:"type:text" json:"error"`
DurationMs int `json:"duration_ms"`
ProviderResponse string `gorm:"type:text" json:"provider_response"`
concerns.Timestamped
}
// TableName mirrors the plan's preferred lowercase plural.
func (NotificationDelivery) TableName() string {
return "notification_deliveries"
}