Commit Graph
14 Commits
Author SHA1 Message Date
binwiederhier b2b1fc1f71 Send reads to the least busy replica, and export each database's health check time
A stalled replica piles up connections in use, so reads go to the replica with the fewest
connections in use instead of round-robin, and to the primary when every replica's pool is
full. ntfy_db_ping_seconds shows how long each database took to answer its health check.
2026-11-03 14:01:34 +00:00
binwiederhier 8d9e876c42 Rename the gauge to ntfy_topics_unifiedpush_total, next to ntfy_topics_total 2026-11-01 14:04:42 +00:00
binwiederhier 68f0ea1279 Rename the gauge to ntfy_unifiedpush_topics_total 2026-11-01 14:04:20 +00:00
binwiederhier 18cdc09698 List ntfy_topic_states_total in the registered metric names 2026-11-01 14:02:48 +00:00
binwiederhier 9ecbf4dc16 Export ntfy_db_up and health-check the primary like the replicas
A node cut off from its database only showed in the logs. The health check loop now always
runs and pings the primary too; ntfy_db_up (1/0, per pool, next to ntfy_db_pool_*) is the
result of the last check, so an alert can fire on ntfy_db_up{role="primary"} == 0.
2026-10-31 01:31:13 +00:00
binwiederhier b49e2c319a Pool roles as constants 2026-10-22 01:33:48 +00:00
binwiederhier 97ec662d5d One field per line in struct literals 2026-10-22 01:29:44 +00:00
binwiederhier dbeeaa72c1 Export PostgreSQL connection pool metrics
ntfy_db_pool_connections_{max,open,in_use,idle}, ntfy_db_pool_waits_total and
ntfy_db_pool_wait_seconds_total, one series per pool (role primary or
replica, host). Read from database/sql's pool stats at scrape time, so a
burst that exhausts a pool between two scrapes still shows up as waits.

Needed to size pool_max_conns for several active cluster nodes: the
managed database allows 97 connections, and the open count alone says
nothing, since the pool rotates through all its connections.
2026-10-22 01:14:41 +00:00
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
binwiederhier 9320fbb113 Rename the tail lag metric to ntfy_tail_messages_lag_seconds 2026-10-12 20:35:22 +00:00
binwiederhier 57e67d9372 Let the tail catch up on a backlog, and report how far behind it is
A read takes up to 10 pages of 1,000 rows until it is caught up, so a
burst of up to 10,000 messages a second no longer leaves it behind. Pages
read before a failed one are returned; the failed one is retried.

ntfy_tail_lag_seconds is the age of the newest message of the last read:
a value well above the write delay means the tail is falling behind.
2026-10-12 20:29:58 +00:00
binwiederhier f9a87cc4da Send subscribers what they missed by tailing the message cache
Every node reads the rows written to the message cache once a second, in
write order (by row id), and sends each message to the local subscribers
that did not get it live: all of them if its fan-out to this node was
lost, or those that subscribed after it went out (they were reconnecting,
and their database read ran before the message was written).

Row ids are handed out on insert but become visible on commit, so a row
id the tail had to skip is re-checked until its row shows up or a minute
passes. In a cluster, since=<id> is read as a time a few seconds before
that message, since nodes write their batches independently.
2026-10-12 15:21:09 +00:00
binwiederhier 30f4bca85d Implement the peer mesh behind the cluster interface
The cluster package now does the thing the seam described. Nothing constructs
it: the server still holds no cluster, and cmd still refuses to start with the
options set, so this is inert like the pieces before it.

A publish marshals once and drops the line into one bounded queue per peer. A
worker batches what it finds, up to the batch linger and flushed early at 100
messages or 256KB, and POSTs NDJSON to that peer's /v1/cluster/message. Fire
and forget: a wedged peer backs up only its own queue, and a full queue drops
rather than blocking the caller. Inbound batches are authenticated by shared
secret, carry an origin header for loop prevention, and are decoded line by
line into the server's delivery callback.

The heartbeat loop refreshes this node's registry row, reads the live set back
and reconciles the per-peer queues, so a departing peer's queue is retired and
a new one is reachable on the next publish. Members reports this node plus its
peers, with peer health being the freshness of this node's own registry view
(registry.Fresh, added here), because an isolated node answering from a frozen
view must not be mistaken for current membership. A node that lost the
database while a peer is still healthy calls back to close its subscribers, and
keeps them when no peer is healthy, since that is an outage rather than
isolation.

Delivery gaps are not here: a failed batch is logged and counted. They come
with the recovery mechanism that uses them.
2026-10-07 19:59:10 +00:00
binwiederhier f8d2fcd7a6 Move metrics to metrics/ pacakge 2026-07-17 22:08:21 +02:00