Tick processes one batch of pending events. Exposed (not just the private processBatch) so integration tests can drive the worker synchronously instead of waiting on the timer.
(ctx context.Context)
| 183 | // private processBatch) so integration tests can drive the worker |
| 184 | // synchronously instead of waiting on the timer. |
| 185 | func (w *OutboxWorker) Tick(ctx context.Context) { |
| 186 | tickStart := time.Now() |
| 187 | notifyWake := w.notifySawLastTick |
| 188 | w.notifySawLastTick = false |
| 189 | |
| 190 | // Publisher-lag gauge: age of the oldest pending row. |
| 191 | var oldestAge float64 |
| 192 | if err := w.pool.QueryRow(ctx, |
| 193 | `SELECT EXTRACT(EPOCH FROM (now() - min(created_at))) |
| 194 | FROM webhook_events WHERE status = 'pending'`, |
| 195 | ).Scan(&oldestAge); err == nil { |
| 196 | w.metrics.SetPublisherLag(oldestAge) |
| 197 | } |
| 198 | |
| 199 | events, err := w.leasePending(ctx) |
| 200 | if err != nil { |
| 201 | log.Printf("[outbox-worker] leasePending err: %v", err) |
| 202 | w.metrics.OutboxFailures("lease") |
| 203 | return |
| 204 | } |
| 205 | if len(events) == 0 { |
| 206 | return |
| 207 | } |
| 208 | // If we picked up events without a NOTIFY wakeup, the fallback |
| 209 | // poll saved us. Non-zero rate signals LISTEN churn. |
| 210 | if !notifyWake { |
| 211 | w.metrics.NotifyMissed() |
| 212 | } |
| 213 | // Slice 10 telemetry hook: log batch size + age of oldest row so |
| 214 | // publisher lag can be derived from access logs. A future |
| 215 | // follow-up wires real Prometheus/OTLP counters. |
| 216 | var oldest time.Time |
| 217 | for _, ev := range events { |
| 218 | _ = ev |
| 219 | } |
| 220 | log.Printf("[outbox-worker-metrics] tick batch=%d oldest_age_estimate=lease-bound elapsed_ms_so_far=%d", |
| 221 | len(events), time.Since(tickStart).Milliseconds()) |
| 222 | _ = oldest |
| 223 | _ = oldestAge // already emitted via SetPublisherLag |
| 224 | |
| 225 | sem := make(chan struct{}, w.concurrency) |
| 226 | var wg sync.WaitGroup |
| 227 | for _, ev := range events { |
| 228 | ev := ev |
| 229 | wg.Add(1) |
| 230 | sem <- struct{}{} |
| 231 | go func() { |
| 232 | defer wg.Done() |
| 233 | defer func() { <-sem }() |
| 234 | w.fanOutOne(ctx, ev) |
| 235 | }() |
| 236 | } |
| 237 | wg.Wait() |
| 238 | } |
| 239 | |
| 240 | // leasedEvent is the worker's row-shape for an in-progress event. |
| 241 | type leasedEvent struct { |