Refactor delivery targets to a Target interface (closes #77)
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:
2026-08-07 21:43:21 +07:00
parent 752d6beead
commit 7b1f997194
9 changed files with 1313 additions and 856 deletions

View File

@@ -39,37 +39,50 @@ func ExportTruncate(s string, maxLen int) string {
return truncate(s, maxLen)
}
// ExportDeliverHTTP exposes deliverHTTP for testing.
// ExportDeliverHTTP delivers via the http target for testing.
func (e *Engine) ExportDeliverHTTP(
ctx context.Context,
webhookDB *gorm.DB,
d *database.Delivery,
task *Task,
) {
e.deliverHTTP(ctx, webhookDB, d, task)
e.httpTarget.Deliver(ctx, webhookDB, d, task, e)
}
// ExportDeliverDatabase exposes deliverDatabase.
// ExportDeliverDatabase delivers via the database target.
func (e *Engine) ExportDeliverDatabase(
webhookDB *gorm.DB, d *database.Delivery,
) {
e.deliverDatabase(webhookDB, d)
e.targets[database.TargetTypeDatabase].Deliver(
context.Background(), webhookDB, d, &Task{}, e,
)
}
// ExportDeliverLog exposes deliverLog for testing.
// ExportDeliverLog delivers via the log target for testing.
func (e *Engine) ExportDeliverLog(
webhookDB *gorm.DB, d *database.Delivery,
) {
e.deliverLog(webhookDB, d)
e.targets[database.TargetTypeLog].Deliver(
context.Background(), webhookDB, d, &Task{}, e,
)
}
// ExportDeliverSlack exposes deliverSlack for testing.
// ExportDeliverSlack delivers via the slack target for
// testing.
func (e *Engine) ExportDeliverSlack(
ctx context.Context,
webhookDB *gorm.DB,
d *database.Delivery,
) {
e.deliverSlack(ctx, webhookDB, d)
task := &Task{
DeliveryID: d.ID,
TargetID: d.TargetID,
AttemptNum: 1,
}
e.targets[database.TargetTypeSlack].Deliver(
ctx, webhookDB, d, task, e,
)
}
// ExportProcessNewTask exposes processNewTask.
@@ -96,53 +109,56 @@ func (e *Engine) ExportProcessDelivery(
e.processDelivery(ctx, webhookDB, d, task)
}
// ExportGetCircuitBreaker exposes getCircuitBreaker.
// ExportGetCircuitBreaker exposes the http target's
// getCircuitBreaker.
func (e *Engine) ExportGetCircuitBreaker(
targetID string,
) *CircuitBreaker {
return e.getCircuitBreaker(targetID)
return e.httpTarget.getCircuitBreaker(targetID)
}
// ExportParseHTTPConfig exposes parseHTTPConfig.
func (e *Engine) ExportParseHTTPConfig(
configJSON string,
) (*HTTPTargetConfig, error) {
return e.parseHTTPConfig(configJSON)
return parseHTTPConfig(configJSON)
}
// ExportParseSlackConfig exposes parseSlackConfig.
func (e *Engine) ExportParseSlackConfig(
configJSON string,
) (*SlackTargetConfig, error) {
return e.parseSlackConfig(configJSON)
return parseSlackConfig(configJSON)
}
// ExportDoHTTPRequest exposes doHTTPRequest.
// ExportDoHTTPRequest exposes the http target's
// doHTTPRequest.
func (e *Engine) ExportDoHTTPRequest(
ctx context.Context,
cfg *HTTPTargetConfig,
event *database.Event,
) (int, string, int64, error) {
return e.doHTTPRequest(ctx, cfg, event)
return e.httpTarget.doHTTPRequest(ctx, cfg, event)
}
// ExportClientForConfig exposes clientForConfig.
// ExportClientForConfig exposes the http target's
// clientForConfig.
func (e *Engine) ExportClientForConfig(
cfg *HTTPTargetConfig,
) *http.Client {
return e.clientForConfig(cfg)
return e.httpTarget.clientForConfig(cfg)
}
// ExportClient returns the engine's shared HTTP client.
// ExportClient returns the http target's shared HTTP client.
func (e *Engine) ExportClient() *http.Client {
return e.client
return e.httpTarget.client
}
// ExportScheduleRetry exposes scheduleRetry.
// ExportScheduleRetry exposes ScheduleRetry.
func (e *Engine) ExportScheduleRetry(
task Task, delay time.Duration,
) {
e.scheduleRetry(task, delay)
e.ScheduleRetry(task, delay)
}
// ExportRecoverPendingDeliveries exposes
@@ -199,13 +215,15 @@ func NewTestEngine(
client *http.Client,
workers int,
) *Engine {
return &Engine{
e := &Engine{
log: log,
client: client,
deliveryCh: make(chan Task, deliveryChannelSize),
retryCh: make(chan Task, retryChannelSize),
workers: workers,
}
e.initTargets(client)
return e
}
// NewTestEngineSmallRetry creates an Engine with a tiny
@@ -213,10 +231,13 @@ func NewTestEngine(
func NewTestEngineSmallRetry(
log *slog.Logger,
) *Engine {
return &Engine{
e := &Engine{
log: log,
retryCh: make(chan Task, 1),
}
e.initTargets(nil)
return e
}
// NewTestEngineWithDB creates an Engine with a real
@@ -228,15 +249,17 @@ func NewTestEngineWithDB(
client *http.Client,
workers int,
) *Engine {
return &Engine{
e := &Engine{
database: db,
dbManager: dbMgr,
log: log,
client: client,
deliveryCh: make(chan Task, deliveryChannelSize),
retryCh: make(chan Task, retryChannelSize),
workers: workers,
}
e.initTargets(client)
return e
}
// NewTestCircuitBreaker creates a CircuitBreaker with