Refactor delivery targets to a Target interface (closes #77)
All checks were successful
check / check (push) Successful in 5s
All checks were successful
check / check (push) Successful in 5s
Each target TYPE is now an implementation of a Target interface, dispatched from a registry in processDelivery instead of a type switch on TargetType. Every target owns its full delivery, including durable retries. - Target.Deliver receives the context, the per-webhook DB, the Delivery, the attempt Task, and a Scheduler for durable re-enqueue (the existing timer + retry queue). The target makes one attempt, records the DeliveryResult, updates DeliveryStatus, and — for retry targets — decides whether to retry, computes its own backoff, gates with its own circuit breaker, and reschedules via the Scheduler. - httpTarget and slackTarget share a retry core (retry, backoff, circuit breaker). database and log targets are fire-and-forget. - Slack retry/breaker is gated on MaxRetries: 0 stays fire-and-forget (existing Slack targets unchanged), >0 gets retry + backoff + breaker on the shared core. - The engine keeps only the worker pool, queue/channels, restart recovery/sweep, the recordResult/updateDeliveryStatus helpers, and ScheduleRetry. Recovery/sweep hand each orphaned retrying delivery back to its target to recompute the backoff. - The log target logs the entire inbound webhook: full body and headers, method, content type, and the webhook and entrypoint ids (supersedes the smaller log-summary work). - Task gains EntrypointID, populated in the webhook handler, the recovery-task builder, and buildEventFromTask. Behaviour is preserved: existing delivery tests pass with their export_test wrappers re-pointed at the new targets; new pure Deliver tests cover the log full-content output and the gated Slack retry path.
This commit is contained in:
101
internal/delivery/target.go
Normal file
101
internal/delivery/target.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user