package models import ( "fmt" "time" "gorm.io/gorm" ) const maintenanceStartNotificationPrefix = "maintenance:%d:start:" // MaintenanceStartNotificationKey uniquely identifies one contact's warning // for one schedule revision and occurrence. Updated windows get a new revision // while concurrent scheduler replicas share the same key. func MaintenanceStartNotificationKey(maintenanceID int64, revision, startsAt time.Time, timezone string, notificationID, contactID int64) string { loc, err := time.LoadLocation(timezone) if err != nil || timezone == "SAME_AS_SERVER" || timezone == "" { loc = time.UTC } // A fall-back hour can have two UTC instants for one wall-clock occurrence. // Warnings are once per civil occurrence, matching the recurrence editor. civilOccurrence := startsAt.In(loc).Format("200601021504") return fmt.Sprintf("maintenance:%d:start:%d:%s:notification:%d:contact:%d", maintenanceID, revision.UnixNano(), civilOccurrence, notificationID, contactID) } // CancelMaintenanceStartNotificationsTx prevents queued warnings from being // delivered after an operator pauses, changes, or deletes the maintenance. // Leased work may already be executing and cannot be recalled from a worker. func CancelMaintenanceStartNotificationsTx(tx *gorm.DB, maintenanceID int64, reason string) error { prefix := fmt.Sprintf(maintenanceStartNotificationPrefix, maintenanceID) + "%" var tasks []Task if err := tx.Clauses(SkipLockedClause).Where("idempotency_key LIKE ? AND state IN ?", prefix, []string{TaskStateQueued, TaskStateFailedRetry}).Find(&tasks).Error; err != nil { return err } for i := range tasks { if err := tx.Model(&Task{}).Where("id = ? AND state IN ?", tasks[i].ID, []string{TaskStateQueued, TaskStateFailedRetry}).Updates(map[string]any{"state": TaskStateDead, "last_error": "canceled: " + reason, "payload": []byte(`{}`), "lease_owner": "", "lease_token": "", "lease_expires_at": nil}).Error; err != nil { return err } if err := FinalizeNotificationTaskTx(tx, &tasks[i], "canceled", "canceled: "+reason); err != nil { return err } } return nil }