128 lines
4.5 KiB
Go
128 lines
4.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/worker"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
TaskTypeSlaTriggerAccounts = "sla:trigger_accounts"
|
|
TaskTypeSlaProcessAccount = "sla:process_account"
|
|
TaskTypeSlaProcessApplied = "sla:process_applied"
|
|
)
|
|
|
|
const slaScanInterval = 5 * time.Minute
|
|
|
|
type slaProcessAccountJob struct {
|
|
AccountID uint `json:"account_id"`
|
|
}
|
|
|
|
type slaProcessAppliedJob struct {
|
|
AppliedSlaID uint `json:"applied_sla_id"`
|
|
}
|
|
|
|
var slaProcessingRegistrations sync.Map
|
|
|
|
// RegisterSlaProcessingJobs wires Chatwoot enterprise SLA scheduled jobs into
|
|
// the durable worker chain:
|
|
// TriggerSlasForAccountsJob -> ProcessAccountAppliedSlasJob -> ProcessAppliedSlaJob.
|
|
func RegisterSlaProcessingJobs(wp *worker.WorkerPool, db *gorm.DB, appliedSvc *AppliedSlaService) {
|
|
registerSlaProcessingJobsWithNow(wp, db, appliedSvc, time.Now)
|
|
}
|
|
|
|
func registerSlaProcessingJobsWithNow(wp *worker.WorkerPool, db *gorm.DB, appliedSvc *AppliedSlaService, now func() time.Time) {
|
|
if wp == nil || db == nil || appliedSvc == nil {
|
|
return
|
|
}
|
|
if _, loaded := slaProcessingRegistrations.LoadOrStore(wp, struct{}{}); loaded {
|
|
return
|
|
}
|
|
runner := &slaProcessingJobRunner{wp: wp, db: db, appliedSvc: appliedSvc, now: now}
|
|
wp.Register(TaskTypeSlaTriggerAccounts, runner.performTriggerAccounts)
|
|
wp.Register(TaskTypeSlaProcessAccount, runner.performProcessAccount)
|
|
wp.Register(TaskTypeSlaProcessApplied, runner.performProcessApplied)
|
|
}
|
|
|
|
// EnqueueSlaAccountsScan queues the root SLA scan job. Bootstrap uses this once
|
|
// at startup; the root job schedules the next scan after each successful run.
|
|
func EnqueueSlaAccountsScan(ctx context.Context, wp *worker.WorkerPool, scheduledAt time.Time) (*model.BackgroundJob, error) {
|
|
if wp == nil {
|
|
return nil, nil
|
|
}
|
|
return wp.Enqueue(ctx, TaskTypeSlaTriggerAccounts, nil,
|
|
worker.WithQueue("scheduled_jobs"),
|
|
worker.WithScheduledAt(scheduledAt),
|
|
worker.WithMaxAttempts(3),
|
|
worker.WithIdempotencyKey(slaTriggerIdempotencyKey(scheduledAt)),
|
|
)
|
|
}
|
|
|
|
func slaTriggerIdempotencyKey(scheduledAt time.Time) string {
|
|
bucket := scheduledAt.UTC().Truncate(slaScanInterval).Unix()
|
|
return fmt.Sprintf("sla:trigger_accounts:%d", bucket)
|
|
}
|
|
|
|
type slaProcessingJobRunner struct {
|
|
wp *worker.WorkerPool
|
|
db *gorm.DB
|
|
appliedSvc *AppliedSlaService
|
|
now func() time.Time
|
|
}
|
|
|
|
func (r *slaProcessingJobRunner) performTriggerAccounts(ctx context.Context, job *model.BackgroundJob) error {
|
|
var accountIDs []uint
|
|
if err := r.db.WithContext(ctx).Model(&model.SlaPolicy{}).Distinct("account_id").Pluck("account_id", &accountIDs).Error; err != nil {
|
|
return fmt.Errorf("find accounts with sla policies: %w", err)
|
|
}
|
|
for _, accountID := range accountIDs {
|
|
if _, err := r.wp.Enqueue(ctx, TaskTypeSlaProcessAccount, slaProcessAccountJob{AccountID: accountID}, worker.WithQueue("medium"), worker.WithMaxAttempts(3)); err != nil {
|
|
return fmt.Errorf("enqueue sla account %d: %w", accountID, err)
|
|
}
|
|
}
|
|
_, err := EnqueueSlaAccountsScan(ctx, r.wp, r.now().Add(slaScanInterval))
|
|
return err
|
|
}
|
|
|
|
func (r *slaProcessingJobRunner) performProcessAccount(ctx context.Context, job *model.BackgroundJob) error {
|
|
var payload slaProcessAccountJob
|
|
if err := json.Unmarshal(job.Payload, &payload); err != nil {
|
|
return fmt.Errorf("unmarshal sla account job: %w", err)
|
|
}
|
|
if payload.AccountID == 0 {
|
|
return fmt.Errorf("invalid sla account job payload: %#v", payload)
|
|
}
|
|
|
|
var appliedIDs []uint
|
|
statuses := []model.SLAStatus{model.SLAStatusActive, model.SLAStatusActiveWithMisses}
|
|
if err := r.db.WithContext(ctx).Model(&model.AppliedSLA{}).
|
|
Where("account_id = ? AND sla_status IN ?", payload.AccountID, statuses).
|
|
Pluck("id", &appliedIDs).Error; err != nil {
|
|
return fmt.Errorf("find active applied slas for account %d: %w", payload.AccountID, err)
|
|
}
|
|
for _, appliedID := range appliedIDs {
|
|
if _, err := r.wp.Enqueue(ctx, TaskTypeSlaProcessApplied, slaProcessAppliedJob{AppliedSlaID: appliedID}, worker.WithQueue("medium"), worker.WithMaxAttempts(3)); err != nil {
|
|
return fmt.Errorf("enqueue applied sla %d: %w", appliedID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *slaProcessingJobRunner) performProcessApplied(ctx context.Context, job *model.BackgroundJob) error {
|
|
var payload slaProcessAppliedJob
|
|
if err := json.Unmarshal(job.Payload, &payload); err != nil {
|
|
return fmt.Errorf("unmarshal applied sla job: %w", err)
|
|
}
|
|
if payload.AppliedSlaID == 0 {
|
|
return fmt.Errorf("invalid applied sla job payload: %#v", payload)
|
|
}
|
|
_, err := r.appliedSvc.Evaluate(ctx, payload.AppliedSlaID)
|
|
return err
|
|
}
|