// Package delivery manages asynchronous event delivery // to configured targets. package delivery import ( "context" "errors" "fmt" "log/slog" "net/http" "sync" "time" "go.uber.org/fx" "gorm.io/gorm" "sneak.berlin/go/webhooker/internal/database" "sneak.berlin/go/webhooker/internal/lifecycle" "sneak.berlin/go/webhooker/internal/logger" "sneak.berlin/go/webhooker/internal/metrics" ) const ( // deliveryChannelSize is the buffer size for the delivery // channel. New Tasks from the webhook handler are sent // here. Workers drain this channel. Sized large enough // that the webhook handler should never block under // normal load. deliveryChannelSize = 10000 // retryChannelSize is the buffer size for the retry // channel. Timer-fired retries are sent here for // processing by workers. retryChannelSize = 10000 // defaultWorkers is the number of worker goroutines in // the delivery engine pool. At most this many deliveries // are in-flight at any time, preventing goroutine // explosions regardless of queue depth. defaultWorkers = 10 // retrySweepInterval is how often the periodic retry // sweep runs. retrySweepInterval = 60 * time.Second // pendingSweepMinAge is how long a delivery must have sat // untouched at pending before the sweep will look at it. // // It is not what keeps the sweep off live work — inflightSet is, // and it is exact. This bound sets the re-dispatch cadence for a // delivery that really is stranded: without it, a delivery the // database will not let the engine settle would be re-sent on // every 60-second tick. // // It is nonetheless set clear of the longest legitimate attempt, // so that the two guards do not both have to be right. That // length is MaxTargetTimeoutSeconds (300s), the per-target // timeout the target form accepts — not httpClientTimeout, which // is merely the default. Fifteen minutes leaves a margin of // three times the ceiling rather than the zero margin the two // equal values would have given. pendingSweepMinAge = 15 * time.Minute // pendingSweepBatch bounds how many stranded pending deliveries // one sweep of one webhook re-dispatches. The sweep runs every // retrySweepInterval, so a larger backlog drains across // successive sweeps instead of arriving as one burst against a // database that was already struggling to accept writes. pendingSweepBatch = 500 // MaxInlineBodySize is the maximum event body size that // will be carried inline in a Task through the channel. // Bodies at or above this size are left nil and fetched // from the per-webhook database on demand. MaxInlineBodySize = 16 * 1024 // httpClientTimeout is the timeout for outbound HTTP // requests. httpClientTimeout = 30 * time.Second // maxBodyLog is the maximum response body length to // store in DeliveryResult. maxBodyLog = 4096 // maxBackoffShift caps the exponential backoff shift to // avoid integer overflow in the 1<