| package eventhandler |
|
|
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "log/slog" |
| "runtime/debug" |
| "sync" |
| "sync/atomic" |
| "time" |
|
|
| "cirello.io/pglock" |
| "go.opentelemetry.io/otel/trace" |
|
|
| "github.com/openmeterio/openmeter/openmeter/notification" |
| "github.com/openmeterio/openmeter/openmeter/notification/webhook" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| type Config struct { |
| Repository notification.Repository |
| Webhook webhook.Handler |
| Logger *slog.Logger |
| Tracer trace.Tracer |
| ReconcileInterval time.Duration |
| SendingTimeout time.Duration |
| PendingTimeout time.Duration |
| ReconcilerWorkers int |
| LockClient *pglock.Client |
| } |
|
|
| func (c *Config) Validate() error { |
| var errs []error |
|
|
| if c.Repository == nil { |
| errs = append(errs, fmt.Errorf("repository is required")) |
| } |
|
|
| if c.Webhook == nil { |
| errs = append(errs, fmt.Errorf("webhook is required")) |
| } |
|
|
| if c.Logger == nil { |
| errs = append(errs, fmt.Errorf("logger is required")) |
| } |
|
|
| if c.Tracer == nil { |
| errs = append(errs, fmt.Errorf("tracer is required")) |
| } |
|
|
| if c.LockClient == nil { |
| errs = append(errs, fmt.Errorf("distributed lock client is required")) |
| } |
|
|
| return models.NewNillableGenericValidationError(errors.Join(errs...)) |
| } |
|
|
| var _ notification.EventHandler = (*Handler)(nil) |
|
|
| type Handler struct { |
| repo notification.Repository |
| webhook webhook.Handler |
|
|
| logger *slog.Logger |
| tracer trace.Tracer |
|
|
| reconcileInterval time.Duration |
|
|
| running atomic.Bool |
| stopCh chan struct{} |
| ctxCancel context.CancelFunc |
| stopChClose func() |
|
|
| lockClient *pglock.Client |
|
|
| |
| sendingTimeout time.Duration |
| pendingTimeout time.Duration |
|
|
| workerPoolSize int64 |
| } |
|
|
| const reconcilerLeaderLockKey = "notification.event_handler.reconcile_lock" |
|
|
| func (h *Handler) Start() error { |
| if !h.running.CompareAndSwap(false, true) { |
| return fmt.Errorf("notification event handler is already running") |
| } |
|
|
| defer func() { |
| if err := recover(); err != nil { |
| h.logger.Error("notification event handler panicked", |
| "error", err, |
| "code.stacktrace", string(debug.Stack())) |
| _ = h.Close() |
| } |
| }() |
|
|
| var ctx context.Context |
|
|
| ctx, h.ctxCancel = context.WithCancel(context.Background()) |
| defer h.ctxCancel() |
|
|
| for h.running.Load() { |
| err := h.lockClient.Do(ctx, reconcilerLeaderLockKey, func(rCtx context.Context, _ *pglock.Lock) error { |
| ticker := time.NewTicker(h.reconcileInterval) |
| defer ticker.Stop() |
|
|
| for { |
| select { |
| case <-rCtx.Done(): |
| return nil |
| case <-h.stopCh: |
| h.logger.DebugContext(rCtx, "close event received: stopping event reconciler") |
| return nil |
| case <-ticker.C: |
| if err := h.Reconcile(rCtx); err != nil { |
| h.logger.ErrorContext(rCtx, "failed to reconcile event(s)", "error", err) |
| } |
| } |
| } |
| }) |
| if err != nil { |
| if errors.Is(err, pglock.ErrNotAcquired) { |
| h.logger.DebugContext(ctx, "reconciliation skipped: lock is not acquired") |
| continue |
| } |
|
|
| return fmt.Errorf("failed to acquire reconciliation lock: %w", err) |
| } |
| } |
|
|
| return nil |
| } |
|
|
| func (h *Handler) Close() error { |
| if h.running.CompareAndSwap(true, false) { |
| h.logger.Debug("closing notification event handler") |
|
|
| h.ctxCancel() |
| h.stopChClose() |
| } |
|
|
| return nil |
| } |
|
|
| func New(config Config) (*Handler, error) { |
| if err := config.Validate(); err != nil { |
| return nil, err |
| } |
|
|
| if config.ReconcileInterval == 0 { |
| config.ReconcileInterval = notification.DefaultReconcileInterval |
| } |
|
|
| if config.PendingTimeout == 0 { |
| config.PendingTimeout = notification.DefaultDeliveryStatePendingTimeout |
| } |
|
|
| if config.SendingTimeout == 0 { |
| config.SendingTimeout = notification.DefaultDeliveryStateSendingTimeout |
| } |
|
|
| if config.ReconcilerWorkers <= 0 { |
| config.ReconcilerWorkers = notification.DefaultReconcilerWorkers |
| } |
|
|
| stopCh := make(chan struct{}) |
| stopChClose := sync.OnceFunc(func() { |
| close(stopCh) |
| }) |
|
|
| return &Handler{ |
| repo: config.Repository, |
| webhook: config.Webhook, |
| reconcileInterval: config.ReconcileInterval, |
| logger: config.Logger, |
| tracer: config.Tracer, |
| stopCh: stopCh, |
| stopChClose: stopChClose, |
| lockClient: config.LockClient, |
| sendingTimeout: config.SendingTimeout, |
| pendingTimeout: config.PendingTimeout, |
| workerPoolSize: int64(config.ReconcilerWorkers), |
| }, nil |
| } |
|
|