introduce statistics collection

This commit is contained in:
Jay
2026-04-24 13:48:52 -04:00
parent 4ac2c488ad
commit 6a3ba05fd5
14 changed files with 453 additions and 140 deletions
+5
View File
@@ -22,6 +22,7 @@ type Connection = transport.Connection
type ConnectionConfig = transport.ConnectionConfig type ConnectionConfig = transport.ConnectionConfig
type RetryConfig = transport.RetryConfig type RetryConfig = transport.RetryConfig
type ConnectionOption = transport.ConnectionOption type ConnectionOption = transport.ConnectionOption
type ConnectionStats = transport.ConnectionStats
// Outbound Pool types // Outbound Pool types
@@ -33,6 +34,8 @@ type OutboundWorkerOption = outbound.WorkerOption
type OutboundInboxMessage = outbound.InboxMessage type OutboundInboxMessage = outbound.InboxMessage
type OutboundPoolEvent = outbound.PoolEvent type OutboundPoolEvent = outbound.PoolEvent
type OutboundPoolEventKind = outbound.PoolEventKind type OutboundPoolEventKind = outbound.PoolEventKind
type OutboundPoolStats = outbound.PoolStats
type OutboundWorkerStats = outbound.WorkerStats
// Pool event constants // Pool event constants
@@ -54,6 +57,8 @@ type InboundWorkerExitKind = inbound.WorkerExitKind
type InboundInboxMessage = inbound.InboxMessage type InboundInboxMessage = inbound.InboxMessage
type InboundPoolEvent = inbound.PoolEvent type InboundPoolEvent = inbound.PoolEvent
type InboundPoolEventKind = inbound.PoolEventKind type InboundPoolEventKind = inbound.PoolEventKind
type InboundPoolStats = inbound.PoolStats
type InboundWorkerStats = inbound.WorkerStats
// Inbound Pool event constants // Inbound Pool event constants
+96 -21
View File
@@ -8,6 +8,7 @@ import (
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
"log/slog" "log/slog"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@@ -36,6 +37,24 @@ type PoolEvent struct {
Kind PoolEventKind Kind PoolEventKind
} }
type PoolStats struct {
ChanInbox int
ChanEvents int
ChanErrors int
TotalReceived uint64
TotalSent uint64
PeerCount int
PeerStats []PeerStats
}
type PeerStats struct {
ID string
Worker WorkerStats
Connection transport.ConnectionStats
}
type InboxMessage struct { type InboxMessage struct {
ID string ID string
Data []byte Data []byte
@@ -43,11 +62,12 @@ type InboxMessage struct {
} }
type PoolPlugin struct { type PoolPlugin struct {
Inbox chan<- InboxMessage Inbox chan<- InboxMessage
Events chan<- PoolEvent Events chan<- PoolEvent
Errors chan<- error Errors chan<- error
OnExit OnExitFunction InboxCounter *atomic.Uint64
Handler slog.Handler OnExit OnExitFunction
Handler slog.Handler
} }
// Pool // Pool
@@ -70,6 +90,9 @@ type Pool struct {
events chan PoolEvent events chan PoolEvent
errors chan error errors chan error
inboxCounter *atomic.Uint64
outgoingCount *atomic.Uint64
config *PoolConfig config *PoolConfig
handler slog.Handler handler slog.Handler
logger *slog.Logger logger *slog.Logger
@@ -116,16 +139,18 @@ func NewPool(ctx context.Context, id string, config *PoolConfig, handler slog.Ha
} }
return &Pool{ return &Pool{
ctx: pctx, ctx: pctx,
cancel: cancel, cancel: cancel,
id: id, id: id,
peers: make(map[string]*Peer), peers: make(map[string]*Peer),
inbox: make(chan InboxMessage, config.InboxBufferSize), inbox: make(chan InboxMessage, config.InboxBufferSize),
events: make(chan PoolEvent, config.EventsBufferSize), events: make(chan PoolEvent, config.EventsBufferSize),
errors: make(chan error, config.ErrorsBufferSize), errors: make(chan error, config.ErrorsBufferSize),
config: config, inboxCounter: &atomic.Uint64{},
handler: handler, outgoingCount: &atomic.Uint64{},
logger: logger, config: config,
handler: handler,
logger: logger,
}, nil }, nil
} }
@@ -153,6 +178,49 @@ func (p *Pool) Errors() <-chan error {
return p.errors return p.errors
} }
func (p *Pool) Stats() PoolStats {
p.mu.RLock()
defer p.mu.RUnlock()
count := len(p.peers)
peerStats := make([]PeerStats, 0, count)
for id, peer := range p.peers {
peerStats = append(peerStats, PeerStats{
ID: id,
Worker: peer.worker.Stats(),
Connection: peer.conn.Stats(),
})
}
return PoolStats{
ChanInbox: len(p.inbox),
ChanEvents: len(p.events),
ChanErrors: len(p.errors),
TotalReceived: p.inboxCounter.Load(),
TotalSent: p.outgoingCount.Load(),
PeerCount: len(p.peers),
PeerStats: peerStats,
}
}
func (p *Pool) PeerStats(id string) (PeerStats, error) {
p.mu.RLock()
defer p.mu.RUnlock()
peer, exists := p.peers[id]
if !exists {
return PeerStats{}, ErrPeerNotFound
}
return PeerStats{
ID: id,
Worker: peer.worker.Stats(),
Connection: peer.conn.Stats(),
}, nil
}
func (p *Pool) Close() { func (p *Pool) Close() {
if p.logger != nil { if p.logger != nil {
p.logger.Debug("closing") p.logger.Debug("closing")
@@ -266,7 +334,13 @@ func (p *Pool) Send(id string, data []byte) error {
return ErrPeerNotFound return ErrPeerNotFound
} }
return peer.worker.Send(data) err := peer.worker.Send(data)
if err != nil {
return err
}
p.outgoingCount.Add(1)
return nil
} }
// addLocked constructs and registers a peer. Caller must hold p.mu write lock. // addLocked constructs and registers a peer. Caller must hold p.mu write lock.
@@ -315,11 +389,12 @@ func (p *Pool) addLocked(id string, socket types.Socket) error {
} }
pool := PoolPlugin{ pool := PoolPlugin{
Inbox: p.inbox, Inbox: p.inbox,
Events: p.events, Events: p.events,
Errors: p.errors, Errors: p.errors,
OnExit: onExit, InboxCounter: p.inboxCounter,
Handler: p.handler, OnExit: onExit,
Handler: p.handler,
} }
peer := &Peer{ peer := &Peer{
+60 -20
View File
@@ -8,6 +8,7 @@ import (
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
"log/slog" "log/slog"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@@ -15,6 +16,7 @@ type Worker interface {
Start(pool PoolPlugin) Start(pool PoolPlugin)
Stop() Stop()
Send(data []byte) error Send(data []byte) error
Stats() WorkerStats
} }
type WorkerExitKind string type WorkerExitKind string
@@ -26,14 +28,32 @@ const (
ExitPolicy WorkerExitKind = "policy" ExitPolicy WorkerExitKind = "policy"
) )
type WorkerStats struct {
ChanIncoming int
ChanQueue int
ChanForwarder int
TotalProcessed uint64
TotalDropped uint64
TotalSent uint64
}
type DefaultWorker struct { type DefaultWorker struct {
id string id string
conn *transport.Connection conn *transport.Connection
heartbeat chan struct{}
config *WorkerConfig heartbeat chan struct{}
ctx context.Context toQueue chan types.ReceivedMessage
cancel context.CancelFunc toForwarder chan types.ReceivedMessage
logger *slog.Logger
processedCount *atomic.Uint64
droppedCount *atomic.Uint64
outgoingCount *atomic.Uint64
ctx context.Context
cancel context.CancelFunc
config *WorkerConfig
logger *slog.Logger
} }
func NewWorker( func NewWorker(
@@ -52,13 +72,18 @@ func NewWorker(
wctx, cancel := context.WithCancel(ctx) wctx, cancel := context.WithCancel(ctx)
return &DefaultWorker{ return &DefaultWorker{
id: id, id: id,
conn: conn, conn: conn,
heartbeat: make(chan struct{}), heartbeat: make(chan struct{}),
config: config, toQueue: make(chan types.ReceivedMessage, 256),
ctx: wctx, toForwarder: make(chan types.ReceivedMessage, 256),
cancel: cancel, processedCount: &atomic.Uint64{},
logger: logger, droppedCount: &atomic.Uint64{},
outgoingCount: &atomic.Uint64{},
config: config,
ctx: wctx,
cancel: cancel,
logger: logger,
}, nil }, nil
} }
@@ -67,15 +92,12 @@ func (w *DefaultWorker) Start(pool PoolPlugin) {
w.logger.Debug("starting") w.logger.Debug("starting")
} }
toQueue := make(chan types.ReceivedMessage, 256)
toForwarder := make(chan types.ReceivedMessage, 256)
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(5) wg.Add(5)
go func() { go func() {
defer wg.Done() defer wg.Done()
RunReader(w.ctx, pool.OnExit, w.conn, toQueue, w.heartbeat, w.logger) RunReader(w.ctx, pool.OnExit, w.conn, w.toQueue, w.heartbeat, w.logger)
}() }()
go func() { go func() {
@@ -85,12 +107,12 @@ func (w *DefaultWorker) Start(pool PoolPlugin) {
go func() { go func() {
defer wg.Done() defer wg.Done()
queue.RunQueue(w.id, w.ctx, toQueue, toForwarder, w.config.MaxQueueSize) queue.RunQueue(w.id, w.ctx, w.toQueue, w.toForwarder, w.config.MaxQueueSize, w.droppedCount)
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
RunForwarder(w.id, w.ctx, toForwarder, pool.Inbox) RunForwarder(w.id, w.ctx, w.toForwarder, pool.Inbox, w.processedCount, pool.InboxCounter)
}() }()
go func() { go func() {
@@ -126,9 +148,23 @@ func (w *DefaultWorker) Send(data []byte) error {
case <-w.ctx.Done(): case <-w.ctx.Done():
} }
w.outgoingCount.Add(1)
return nil return nil
} }
func (w *DefaultWorker) Stats() WorkerStats {
return WorkerStats{
ChanIncoming: len(w.conn.Incoming()),
ChanQueue: len(w.toQueue),
ChanForwarder: len(w.toForwarder),
TotalProcessed: w.processedCount.Load(),
TotalDropped: w.droppedCount.Load(),
TotalSent: w.outgoingCount.Load(),
}
}
func RunReader( func RunReader(
ctx context.Context, ctx context.Context,
onPeerClose OnExitFunction, onPeerClose OnExitFunction,
@@ -217,6 +253,8 @@ func RunForwarder(
ctx context.Context, ctx context.Context,
messages <-chan types.ReceivedMessage, messages <-chan types.ReceivedMessage,
inbox chan<- InboxMessage, inbox chan<- InboxMessage,
workerProcessedCount *atomic.Uint64,
poolInboxCount *atomic.Uint64,
) { ) {
for { for {
select { select {
@@ -235,6 +273,8 @@ func RunForwarder(
Data: msg.Data, Data: msg.Data,
ReceivedAt: msg.ReceivedAt, ReceivedAt: msg.ReceivedAt,
}: }:
workerProcessedCount.Add(1)
poolInboxCount.Add(1)
} }
} }
} }
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"git.wisehodl.dev/jay/go-honeybee/honeybeetest" "git.wisehodl.dev/jay/go-honeybee/honeybeetest"
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@@ -16,7 +17,7 @@ func TestRunForwarder(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
go RunForwarder(id, ctx, messages, inbox) go RunForwarder(id, ctx, messages, inbox, &atomic.Uint64{}, &atomic.Uint64{})
messages <- types.ReceivedMessage{Data: []byte("hello"), ReceivedAt: time.Now()} messages <- types.ReceivedMessage{Data: []byte("hello"), ReceivedAt: time.Now()}
+1
View File
@@ -47,6 +47,7 @@ func setupWorkerTest(t *testing.T) workerTestVars {
OnExit: func(kind WorkerExitKind) { OnExit: func(kind WorkerExitKind) {
once.Do(func() { exitKind.Store(kind) }) once.Do(func() { exitKind.Store(kind) })
}, },
InboxCounter: &atomic.Uint64{},
} }
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
+84 -12
View File
@@ -7,6 +7,7 @@ import (
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
"log/slog" "log/slog"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@@ -24,6 +25,23 @@ type PoolEvent struct {
Kind PoolEventKind Kind PoolEventKind
} }
type PoolStats struct {
ChanInbox int
ChanEvents int
ChanErrors int
TotalReceived uint64
TotalSent uint64
PeerCount int
PeerStats []PeerStats
}
type PeerStats struct {
ID string
Worker WorkerStats
}
type InboxMessage struct { type InboxMessage struct {
ID string ID string
Data []byte Data []byte
@@ -35,6 +53,7 @@ type PoolPlugin struct {
Inbox chan<- InboxMessage Inbox chan<- InboxMessage
Events chan<- PoolEvent Events chan<- PoolEvent
Errors chan<- error Errors chan<- error
InboxCounter *atomic.Uint64
Dialer types.Dialer Dialer types.Dialer
ConnectionConfig *transport.ConnectionConfig ConnectionConfig *transport.ConnectionConfig
Handler slog.Handler Handler slog.Handler
@@ -58,6 +77,9 @@ type Pool struct {
events chan PoolEvent events chan PoolEvent
errors chan error errors chan error
inboxCounter *atomic.Uint64
outgoingCount *atomic.Uint64
dialer types.Dialer dialer types.Dialer
config *PoolConfig config *PoolConfig
handler slog.Handler handler slog.Handler
@@ -101,17 +123,19 @@ func NewPool(ctx context.Context, id string, config *PoolConfig, handler slog.Ha
} }
return &Pool{ return &Pool{
ctx: pctx, ctx: pctx,
cancel: cancel, cancel: cancel,
id: id, id: id,
peers: make(map[string]*Peer), peers: make(map[string]*Peer),
inbox: make(chan InboxMessage, config.InboxBufferSize), inbox: make(chan InboxMessage, config.InboxBufferSize),
events: make(chan PoolEvent, config.EventsBufferSize), events: make(chan PoolEvent, config.EventsBufferSize),
errors: make(chan error, config.ErrorsBufferSize), errors: make(chan error, config.ErrorsBufferSize),
dialer: transport.NewDialer(), inboxCounter: &atomic.Uint64{},
config: config, outgoingCount: &atomic.Uint64{},
handler: handler, dialer: transport.NewDialer(),
logger: logger, config: config,
handler: handler,
logger: logger,
}, nil }, nil
} }
@@ -138,6 +162,47 @@ func (p *Pool) Errors() <-chan error {
return p.errors return p.errors
} }
func (p *Pool) Stats() PoolStats {
p.mu.RLock()
defer p.mu.RUnlock()
count := len(p.peers)
peerStats := make([]PeerStats, 0, count)
for id, peer := range p.peers {
peerStats = append(peerStats, PeerStats{
ID: id,
Worker: peer.worker.Stats(),
})
}
return PoolStats{
ChanInbox: len(p.inbox),
ChanEvents: len(p.events),
ChanErrors: len(p.errors),
TotalReceived: p.inboxCounter.Load(),
TotalSent: p.outgoingCount.Load(),
PeerCount: len(p.peers),
PeerStats: peerStats,
}
}
func (p *Pool) PeerStats(id string) (PeerStats, error) {
p.mu.RLock()
defer p.mu.RUnlock()
peer, exists := p.peers[id]
if !exists {
return PeerStats{}, ErrPeerNotFound
}
return PeerStats{
ID: id,
Worker: peer.worker.Stats(),
}, nil
}
func (p *Pool) SetDialer(d types.Dialer) { func (p *Pool) SetDialer(d types.Dialer) {
if d == nil { if d == nil {
panic("dialer cannot be nil") panic("dialer cannot be nil")
@@ -214,6 +279,7 @@ func (p *Pool) Connect(id string) error {
Inbox: p.inbox, Inbox: p.inbox,
Events: p.events, Events: p.events,
Errors: p.errors, Errors: p.errors,
InboxCounter: p.inboxCounter,
Dialer: p.dialer, Dialer: p.dialer,
ConnectionConfig: p.config.ConnectionConfig, ConnectionConfig: p.config.ConnectionConfig,
Handler: p.handler, Handler: p.handler,
@@ -284,5 +350,11 @@ func (p *Pool) Send(id string, data []byte) error {
return NewPoolError(ErrPeerNotFound) return NewPoolError(ErrPeerNotFound)
} }
return peer.worker.Send(data) err = peer.worker.Send(data)
if err != nil {
return err
}
p.outgoingCount.Add(1)
return nil
} }
+84 -18
View File
@@ -18,16 +18,41 @@ type Worker interface {
Start(pool PoolPlugin) Start(pool PoolPlugin)
Stop() Stop()
Send(data []byte) error Send(data []byte) error
Stats() WorkerStats
}
type WorkerStats struct {
IncomingAvailable bool
ChanIncoming int
ChanQueue int
ChanForwarder int
ConnectionAvailable bool
Connection transport.ConnectionStats
TotalProcessed uint64
TotalDropped uint64
TotalSent uint64
TotalRestarts uint64
} }
type DefaultWorker struct { type DefaultWorker struct {
id string id string
conn atomic.Pointer[transport.Connection] conn atomic.Pointer[transport.Connection]
heartbeat chan struct{}
config *WorkerConfig heartbeat chan struct{}
ctx context.Context toQueue chan types.ReceivedMessage
cancel context.CancelFunc toForwarder chan types.ReceivedMessage
logger *slog.Logger
processedCount *atomic.Uint64
droppedCount *atomic.Uint64
outgoingCount *atomic.Uint64
restartCount *atomic.Uint64
config *WorkerConfig
ctx context.Context
cancel context.CancelFunc
logger *slog.Logger
} }
func NewWorker( func NewWorker(
@@ -45,12 +70,18 @@ func NewWorker(
wctx, wcancel := context.WithCancel(ctx) wctx, wcancel := context.WithCancel(ctx)
w := &DefaultWorker{ w := &DefaultWorker{
id: id, id: id,
config: config, config: config,
heartbeat: make(chan struct{}), heartbeat: make(chan struct{}),
ctx: wctx, toQueue: make(chan types.ReceivedMessage, 256),
cancel: wcancel, toForwarder: make(chan types.ReceivedMessage, 256),
logger: logger, processedCount: &atomic.Uint64{},
droppedCount: &atomic.Uint64{},
outgoingCount: &atomic.Uint64{},
restartCount: &atomic.Uint64{},
ctx: wctx,
cancel: wcancel,
logger: logger,
} }
return w, nil return w, nil
@@ -63,8 +94,6 @@ func (w *DefaultWorker) Start(pool PoolPlugin) {
dial := make(chan struct{}, 1) dial := make(chan struct{}, 1)
newConn := make(chan *transport.Connection, 1) newConn := make(chan *transport.Connection, 1)
toQueue := make(chan types.ReceivedMessage, 256)
toForwarder := make(chan types.ReceivedMessage, 256)
keepalive := make(chan struct{}, 1) keepalive := make(chan struct{}, 1)
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -82,12 +111,12 @@ func (w *DefaultWorker) Start(pool PoolPlugin) {
go func() { go func() {
defer wg.Done() defer wg.Done()
queue.RunQueue(w.id, w.ctx, toQueue, toForwarder, w.config.MaxQueueSize) queue.RunQueue(w.id, w.ctx, w.toQueue, w.toForwarder, w.config.MaxQueueSize, w.droppedCount)
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
RunForwarder(w.id, w.ctx, toForwarder, pool.Inbox) RunForwarder(w.id, w.ctx, w.toForwarder, pool.Inbox, w.processedCount, pool.InboxCounter)
}() }()
go func() { go func() {
@@ -95,12 +124,13 @@ func (w *DefaultWorker) Start(pool PoolPlugin) {
session := &Session{ session := &Session{
id: w.id, id: w.id,
connPtr: &w.conn, connPtr: &w.conn,
messages: toQueue, messages: w.toQueue,
heartbeat: w.heartbeat, heartbeat: w.heartbeat,
dial: dial, dial: dial,
keepalive: keepalive, keepalive: keepalive,
newConn: newConn, newConn: newConn,
reconnectDelay: w.config.ReconnectDelay, reconnectDelay: w.config.ReconnectDelay,
restartCount: w.restartCount,
logger: w.logger, logger: w.logger,
} }
session.Start(w.ctx, pool) session.Start(w.ctx, pool)
@@ -140,9 +170,39 @@ func (w *DefaultWorker) Send(data []byte) error {
case <-w.ctx.Done(): case <-w.ctx.Done():
} }
w.outgoingCount.Add(1)
return nil return nil
} }
func (w *DefaultWorker) Stats() WorkerStats {
connectionAvailable := false
incomingLen := 0
connStats := transport.ConnectionStats{}
conn := w.conn.Load()
if conn != nil {
connectionAvailable = true
incomingLen = len(conn.Incoming())
connStats = conn.Stats()
}
return WorkerStats{
IncomingAvailable: connectionAvailable,
ChanIncoming: incomingLen,
ChanQueue: len(w.toQueue),
ChanForwarder: len(w.toForwarder),
ConnectionAvailable: connectionAvailable,
Connection: connStats,
TotalProcessed: w.processedCount.Load(),
TotalDropped: w.droppedCount.Load(),
TotalRestarts: w.restartCount.Load(),
TotalSent: w.outgoingCount.Load(),
}
}
type Session struct { type Session struct {
id string id string
connPtr *atomic.Pointer[transport.Connection] connPtr *atomic.Pointer[transport.Connection]
@@ -155,6 +215,7 @@ type Session struct {
newConn <-chan *transport.Connection newConn <-chan *transport.Connection
reconnectDelay time.Duration reconnectDelay time.Duration
restartCount *atomic.Uint64
logger *slog.Logger logger *slog.Logger
} }
@@ -246,6 +307,7 @@ func (s *Session) Start(
// refresh session // refresh session
time.Sleep(s.reconnectDelay) time.Sleep(s.reconnectDelay)
s.restartCount.Add(1)
} }
} }
@@ -346,6 +408,8 @@ func RunForwarder(
ctx context.Context, ctx context.Context,
messages <-chan types.ReceivedMessage, messages <-chan types.ReceivedMessage,
inbox chan<- InboxMessage, inbox chan<- InboxMessage,
workerProcessedCount *atomic.Uint64,
poolInboxCount *atomic.Uint64,
) { ) {
for { for {
select { select {
@@ -364,6 +428,8 @@ func RunForwarder(
Data: msg.Data, Data: msg.Data,
ReceivedAt: msg.ReceivedAt, ReceivedAt: msg.ReceivedAt,
}: }:
workerProcessedCount.Add(1)
poolInboxCount.Add(1)
} }
} }
} }
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"git.wisehodl.dev/jay/go-honeybee/honeybeetest" "git.wisehodl.dev/jay/go-honeybee/honeybeetest"
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@@ -16,7 +17,7 @@ func TestRunForwarder(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
go RunForwarder(id, ctx, messages, inbox) go RunForwarder(id, ctx, messages, inbox, &atomic.Uint64{}, &atomic.Uint64{})
messages <- types.ReceivedMessage{Data: []byte("hello"), ReceivedAt: time.Now()} messages <- types.ReceivedMessage{Data: []byte("hello"), ReceivedAt: time.Now()}
+10 -8
View File
@@ -20,10 +20,11 @@ func TestWorkerSend(t *testing.T) {
heartbeatCount := atomic.Int32{} heartbeatCount := atomic.Int32{}
w := &DefaultWorker{ w := &DefaultWorker{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
id: "wss://test", id: "wss://test",
heartbeat: heartbeat, heartbeat: heartbeat,
outgoingCount: &atomic.Uint64{},
} }
w.conn.Store(conn) w.conn.Store(conn)
defer w.cancel() defer w.cancel()
@@ -64,10 +65,11 @@ func TestWorkerSend(t *testing.T) {
heartbeatCount := atomic.Int32{} heartbeatCount := atomic.Int32{}
w := &DefaultWorker{ w := &DefaultWorker{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
id: "wss://test", id: "wss://test",
heartbeat: heartbeat, heartbeat: heartbeat,
outgoingCount: &atomic.Uint64{},
} }
w.conn.Store(conn) w.conn.Store(conn)
defer w.cancel() defer w.cancel()
+32 -28
View File
@@ -211,13 +211,14 @@ func TestRunSessionDisconnect(t *testing.T) {
events := make(chan PoolEvent, 10) events := make(chan PoolEvent, 10)
pool := PoolPlugin{Events: events} pool := PoolPlugin{Events: events}
session := &Session{ session := &Session{
id: v.id, id: v.id,
connPtr: v.connPtr, connPtr: v.connPtr,
messages: v.messages, messages: v.messages,
heartbeat: v.heartbeat, heartbeat: v.heartbeat,
dial: v.dial, dial: v.dial,
keepalive: v.keepalive, keepalive: v.keepalive,
newConn: v.newConn, newConn: v.newConn,
restartCount: &atomic.Uint64{},
} }
go session.Start(ctx, pool) go session.Start(ctx, pool)
@@ -237,13 +238,14 @@ func TestRunSessionDisconnect(t *testing.T) {
events := make(chan PoolEvent, 10) events := make(chan PoolEvent, 10)
pool := PoolPlugin{Events: events} pool := PoolPlugin{Events: events}
session := &Session{ session := &Session{
id: v.id, id: v.id,
connPtr: v.connPtr, connPtr: v.connPtr,
messages: v.messages, messages: v.messages,
heartbeat: v.heartbeat, heartbeat: v.heartbeat,
dial: v.dial, dial: v.dial,
keepalive: v.keepalive, keepalive: v.keepalive,
newConn: v.newConn, newConn: v.newConn,
restartCount: &atomic.Uint64{},
} }
go session.Start(ctx, pool) go session.Start(ctx, pool)
@@ -266,13 +268,14 @@ func TestRunSessionDisconnect(t *testing.T) {
events := make(chan PoolEvent, 10) events := make(chan PoolEvent, 10)
pool := PoolPlugin{Events: events} pool := PoolPlugin{Events: events}
session := &Session{ session := &Session{
id: v.id, id: v.id,
connPtr: v.connPtr, connPtr: v.connPtr,
messages: v.messages, messages: v.messages,
heartbeat: v.heartbeat, heartbeat: v.heartbeat,
dial: v.dial, dial: v.dial,
keepalive: v.keepalive, keepalive: v.keepalive,
newConn: v.newConn, newConn: v.newConn,
restartCount: &atomic.Uint64{},
} }
go session.Start(ctx, pool) go session.Start(ctx, pool)
@@ -303,13 +306,14 @@ func TestRunSessionDisconnect(t *testing.T) {
events := make(chan PoolEvent, 10) events := make(chan PoolEvent, 10)
pool := PoolPlugin{Events: events} pool := PoolPlugin{Events: events}
session := &Session{ session := &Session{
id: v.id, id: v.id,
connPtr: v.connPtr, connPtr: v.connPtr,
messages: v.messages, messages: v.messages,
heartbeat: v.heartbeat, heartbeat: v.heartbeat,
dial: v.dial, dial: v.dial,
keepalive: v.keepalive, keepalive: v.keepalive,
newConn: v.newConn, newConn: v.newConn,
restartCount: &atomic.Uint64{},
} }
go session.Start(ctx, pool) go session.Start(ctx, pool)
+16 -8
View File
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http" "net/http"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@@ -25,9 +26,10 @@ func makeWorkerContext(t *testing.T) (
events = make(chan PoolEvent, 10) events = make(chan PoolEvent, 10)
errors = make(chan error, 10) errors = make(chan error, 10)
pool = PoolPlugin{ pool = PoolPlugin{
Inbox: inbox, Inbox: inbox,
Events: events, Events: events,
Errors: errors, Errors: errors,
InboxCounter: &atomic.Uint64{},
} }
return return
} }
@@ -38,11 +40,17 @@ func makeWorker(t *testing.T, ctx context.Context, cancel context.CancelFunc) *D
WithReconnectDelay(0 * time.Second), WithReconnectDelay(0 * time.Second),
) )
return &DefaultWorker{ return &DefaultWorker{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
id: "wss://test", id: "wss://test",
config: config, config: config,
heartbeat: make(chan struct{}), heartbeat: make(chan struct{}),
toQueue: make(chan types.ReceivedMessage, 256),
toForwarder: make(chan types.ReceivedMessage, 256),
processedCount: &atomic.Uint64{},
droppedCount: &atomic.Uint64{},
outgoingCount: &atomic.Uint64{},
restartCount: &atomic.Uint64{},
} }
} }
+3
View File
@@ -3,6 +3,7 @@ package queue
import ( import (
"context" "context"
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
"sync/atomic"
) )
func RunQueue( func RunQueue(
@@ -11,6 +12,7 @@ func RunQueue(
in <-chan types.ReceivedMessage, in <-chan types.ReceivedMessage,
out chan<- types.ReceivedMessage, out chan<- types.ReceivedMessage,
maxQueueSize int, maxQueueSize int,
droppedCount *atomic.Uint64,
) { ) {
var next types.ReceivedMessage var next types.ReceivedMessage
var queue messageQueue var queue messageQueue
@@ -37,6 +39,7 @@ func RunQueue(
if maxQueueSize > 0 && queue.len() >= maxQueueSize { if maxQueueSize > 0 && queue.len() >= maxQueueSize {
// drop oldest message // drop oldest message
_ = queue.pop() _ = queue.pop()
droppedCount.Add(1)
} }
// add new message // add new message
queue.push(msg) queue.push(msg)
+4 -3
View File
@@ -5,6 +5,7 @@ import (
"git.wisehodl.dev/jay/go-honeybee/honeybeetest" "git.wisehodl.dev/jay/go-honeybee/honeybeetest"
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@@ -17,7 +18,7 @@ func TestRunQueue(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
go RunQueue(id, ctx, inChan, outChan, 0) go RunQueue(id, ctx, inChan, outChan, 0, &atomic.Uint64{})
inChan <- types.ReceivedMessage{Data: []byte("hello"), ReceivedAt: time.Now()} inChan <- types.ReceivedMessage{Data: []byte("hello"), ReceivedAt: time.Now()}
@@ -49,7 +50,7 @@ func TestRunQueue(t *testing.T) {
} }
}() }()
go RunQueue(id, ctx, inChan, gatedInbox, 2) go RunQueue(id, ctx, inChan, gatedInbox, 2, &atomic.Uint64{})
// send three messages while the gated inbox is blocked // send three messages while the gated inbox is blocked
inChan <- types.ReceivedMessage{Data: []byte("first"), ReceivedAt: time.Now()} inChan <- types.ReceivedMessage{Data: []byte("first"), ReceivedAt: time.Now()}
@@ -87,7 +88,7 @@ func TestRunQueue(t *testing.T) {
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
RunQueue(id, ctx, inChan, outChan, 0) RunQueue(id, ctx, inChan, outChan, 0, &atomic.Uint64{})
close(done) close(done)
}() }()
+54 -20
View File
@@ -8,6 +8,7 @@ import (
"math/rand" "math/rand"
"net/url" "net/url"
"sync" "sync"
"sync/atomic"
"time" "time"
"git.wisehodl.dev/jay/go-honeybee/types" "git.wisehodl.dev/jay/go-honeybee/types"
@@ -38,6 +39,15 @@ func (s ConnectionState) String() string {
} }
} }
type ConnectionStats struct {
ChanIncoming int
ChanErrors int
TotalReceived uint64
TotalSent uint64
TotalHeartbeats uint64
}
type Connection struct { type Connection struct {
url *url.URL url *url.URL
dialer types.Dialer dialer types.Dialer
@@ -50,6 +60,10 @@ type Connection struct {
errors chan error errors chan error
done chan struct{} done chan struct{}
incomingCount *atomic.Uint64
outgoingCount *atomic.Uint64
heartbeatCount *atomic.Uint64
state ConnectionState state ConnectionState
wg sync.WaitGroup wg sync.WaitGroup
@@ -75,16 +89,19 @@ func NewConnection(urlStr string, config *ConnectionConfig, logger *slog.Logger)
} }
conn := &Connection{ conn := &Connection{
url: url, url: url,
dialer: NewDialer(), dialer: NewDialer(),
socket: nil, socket: nil,
config: config, config: config,
logger: logger, logger: logger,
incoming: make(chan []byte, config.IncomingBufferSize), incoming: make(chan []byte, config.IncomingBufferSize),
heartbeat: make(chan struct{}, 1), heartbeat: make(chan struct{}, 1),
errors: make(chan error, config.ErrorsBufferSize), errors: make(chan error, config.ErrorsBufferSize),
state: StateDisconnected, incomingCount: &atomic.Uint64{},
done: make(chan struct{}), outgoingCount: &atomic.Uint64{},
heartbeatCount: &atomic.Uint64{},
state: StateDisconnected,
done: make(chan struct{}),
} }
return conn, nil return conn, nil
@@ -106,16 +123,19 @@ func NewConnectionFromSocket(
} }
conn := &Connection{ conn := &Connection{
url: nil, url: nil,
dialer: nil, dialer: nil,
socket: socket, socket: socket,
config: config, config: config,
logger: logger, logger: logger,
incoming: make(chan []byte, config.IncomingBufferSize), incoming: make(chan []byte, config.IncomingBufferSize),
heartbeat: make(chan struct{}, 1), heartbeat: make(chan struct{}, 1),
errors: make(chan error, config.ErrorsBufferSize), errors: make(chan error, config.ErrorsBufferSize),
state: StateConnected, incomingCount: &atomic.Uint64{},
done: make(chan struct{}), outgoingCount: &atomic.Uint64{},
heartbeatCount: &atomic.Uint64{},
state: StateConnected,
done: make(chan struct{}),
} }
if config.CloseHandler != nil { if config.CloseHandler != nil {
@@ -336,6 +356,7 @@ func (c *Connection) startReader() {
case <-c.done: case <-c.done:
return return
case c.incoming <- data: case c.incoming <- data:
c.incomingCount.Add(1)
} }
} }
@@ -348,6 +369,7 @@ func (c *Connection) setupPongHandler() {
c.socket.SetPongHandler(func(appData string) error { c.socket.SetPongHandler(func(appData string) error {
select { select {
case c.heartbeat <- struct{}{}: case c.heartbeat <- struct{}{}:
c.heartbeatCount.Add(1)
default: default:
} }
return nil return nil
@@ -410,6 +432,8 @@ func (c *Connection) Send(data []byte) error {
return NewConnectionError(fmt.Errorf("%w: %w", ErrWriteFailed, err)) return NewConnectionError(fmt.Errorf("%w: %w", ErrWriteFailed, err))
} }
c.outgoingCount.Add(1)
return nil return nil
} }
@@ -431,6 +455,16 @@ func (c *Connection) State() ConnectionState {
return c.state return c.state
} }
func (c *Connection) Stats() ConnectionStats {
return ConnectionStats{
ChanIncoming: len(c.incoming),
ChanErrors: len(c.errors),
TotalReceived: c.incomingCount.Load(),
TotalSent: c.outgoingCount.Load(),
TotalHeartbeats: c.heartbeatCount.Load(),
}
}
func (c *Connection) SetDialer(d types.Dialer) { func (c *Connection) SetDialer(d types.Dialer) {
c.dialer = d c.dialer = d
} }