2026-02-04 11:06:13 +00:00
|
|
|
package bus
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
"errors"
|
|
|
|
|
"sync/atomic"
|
2026-02-23 13:34:37 +00:00
|
|
|
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
2026-02-04 11:06:13 +00:00
|
|
|
)
|
|
|
|
|
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
// ErrBusClosed is returned when publishing to a closed MessageBus.
|
|
|
|
|
var ErrBusClosed = errors.New("message bus closed")
|
|
|
|
|
|
2026-02-26 14:46:57 +00:00
|
|
|
const defaultBusBufferSize = 64
|
2026-02-24 14:30:22 +00:00
|
|
|
|
2026-02-04 11:06:13 +00:00
|
|
|
type MessageBus struct {
|
2026-02-22 19:10:57 +00:00
|
|
|
inbound chan InboundMessage
|
|
|
|
|
outbound chan OutboundMessage
|
|
|
|
|
outboundMedia chan OutboundMediaMessage
|
|
|
|
|
done chan struct{}
|
|
|
|
|
closed atomic.Bool
|
2026-02-04 11:06:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewMessageBus() *MessageBus {
|
|
|
|
|
return &MessageBus{
|
2026-02-24 14:30:22 +00:00
|
|
|
inbound: make(chan InboundMessage, defaultBusBufferSize),
|
|
|
|
|
outbound: make(chan OutboundMessage, defaultBusBufferSize),
|
|
|
|
|
outboundMedia: make(chan OutboundMediaMessage, defaultBusBufferSize),
|
2026-02-22 19:10:57 +00:00
|
|
|
done: make(chan struct{}),
|
2026-02-04 11:06:13 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
func (mb *MessageBus) PublishInbound(ctx context.Context, msg InboundMessage) error {
|
|
|
|
|
if mb.closed.Load() {
|
|
|
|
|
return ErrBusClosed
|
|
|
|
|
}
|
2026-02-26 06:03:01 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
select {
|
|
|
|
|
case mb.inbound <- msg:
|
|
|
|
|
return nil
|
|
|
|
|
case <-mb.done:
|
|
|
|
|
return ErrBusClosed
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return ctx.Err()
|
2026-02-16 07:04:25 +00:00
|
|
|
}
|
2026-02-04 11:06:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (mb *MessageBus) ConsumeInbound(ctx context.Context) (InboundMessage, bool) {
|
|
|
|
|
select {
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
case msg, ok := <-mb.inbound:
|
|
|
|
|
return msg, ok
|
|
|
|
|
case <-mb.done:
|
|
|
|
|
return InboundMessage{}, false
|
2026-02-04 11:06:13 +00:00
|
|
|
case <-ctx.Done():
|
|
|
|
|
return InboundMessage{}, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
func (mb *MessageBus) PublishOutbound(ctx context.Context, msg OutboundMessage) error {
|
|
|
|
|
if mb.closed.Load() {
|
|
|
|
|
return ErrBusClosed
|
|
|
|
|
}
|
2026-02-26 06:03:01 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
select {
|
|
|
|
|
case mb.outbound <- msg:
|
|
|
|
|
return nil
|
|
|
|
|
case <-mb.done:
|
|
|
|
|
return ErrBusClosed
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return ctx.Err()
|
2026-02-16 07:04:25 +00:00
|
|
|
}
|
2026-02-04 11:06:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (mb *MessageBus) SubscribeOutbound(ctx context.Context) (OutboundMessage, bool) {
|
|
|
|
|
select {
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
case msg, ok := <-mb.outbound:
|
|
|
|
|
return msg, ok
|
|
|
|
|
case <-mb.done:
|
|
|
|
|
return OutboundMessage{}, false
|
2026-02-04 11:06:13 +00:00
|
|
|
case <-ctx.Done():
|
|
|
|
|
return OutboundMessage{}, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 19:10:57 +00:00
|
|
|
func (mb *MessageBus) PublishOutboundMedia(ctx context.Context, msg OutboundMediaMessage) error {
|
|
|
|
|
if mb.closed.Load() {
|
|
|
|
|
return ErrBusClosed
|
|
|
|
|
}
|
2026-02-26 06:03:01 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-02-22 19:10:57 +00:00
|
|
|
select {
|
|
|
|
|
case mb.outboundMedia <- msg:
|
|
|
|
|
return nil
|
|
|
|
|
case <-mb.done:
|
|
|
|
|
return ErrBusClosed
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (mb *MessageBus) SubscribeOutboundMedia(ctx context.Context) (OutboundMediaMessage, bool) {
|
|
|
|
|
select {
|
|
|
|
|
case msg, ok := <-mb.outboundMedia:
|
|
|
|
|
return msg, ok
|
|
|
|
|
case <-mb.done:
|
|
|
|
|
return OutboundMediaMessage{}, false
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return OutboundMediaMessage{}, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 11:06:13 +00:00
|
|
|
func (mb *MessageBus) Close() {
|
refactor(bus): fix deadlock and concurrency issues in MessageBus
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
2026-02-22 16:44:45 +00:00
|
|
|
if mb.closed.CompareAndSwap(false, true) {
|
|
|
|
|
close(mb.done)
|
2026-02-23 13:34:37 +00:00
|
|
|
|
|
|
|
|
// Drain buffered channels so messages aren't silently lost.
|
|
|
|
|
// Channels are NOT closed to avoid send-on-closed panics from concurrent publishers.
|
|
|
|
|
drained := 0
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-mb.inbound:
|
|
|
|
|
drained++
|
|
|
|
|
default:
|
|
|
|
|
goto doneInbound
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
doneInbound:
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-mb.outbound:
|
|
|
|
|
drained++
|
|
|
|
|
default:
|
|
|
|
|
goto doneOutbound
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
doneOutbound:
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-mb.outboundMedia:
|
|
|
|
|
drained++
|
|
|
|
|
default:
|
|
|
|
|
goto doneMedia
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
doneMedia:
|
|
|
|
|
if drained > 0 {
|
|
|
|
|
logger.DebugCF("bus", "Drained buffered messages during close", map[string]any{
|
|
|
|
|
"count": drained,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-02-16 07:04:25 +00:00
|
|
|
}
|
2026-02-04 11:06:13 +00:00
|
|
|
}
|