Files
member-console/internal/workflows/domains/schedule.go
T
cgalo5758 c4bb1ba585 Harden domain claim expiry and carving
Sweep stranded pending claims at boot and on a Temporal schedule while
preserving evidence-based abandonment semantics.

Apply occupancy and name-policy checks to carves by operator-root owners
without affecting direct operator placements.
2026-07-25 04:10:34 -05:00

95 lines
3.5 KiB
Go

package domains
import (
"context"
"fmt"
"log/slog"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/queues"
"go.temporal.io/sdk/client"
)
const (
// ExpirySweepScheduleID is the unique identifier for the claim expiry
// sweep schedule.
ExpirySweepScheduleID = "domains-claim-expiry-sweep"
// ExpirySweepWorkflowID is the workflow ID used for sweep executions.
ExpirySweepWorkflowID = "domains-claim-expiry-sweep-workflow"
// DefaultExpirySweepInterval is the default interval between sweeps.
// Shorter than the billing sweep's hour: an unswept claim holds a name
// another workspace may be waiting on, and the candidate read is a single
// range scan of idx_claims_pending_expires_at.
DefaultExpirySweepInterval = 15 * time.Minute
)
// ScheduleManager manages Temporal schedules for domain-registry maintenance.
type ScheduleManager struct {
client client.Client
logger *slog.Logger
}
// NewScheduleManager creates a new ScheduleManager.
func NewScheduleManager(c client.Client, logger *slog.Logger) *ScheduleManager {
return &ScheduleManager{client: c, logger: logger}
}
// ExpirySweepScheduleConfig holds configuration for the expiry sweep schedule.
type ExpirySweepScheduleConfig struct {
// Interval is how often to run the sweep.
Interval time.Duration
// TriggerImmediately runs the sweep immediately when creating the
// schedule. The console already sweeps once per boot, so this defaults
// off — the boot pass is the immediate run.
TriggerImmediately bool
}
// EnsureExpirySweepSchedule creates or updates the claim expiry sweep
// schedule. If the schedule already exists, it updates the spec; otherwise it
// creates it.
func (m *ScheduleManager) EnsureExpirySweepSchedule(ctx context.Context, cfg ExpirySweepScheduleConfig) error {
if cfg.Interval == 0 {
cfg.Interval = DefaultExpirySweepInterval
}
scheduleClient := m.client.ScheduleClient()
handle := scheduleClient.GetHandle(ctx, ExpirySweepScheduleID)
if _, err := handle.Describe(ctx); err == nil {
m.logger.Info("updating existing domain claim expiry sweep schedule", slog.Duration("interval", cfg.Interval))
if err := handle.Update(ctx, client.ScheduleUpdateOptions{
DoUpdate: func(schedule client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) {
schedule.Description.Schedule.Spec = &client.ScheduleSpec{
Intervals: []client.ScheduleIntervalSpec{{Every: cfg.Interval}},
}
schedule.Description.Schedule.Action = &client.ScheduleWorkflowAction{
ID: ExpirySweepWorkflowID,
Workflow: SweepExpiredClaimsWorkflow,
TaskQueue: queues.Main,
}
return &client.ScheduleUpdate{Schedule: &schedule.Description.Schedule}, nil
},
}); err != nil {
return fmt.Errorf("failed to update claim expiry sweep schedule: %w", err)
}
return nil
}
m.logger.Info("creating domain claim expiry sweep schedule",
slog.Duration("interval", cfg.Interval), slog.Bool("triggerImmediately", cfg.TriggerImmediately))
if _, err := scheduleClient.Create(ctx, client.ScheduleOptions{
ID: ExpirySweepScheduleID,
Spec: client.ScheduleSpec{Intervals: []client.ScheduleIntervalSpec{{Every: cfg.Interval}}},
Action: &client.ScheduleWorkflowAction{
ID: ExpirySweepWorkflowID,
Workflow: SweepExpiredClaimsWorkflow,
TaskQueue: queues.Main,
},
TriggerImmediately: cfg.TriggerImmediately,
}); err != nil {
return fmt.Errorf("failed to create claim expiry sweep schedule: %w", err)
}
m.logger.Info("domain claim expiry sweep schedule created", slog.String("scheduleID", ExpirySweepScheduleID))
return nil
}