mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-11-11 15:10:56 +08:00
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.
106 lines
3.3 KiB
Go
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())
|
|
}
|