feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
145
app/models/task.go
Обычный файл
145
app/models/task.go
Обычный файл
@@ -0,0 +1,145 @@
|
||||
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"
|
||||
}
|
||||
Ссылка в новой задаче
Block a user