yao/event/sub.go
Max 1e4e1224e2 Enhance subscription management by adding cancel functions and improving channel handling
- Update the Subscribe and SubscribeFrom methods to return a cancel function, ensuring proper resource cleanup when subscriptions are no longer needed.
- Modify unsubscribe logic to close channels safely, preventing potential panics from sending on closed channels.
- Enhance test cases to utilize the new cancel functionality, ensuring robust handling of subscriptions in various scenarios.
2026-02-24 00:22:08 +08:00

124 lines
2.7 KiB
Go

package event
import (
"fmt"
"sync"
"sync/atomic"
"github.com/yaoapp/yao/event/types"
)
var subIDCounter atomic.Uint64
func nextSubID() string {
id := subIDCounter.Add(1)
return fmt.Sprintf("sub-%d", id)
}
// subEntry holds a dynamic subscriber registration.
type subEntry struct {
id string
pattern string
filter func(*types.Event) bool
ch chan<- *types.Event
}
// subManager manages dynamic subscribers.
type subManager struct {
mu sync.RWMutex
entries map[string]*subEntry // id -> entry
}
func newSubManager() *subManager {
return &subManager{
entries: make(map[string]*subEntry),
}
}
// subscribe adds a dynamic subscriber. Returns the subscription ID.
func (sm *subManager) subscribe(pattern string, ch chan<- *types.Event, opts ...types.FilterOption) string {
fe := &types.FilterEntry{Pattern: pattern}
for _, opt := range opts {
opt(fe)
}
id := nextSubID()
sm.mu.Lock()
defer sm.mu.Unlock()
sm.entries[id] = &subEntry{
id: id,
pattern: pattern,
filter: fe.Filter,
ch: ch,
}
return id
}
// unsubscribe removes a subscriber by ID and closes its channel
// so that any goroutine blocked on `range ch` will unblock and exit.
func (sm *subManager) unsubscribe(id string) {
sm.mu.Lock()
entry, ok := sm.entries[id]
delete(sm.entries, id)
sm.mu.Unlock()
if ok && entry.ch != nil {
func() {
defer func() { recover() }()
close(entry.ch)
}()
}
}
// notify sends an event to all matching subscribers (non-blocking).
// Recovers from send-on-closed-channel panics that may occur if
// unsubscribe closes a channel concurrently.
func (sm *subManager) notify(ev *types.Event) {
sm.mu.RLock()
defer sm.mu.RUnlock()
for _, entry := range sm.entries {
if !matchPattern(entry.pattern, ev.Type) {
continue
}
if entry.filter != nil && !entry.filter(ev) {
continue
}
func() {
defer func() { recover() }()
select {
case entry.ch <- ev:
default:
}
}()
}
}
// clear removes all subscribers and closes their channels. Used during Stop.
func (sm *subManager) clear() {
sm.mu.Lock()
old := sm.entries
sm.entries = make(map[string]*subEntry)
sm.mu.Unlock()
for _, entry := range old {
if entry.ch != nil {
func() {
defer func() { recover() }()
close(entry.ch)
}()
}
}
}
// Subscribe dynamically subscribes to events matching the given pattern.
// Returns the subscription ID for later unsubscription.
// Event delivery is non-blocking: if ch is full, the event is skipped.
func Subscribe(pattern string, ch chan<- *types.Event, opts ...types.FilterOption) string {
return svc.smgr.subscribe(pattern, ch, opts...)
}
// Unsubscribe removes a dynamic subscription by ID.
func Unsubscribe(id string) {
svc.smgr.unsubscribe(id)
}