detached 196ef1e

fix(concurrency): shadow loop variable in event handler goroutine

Create local copy of loop variable before passing to goroutine to prevent handlers from capturing wrong event due to loop variable mutation.
commit
196ef1eb281e20edb0feb5fa9a3cf4c1e40542be
author
brice · · 2026-02-06 05:56 UTC
parent
e62cfa8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +5 -4
M internal/adapters/out/eventbus/inmemory.go +5 -4
@@ -186,11 +186,12 @@ func (bus *InMemory) handleEvent(event domain.Event) {
186186
187187 for _, handler := range handlers {
188188 if handler.CanHandle(event.Type) {
189+ h := handler // shadow loop variable for goroutine capture
189190 start := time.Now()
190191
191192 done := make(chan error, 1)
192193 go func() {
193- done <- handler.Handle(event)
194+ done <- h.Handle(event)
194195 }()
195196
196197 select {
@@ -202,7 +203,7 @@ func (bus *InMemory) handleEvent(event domain.Event) {
202203 Err(err).
203204 Str("event_id", event.ID).
204205 Str(zerowrap.FieldEvent, string(event.Type)).
205- Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)).
206+ Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)).
206207 Msg("error handling event")
207208 } else {
208209 bus.log.Debug().
@@ -210,7 +211,7 @@ func (bus *InMemory) handleEvent(event domain.Event) {
210211 Str(zerowrap.FieldAdapter, "eventbus").
211212 Str("event_id", event.ID).
212213 Str(zerowrap.FieldEvent, string(event.Type)).
213- Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)).
214+ Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)).
214215 Dur(zerowrap.FieldDuration, time.Since(start)).
215216 Msg("event handled successfully")
216217 }
@@ -220,7 +221,7 @@ func (bus *InMemory) handleEvent(event domain.Event) {
220221 Str(zerowrap.FieldAdapter, "eventbus").
221222 Str("event_id", event.ID).
222223 Str(zerowrap.FieldEvent, string(event.Type)).
223- Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)).
224+ Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)).
224225 Dur(zerowrap.FieldDuration, time.Since(start)).
225226 Msg("handler timeout after 30s")
226227 }
@@ -186,11 +186,12 @@ func (bus *InMemory) handleEvent(event domain.Event) {
186 186
187 for _, handler := range handlers {187 for _, handler := range handlers {
188 if handler.CanHandle(event.Type) {188 if handler.CanHandle(event.Type) {
189+ h := handler // shadow loop variable for goroutine capture
189 start := time.Now()190 start := time.Now()
190 191
191 done := make(chan error, 1)192 done := make(chan error, 1)
192 go func() {193 go func() {
193- done <- handler.Handle(event)194+ done <- h.Handle(event)
194 }()195 }()
195 196
196 select {197 select {
@@ -202,7 +203,7 @@ func (bus *InMemory) handleEvent(event domain.Event) {
202 Err(err).203 Err(err).
203 Str("event_id", event.ID).204 Str("event_id", event.ID).
204 Str(zerowrap.FieldEvent, string(event.Type)).205 Str(zerowrap.FieldEvent, string(event.Type)).
205- Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)).206+ Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)).
206 Msg("error handling event")207 Msg("error handling event")
207 } else {208 } else {
208 bus.log.Debug().209 bus.log.Debug().
@@ -210,7 +211,7 @@ func (bus *InMemory) handleEvent(event domain.Event) {
210 Str(zerowrap.FieldAdapter, "eventbus").211 Str(zerowrap.FieldAdapter, "eventbus").
211 Str("event_id", event.ID).212 Str("event_id", event.ID).
212 Str(zerowrap.FieldEvent, string(event.Type)).213 Str(zerowrap.FieldEvent, string(event.Type)).
213- Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)).214+ Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)).
214 Dur(zerowrap.FieldDuration, time.Since(start)).215 Dur(zerowrap.FieldDuration, time.Since(start)).
215 Msg("event handled successfully")216 Msg("event handled successfully")
216 }217 }
@@ -220,7 +221,7 @@ func (bus *InMemory) handleEvent(event domain.Event) {
220 Str(zerowrap.FieldAdapter, "eventbus").221 Str(zerowrap.FieldAdapter, "eventbus").
221 Str("event_id", event.ID).222 Str("event_id", event.ID).
222 Str(zerowrap.FieldEvent, string(event.Type)).223 Str(zerowrap.FieldEvent, string(event.Type)).
223- Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)).224+ Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)).
224 Dur(zerowrap.FieldDuration, time.Since(start)).225 Dur(zerowrap.FieldDuration, time.Since(start)).
225 Msg("handler timeout after 30s")226 Msg("handler timeout after 30s")
226 }227 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close