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" }