pico

created pr with 105.1 on 2026-01-25T17:03:08Z · by c8ef7d19
added 105.2 on 2026-01-27T04:11:48Z · by c8ef7d19
1: 603ca6e = 1: 603ca6e chore(pubsub): add more tests
2: 501c042 ! 2: 17e00b2 feat(pubsub): round robin
added 105.3 on 2026-01-29T01:03:33Z · by c8ef7d19
1: 603ca6e = 1: 603ca6e chore(pubsub): add more tests
2: 17e00b2 = 2: 17e00b2 feat(pubsub): round robin
-: ------- > 3: e3136bd fix(pubsub): check for eof before processing and skip empty byte reads
-: ------- > 4: 9a6d19e fix: rr
-: ------- > 5: 5b3f3a1 fix: sending 0 byte read
added 105.4 on 2026-02-01T17:16:43Z · by c8ef7d19
1: 603ca6e = 1: 603ca6e chore(pubsub): add more tests
2: 17e00b2 = 2: 17e00b2 feat(pubsub): round robin
3: e3136bd = 3: e3136bd fix(pubsub): check for eof before processing and skip empty byte reads
4: 9a6d19e = 4: 9a6d19e fix: rr
5: 5b3f3a1 = 5: 5b3f3a1 fix: sending 0 byte read
-: ------- > 6: 4fff471 refactor: fixes
-: ------- > 7: d0dfc85 chore: SetDispatch on Broker
changed status to accepted on 2026-02-23T02:02:07Z · by c8ef7d19
cmds
checkout latest patchset:
ssh pr.pico.sh print 105 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 105.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 105
set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 105
set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 105
+1 -1 pkg/apps/pipe/cli.go #
......@@ -803,7 +803,7 @@ func (handler *CliHandler) pub(cmd *CliCmd, topic string, clientID string) error
803803 bk = &psub.RoundRobinDispatcher{}
804804 }
805805 channel := psub.NewChannel(name)
806- channel.Dispatcher = bk
806+ _ = handler.PubSub.SetDispatcher(bk, []*psub.Channel{channel})
807807
808808 err := handler.PubSub.Pub(
809809 cmd.pipeCtx,
+12 -5 pkg/pubsub/broker.go #
......@@ -5,6 +5,7 @@ import (
55 "io"
66 "iter"
77 "log/slog"
8+ "reflect"
89 "sync"
910 "time"
1011
......@@ -21,6 +22,7 @@ type Broker interface {
2122 GetChannels() iter.Seq2[string, *Channel]
2223 GetClients() iter.Seq2[string, *Client]
2324 Connect(*Client, []*Channel) (error, error)
25+ SetDispatcher(dispatcher MessageDispatcher, channels []*Channel) error
2426 }
2527
2628 type BaseBroker struct {
......@@ -197,13 +199,18 @@ func (b *BaseBroker) Connect(client *Client, channels []*Channel) (error, error)
197199
198200 func (b *BaseBroker) ensureChannel(channel *Channel) *Channel {
199201 dataChannel, _ := b.Channels.LoadOrStore(channel.Topic, channel)
200- // Allow overwriting the dispatcher
201- if channel.Dispatcher != nil && dataChannel.Dispatcher == nil {
202- dataChannel.Dispatcher = channel.Dispatcher
203- }
204-
205202 dataChannel.Handle()
206203 return dataChannel
207204 }
208205
206+func (b *BaseBroker) SetDispatcher(dispatcher MessageDispatcher, channels []*Channel) error {
207+ for _, channel := range channels {
208+ dataChannel := b.ensureChannel(channel)
209+ if reflect.TypeOf(dataChannel.Dispatcher) != reflect.TypeOf(dispatcher) {
210+ dataChannel.Dispatcher = dispatcher
211+ }
212+ }
213+ return nil
214+}
215+
209216 var _ Broker = (*BaseBroker)(nil)
+70 -0 pkg/pubsub/dispatcher.go #
......@@ -1,8 +1,78 @@
11 package pubsub
22
3+import (
4+ "slices"
5+ "strings"
6+ "sync"
7+)
8+
39 // MessageDispatcher defines how messages are dispatched to subscribers.
410 type MessageDispatcher interface {
511 // Dispatch sends a message to the appropriate subscriber(s).
612 // It receives the message, all subscribers, and the channel's sync primitives.
713 Dispatch(msg ChannelMessage, subscribers []*Client, channelDone chan struct{}) error
814 }
15+
16+// MulticastDispatcher sends each message to all eligible subscribers.
17+type MulticastDispatcher struct{}
18+
19+func (d *MulticastDispatcher) Dispatch(msg ChannelMessage, subscribers []*Client, channelDone chan struct{}) error {
20+ var wg sync.WaitGroup
21+ for _, client := range subscribers {
22+ wg.Add(1)
23+ go func(cl *Client) {
24+ defer wg.Done()
25+ select {
26+ case cl.Data <- msg:
27+ case <-cl.Done:
28+ case <-channelDone:
29+ }
30+ }(client)
31+ }
32+ wg.Wait()
33+ return nil
34+}
35+
36+/*
37+RoundRobin is a load-balancing broker that distributes published messages
38+to subscribers using a round-robin algorithm.
39+
40+Unlike Multicast which sends each message to all subscribers, RoundRobin
41+sends each message to exactly one subscriber, rotating through the available
42+subscribers for each published message. This provides load balancing for
43+message processing.
44+
45+It maintains independent round-robin state per channel/topic.
46+*/
47+type RoundRobinDispatcher struct {
48+ index uint32
49+ mu sync.Mutex
50+}
51+
52+func (d *RoundRobinDispatcher) Dispatch(msg ChannelMessage, subscribers []*Client, channelDone chan struct{}) error {
53+ // If no subscribers, nothing to dispatch
54+ // BlockWrite behavior at publish time ensures subscribers are present when needed
55+ if len(subscribers) == 0 {
56+ return nil
57+ }
58+
59+ slices.SortFunc(subscribers, func(a, b *Client) int {
60+ return strings.Compare(a.ID, b.ID)
61+ })
62+
63+ // Select the next subscriber in round-robin order
64+ d.mu.Lock()
65+ selectedIdx := int(d.index % uint32(len(subscribers)))
66+ d.index++
67+ d.mu.Unlock()
68+
69+ selectedClient := subscribers[selectedIdx]
70+
71+ select {
72+ case selectedClient.Data <- msg:
73+ case <-selectedClient.Done:
74+ case <-channelDone:
75+ }
76+
77+ return nil
78+}
+4 -23 pkg/pubsub/multicast.go #
......@@ -6,7 +6,6 @@ import (
66 "io"
77 "iter"
88 "log/slog"
9- "sync"
109
1110 "github.com/antoniomika/syncmap"
1211 )
......@@ -62,9 +61,11 @@ func (p *Multicast) GetSubs() iter.Seq2[string, *Client] {
6261 func (p *Multicast) connect(ctx context.Context, ID string, rw io.ReadWriter, channels []*Channel, direction ChannelDirection, blockWrite bool, replay, keepAlive bool, dispatcher MessageDispatcher) (error, error) {
6362 client := NewClient(ID, rw, direction, blockWrite, replay, keepAlive)
6463
65- // Set dispatcher on all channels
64+ // Set dispatcher on all channels (only if not already set)
6665 for _, ch := range channels {
67- ch.Dispatcher = dispatcher
66+ if ch.Dispatcher == nil {
67+ ch.Dispatcher = dispatcher
68+ }
6869 }
6970
7071 go func() {
......@@ -87,24 +88,4 @@ func (p *Multicast) Sub(ctx context.Context, ID string, rw io.ReadWriter, channe
8788 return errors.Join(p.connect(ctx, ID, rw, channels, ChannelDirectionOutput, false, false, keepAlive, &MulticastDispatcher{}))
8889 }
8990
90-// MulticastDispatcher sends each message to all eligible subscribers.
91-type MulticastDispatcher struct{}
92-
93-func (d *MulticastDispatcher) Dispatch(msg ChannelMessage, subscribers []*Client, channelDone chan struct{}) error {
94- var wg sync.WaitGroup
95- for _, client := range subscribers {
96- wg.Add(1)
97- go func(cl *Client) {
98- defer wg.Done()
99- select {
100- case cl.Data <- msg:
101- case <-cl.Done:
102- case <-channelDone:
103- }
104- }(client)
105- }
106- wg.Wait()
107- return nil
108-}
109-
11091 var _ PubSub = (*Multicast)(nil)
+0 -51 pkg/pubsub/roundrobin.go #
......@@ -1,51 +0,0 @@
1-package pubsub
2-
3-import (
4- "slices"
5- "strings"
6- "sync"
7-)
8-
9-/*
10-RoundRobin is a load-balancing broker that distributes published messages
11-to subscribers using a round-robin algorithm.
12-
13-Unlike Multicast which sends each message to all subscribers, RoundRobin
14-sends each message to exactly one subscriber, rotating through the available
15-subscribers for each published message. This provides load balancing for
16-message processing.
17-
18-It maintains independent round-robin state per channel/topic.
19-*/
20-type RoundRobinDispatcher struct {
21- index uint32
22- mu sync.Mutex
23-}
24-
25-func (d *RoundRobinDispatcher) Dispatch(msg ChannelMessage, subscribers []*Client, channelDone chan struct{}) error {
26- // If no subscribers, nothing to dispatch
27- // BlockWrite behavior at publish time ensures subscribers are present when needed
28- if len(subscribers) == 0 {
29- return nil
30- }
31-
32- slices.SortFunc(subscribers, func(a, b *Client) int {
33- return strings.Compare(a.ID, b.ID)
34- })
35-
36- // Select the next subscriber in round-robin order
37- d.mu.Lock()
38- selectedIdx := int(d.index % uint32(len(subscribers)))
39- d.index++
40- d.mu.Unlock()
41-
42- selectedClient := subscribers[selectedIdx]
43-
44- select {
45- case selectedClient.Data <- msg:
46- case <-selectedClient.Done:
47- case <-channelDone:
48- }
49-
50- return nil
51-}
Back to top