Files
ntfy/util/linger_queue_test.go
T
binwiederhier 134d8bb653 Show each cluster node's view of its peers: status endpoint, per-peer metrics, CLI
A link that only works one way (a firewall dropping the cluster port in one direction) left
both nodes healthy: the sender only logged lost batches and bumped an unlabeled error counter,
and the receiver had no way to see it never heard from that peer. Every node now counts the
message batches it sends to and receives from each peer, and shows them:

- GET /v1/cluster/status on the cluster listener (secret-authenticated): role, leadership,
  health, and per peer the outbound and inbound counters, last success and last error
- ntfy_cluster_peer_batches_sent_total{peer,result}, ntfy_cluster_peer_batches_received_total{peer}
  and ntfy_cluster_peer_last_send_success_timestamp_seconds{peer}
- ntfy cluster status [--json], which reads the listener and secret from the server config and
  lists the link problems

/v1/health and /v1/ready are unchanged.
2026-10-18 22:59:21 +00:00

106 lines
3.3 KiB
Go

package util_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/util"
)
func TestLingerQueue_LingerFlush(t *testing.T) {
// Items enqueued within the linger window are emitted as a single batch when it expires
q := util.NewLingerQueue[int](16, 100, 0, nil, 50*time.Millisecond)
defer q.Close()
require.True(t, q.TryEnqueue(1))
require.True(t, q.TryEnqueue(2))
require.True(t, q.TryEnqueue(3))
start := time.Now()
batch := <-q.Dequeue()
require.Equal(t, []int{1, 2, 3}, batch)
require.GreaterOrEqual(t, time.Since(start), 30*time.Millisecond) // Waited out the linger
}
func TestLingerQueue_MaxBatchFlush(t *testing.T) {
// Hitting the count cap flushes early, before the linger expires
q := util.NewLingerQueue[int](16, 5, 0, nil, time.Minute)
defer q.Close()
for i := 0; i < 12; i++ {
require.True(t, q.TryEnqueue(i))
}
require.Len(t, <-q.Dequeue(), 5)
require.Len(t, <-q.Dequeue(), 5)
q.Close() // Flushes the remainder
require.Len(t, <-q.Dequeue(), 2)
}
func TestLingerQueue_SizeCapFlush(t *testing.T) {
// Hitting the byte cap flushes early, before count cap or linger
q := util.NewLingerQueue(16, 100, 10, func(s string) int { return len(s) }, time.Minute)
defer q.Close()
require.True(t, q.TryEnqueue("aaaa"))
require.True(t, q.TryEnqueue("bbbb"))
require.True(t, q.TryEnqueue("cccc")) // 12 bytes >= 10 -> flush
batch := <-q.Dequeue()
require.Equal(t, []string{"aaaa", "bbbb", "cccc"}, batch)
}
func TestLingerQueue_TryEnqueueFull(t *testing.T) {
// A full queue drops (returns false) instead of blocking the producer
q := util.NewLingerQueue[int](1, 1, 0, nil, 0)
defer q.Close()
require.True(t, q.TryEnqueue(1)) // Taken by the batcher, blocks emitting (no consumer)
waitForCond(t, func() bool { return q.TryEnqueue(2) }) // Fills the buffer once slot frees
require.False(t, q.TryEnqueue(3)) // Buffer full, batcher blocked -> drop
}
func TestLingerQueue_CloseFlushesAndCloses(t *testing.T) {
q := util.NewLingerQueue[int](16, 100, 0, nil, time.Minute)
require.True(t, q.TryEnqueue(1))
require.True(t, q.TryEnqueue(2))
q.Close()
require.Equal(t, []int{1, 2}, <-q.Dequeue()) // Remainder flushed without waiting out the linger
_, ok := <-q.Dequeue()
require.False(t, ok) // Channel closed
require.False(t, q.TryEnqueue(3))
q.Close() // Idempotent
}
func TestLingerQueue_LingerZeroImmediate(t *testing.T) {
q := util.NewLingerQueue[int](16, 100, 0, nil, 0)
defer q.Close()
require.True(t, q.TryEnqueue(1))
select {
case batch := <-q.Dequeue():
require.Contains(t, batch, 1)
case <-time.After(time.Second):
t.Fatal("expected immediate flush with zero linger")
}
}
func waitForCond(t *testing.T, f func() bool) {
t.Helper()
for i := 0; i < 100; i++ {
if f() {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("timed out waiting for condition")
}
func TestLingerQueue_Len(t *testing.T) {
// Len counts everything enqueued and not yet emitted, including the batch being collected
q := util.NewLingerQueue[int](16, 100, 0, nil, time.Hour)
require.Equal(t, 0, q.Len())
require.True(t, q.TryEnqueue(1))
require.True(t, q.TryEnqueue(2))
require.True(t, q.TryEnqueue(3))
require.Equal(t, 3, q.Len())
q.Close()
require.Equal(t, []int{1, 2, 3}, <-q.Dequeue())
_, ok := <-q.Dequeue()
require.False(t, ok)
require.Equal(t, 0, q.Len())
}