package delivery import ( "context" "net/http" "time" "gorm.io/gorm" "sneak.berlin/go/webhooker/internal/database" ) // Scheduler re-enqueues a task for a future delivery attempt. // The engine provides one to each target so a target can own // its retries durably: it records the attempt, marks the // delivery retrying, and asks the Scheduler to deliver the // next attempt after delay — exactly what the engine does for // its own restart recovery. type Scheduler interface { ScheduleRetry(task Task, delay time.Duration) } // Target delivers an event to one target type. Each type is // an implementation. A Target owns its whole delivery: it // makes the attempt, records the DeliveryResult and updates // the DeliveryStatus, and — for targets that retry — decides // whether to retry, computes its own backoff, gates with its // own circuit breaker, and reschedules via the injected // Scheduler. Fire-and-forget targets simply record a single // attempt. type Target interface { Deliver( ctx context.Context, webhookDB *gorm.DB, d *database.Delivery, task *Task, sched Scheduler, ) } // rescheduler is implemented by targets that own durable // retries. The engine's restart recovery and periodic sweep // use it to let the target recompute the schedule for an // orphaned retrying delivery, keeping the retry schedule // target-owned. Fire-and-forget targets do not implement it // and their (never-occurring) retrying deliveries are // skipped. type rescheduler interface { // remainingBackoff returns how long to wait before the // next attempt of a recovered retrying delivery. remainingBackoff( webhookDB *gorm.DB, deliveryID string, attemptNum int, ) time.Duration // backoffElapsed reports whether the backoff window for // the last attempt has already passed, so the periodic // sweep can re-enqueue the delivery now. backoffElapsed( webhookDB *gorm.DB, deliveryID string, attemptNum int, ) bool } // attemptResult is the outcome of a single delivery attempt, // as reported by a target's per-attempt function to the // shared retry core. type attemptResult struct { statusCode int respBody string duration int64 success bool errMsg string } // initTargets builds the target registry, wiring each target // to the engine's persistence helpers and giving the HTTP and // Slack targets the shared SSRF-safe client. It is called by // both New and the test constructors so the registry is // always populated. func (e *Engine) initTargets(client *http.Client) { httpT := &httpTarget{ httpCore: &httpCore{eng: e}, client: client, } slackT := &slackTarget{ httpCore: &httpCore{eng: e}, client: client, } e.httpTarget = httpT e.targets = map[database.TargetType]Target{ database.TargetTypeHTTP: httpT, database.TargetTypeSlack: slackT, database.TargetTypeDatabase: &databaseTarget{eng: e}, database.TargetTypeLog: &logTarget{eng: e}, } }