detached 58ac19e

refactor(eventbus): unsubscribe with context.AfterFunc and key subscribers by channel

Drops the per-subscriber goroutine and the subscriber struct. Subscribers()
moves to export_test.go.
commit
58ac19e70fed8c963b404bd373f18b0f335836c1
author
brice · · 2026-10-10 08:11 UTC
parent
a771dd2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +24 -26
M internal/adapters/eventbus/bus.go +14 -26
@@ -22,11 +22,7 @@ type Bus struct {
2222 buffer int
2323
2424 mu sync.Mutex
25- subs map[*subscriber]struct{}
26-}
27-
28-type subscriber struct {
29- ch chan domain.Event
25+ subs map[chan domain.Event]struct{}
3026 }
3127
3228 // New builds a bus whose subscribers buffer DefaultBuffer events.
@@ -35,7 +31,7 @@ func New() *Bus { return NewWithBuffer(DefaultBuffer) }
3531 // NewWithBuffer builds a bus whose subscribers buffer n events (at least 2,
3632 // so that a gap marker and the event that follows it fit).
3733 func NewWithBuffer(n int) *Bus {
38- return &Bus{buffer: max(n, 2), subs: make(map[*subscriber]struct{})}
34+ return &Bus{buffer: max(n, 2), subs: make(map[chan domain.Event]struct{})}
3935 }
4036
4137 // Publish delivers ev to every subscriber. A subscriber whose buffer is full
@@ -44,9 +40,9 @@ func NewWithBuffer(n int) *Bus {
4440 func (b *Bus) Publish(_ context.Context, ev domain.Event) {
4541 b.mu.Lock()
4642 defer b.mu.Unlock()
47- for s := range b.subs {
43+ for ch := range b.subs {
4844 select {
49- case s.ch <- ev:
45+ case ch <- ev:
5046 continue
5147 default:
5248 }
@@ -55,36 +51,28 @@ func (b *Bus) Publish(_ context.Context, ev domain.Event) {
5551 drain:
5652 for {
5753 select {
58- case <-s.ch:
54+ case <-ch:
5955 default:
6056 break drain
6157 }
6258 }
63- s.ch <- domain.EventGap{}
64- s.ch <- ev
59+ ch <- domain.EventGap{}
60+ ch <- ev
6561 }
6662 }
6763
6864 // Subscribe registers a subscriber until ctx ends, then closes its channel.
6965 func (b *Bus) Subscribe(ctx context.Context) <-chan domain.Event {
70- s := &subscriber{ch: make(chan domain.Event, b.buffer)}
66+ ch := make(chan domain.Event, b.buffer)
7167 b.mu.Lock()
72- b.subs[s] = struct{}{}
68+ b.subs[ch] = struct{}{}
7369 b.mu.Unlock()
7470
75- go func() {
76- <-ctx.Done()
71+ context.AfterFunc(ctx, func() {
7772 b.mu.Lock()
78- delete(b.subs, s)
79- close(s.ch)
73+ delete(b.subs, ch)
74+ close(ch)
8075 b.mu.Unlock()
81- }()
82- return s.ch
83-}
84-
85-// Subscribers returns the number of live subscribers, for tests.
86-func (b *Bus) Subscribers() int {
87- b.mu.Lock()
88- defer b.mu.Unlock()
89- return len(b.subs)
76+ })
77+ return ch
9078 }
@@ -22,11 +22,7 @@ type Bus struct {
22 buffer int22 buffer int
23 23
24 mu sync.Mutex24 mu sync.Mutex
25- subs map[*subscriber]struct{}25+ subs map[chan domain.Event]struct{}
26-}
27-
28-type subscriber struct {
29- ch chan domain.Event
30 }26 }
31 27
32 // New builds a bus whose subscribers buffer DefaultBuffer events.28 // New builds a bus whose subscribers buffer DefaultBuffer events.
@@ -35,7 +31,7 @@ func New() *Bus { return NewWithBuffer(DefaultBuffer) }
35 // NewWithBuffer builds a bus whose subscribers buffer n events (at least 2,31 // NewWithBuffer builds a bus whose subscribers buffer n events (at least 2,
36 // so that a gap marker and the event that follows it fit).32 // so that a gap marker and the event that follows it fit).
37 func NewWithBuffer(n int) *Bus {33 func NewWithBuffer(n int) *Bus {
38- return &Bus{buffer: max(n, 2), subs: make(map[*subscriber]struct{})}34+ return &Bus{buffer: max(n, 2), subs: make(map[chan domain.Event]struct{})}
39 }35 }
40 36
41 // Publish delivers ev to every subscriber. A subscriber whose buffer is full37 // Publish delivers ev to every subscriber. A subscriber whose buffer is full
@@ -44,9 +40,9 @@ func NewWithBuffer(n int) *Bus {
44 func (b *Bus) Publish(_ context.Context, ev domain.Event) {40 func (b *Bus) Publish(_ context.Context, ev domain.Event) {
45 b.mu.Lock()41 b.mu.Lock()
46 defer b.mu.Unlock()42 defer b.mu.Unlock()
47- for s := range b.subs {43+ for ch := range b.subs {
48 select {44 select {
49- case s.ch <- ev:45+ case ch <- ev:
50 continue46 continue
51 default:47 default:
52 }48 }
@@ -55,36 +51,28 @@ func (b *Bus) Publish(_ context.Context, ev domain.Event) {
55 drain:51 drain:
56 for {52 for {
57 select {53 select {
58- case <-s.ch:54+ case <-ch:
59 default:55 default:
60 break drain56 break drain
61 }57 }
62 }58 }
63- s.ch <- domain.EventGap{}59+ ch <- domain.EventGap{}
64- s.ch <- ev60+ ch <- ev
65 }61 }
66 }62 }
67 63
68 // Subscribe registers a subscriber until ctx ends, then closes its channel.64 // Subscribe registers a subscriber until ctx ends, then closes its channel.
69 func (b *Bus) Subscribe(ctx context.Context) <-chan domain.Event {65 func (b *Bus) Subscribe(ctx context.Context) <-chan domain.Event {
70- s := &subscriber{ch: make(chan domain.Event, b.buffer)}66+ ch := make(chan domain.Event, b.buffer)
71 b.mu.Lock()67 b.mu.Lock()
72- b.subs[s] = struct{}{}68+ b.subs[ch] = struct{}{}
73 b.mu.Unlock()69 b.mu.Unlock()
74 70
75- go func() {71+ context.AfterFunc(ctx, func() {
76- <-ctx.Done()
77 b.mu.Lock()72 b.mu.Lock()
78- delete(b.subs, s)73+ delete(b.subs, ch)
79- close(s.ch)74+ close(ch)
80 b.mu.Unlock()75 b.mu.Unlock()
81- }()76+ })
82- return s.ch77+ return ch
83-}
84-
85-// Subscribers returns the number of live subscribers, for tests.
86-func (b *Bus) Subscribers() int {
87- b.mu.Lock()
88- defer b.mu.Unlock()
89- return len(b.subs)
90 }78 }
A internal/adapters/eventbus/export_test.go +10 -0
@@ -0,0 +1,10 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package eventbus
4+
5+// Subscribers returns the number of live subscribers.
6+func (b *Bus) Subscribers() int {
7+ b.mu.Lock()
8+ defer b.mu.Unlock()
9+ return len(b.subs)
10+}
@@ -0,0 +1,10 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package eventbus
4+
5+// Subscribers returns the number of live subscribers.
6+func (b *Bus) Subscribers() int {
7+ b.mu.Lock()
8+ defer b.mu.Unlock()
9+ return len(b.subs)
10+}

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 2

Tags 45

↑↓ moveenter openesc close