@@ -91,12 +91,7 @@ func (e *EmailTriggerPoller) Reload(ctx context.Context) error {
9191
9292 // Start pollers for newly added triggers.
9393 for _ , t := range triggers {
94- e .mu .Lock ()
95- _ , running := e .pollers [t .ID ]
96- e .mu .Unlock ()
97- if ! running {
98- e .startPoller (ctx , t )
99- }
94+ e .startPollerIfNotRunning (ctx , t )
10095 }
10196
10297 return nil
@@ -112,10 +107,30 @@ func (e *EmailTriggerPoller) Stop() {
112107
113108// startPoller starts a single goroutine for the given trigger, subject to the
114109// maxConnections limit. Excess triggers are logged and skipped.
110+ // startPollerIfNotRunning acquires the lock, checks if the trigger is already
111+ // running, and starts it if not. The check-and-start is atomic under the lock
112+ // to prevent duplicate pollers from concurrent Reload calls.
113+ func (e * EmailTriggerPoller ) startPollerIfNotRunning (ctx context.Context , t TriggerRecord ) {
114+ e .mu .Lock ()
115+ defer e .mu .Unlock ()
116+
117+ if _ , running := e .pollers [t .ID ]; running {
118+ return
119+ }
120+ e .startPollerLocked (ctx , t )
121+ }
122+
123+ // startPoller acquires the lock and starts a poller. Used during Start() where
124+ // no existence check is needed (fresh startup).
115125func (e * EmailTriggerPoller ) startPoller (ctx context.Context , t TriggerRecord ) {
116126 e .mu .Lock ()
117127 defer e .mu .Unlock ()
128+ e .startPollerLocked (ctx , t )
129+ }
118130
131+ // startPollerLocked starts a single goroutine for the given trigger. Must be
132+ // called with e.mu held. Excess triggers beyond maxConnections are skipped.
133+ func (e * EmailTriggerPoller ) startPollerLocked (ctx context.Context , t TriggerRecord ) {
119134 if len (e .pollers ) >= e .maxConnections {
120135 e .server .Logger .Warn ("email poller: maxConnections reached, skipping trigger" ,
121136 "trigger_id" , t .ID ,
0 commit comments