Files
ntfy/server/server_cluster.go
T
binwiederhier 859e6443c7 Count forwarded messages for topics without local subscribers as wasted, and do not keep those topics alive
A cluster message whose topic is known on the receiving node but has no subscriber there was published to nobody and uncounted, and its Keepalive kept the topic from ever expiring: a passive node held every topic the active one published to. It is now counted in ntfy_cluster_messages_wasted_total and not published, so idle topics expire after topicExpungeAfter.
2026-10-31 19:43:29 +00:00

53 lines
2.2 KiB
Go

package server
import (
"time"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/metrics"
"heckel.io/ntfy/v2/model"
)
// deliverFromBus delivers a message received from a peer node (via the cluster) to this
// node's local subscribers. It is the receive-side counterpart to Cluster.ForwardMessage: local
// delivery and all global side effects (Firebase, email, web push, upstream) already ran on the
// origin node, so this only publishes to the local topic, and never re-relays.
//
// A message whose topic has no subscriber here is counted as wasted and not published: publishing
// would keep the idle topic alive (Keepalive), so a node would never expunge a topic the other
// nodes keep publishing to. A subscriber that comes along later gets it from its since= read of
// the cache, or from the tail (which sends what never went out here).
func (s *Server) deliverFromBus(m *model.Message) {
s.mu.RLock()
t, ok := s.topics[m.Topic]
s.mu.RUnlock()
if !ok || t.SubscribersCount() == 0 {
metrics.ClusterMessagesWasted.Inc() // Relayed here needlessly: no subscriber here wanted the message
return
}
v := s.visitor(m.Sender, nil)
if err := t.Publish(v, m); err != nil {
logvm(v, m).Err(err).Warn("Cluster: unable to deliver fan-out message to local subscribers")
}
}
// maybeCloseLocalSubscribers closes every subscriber connection on this node. The cluster calls it
// while this node is isolated (lost its registration, peers healthy): peers no longer forward
// to it, so its subscribers would silently receive nothing; they reconnect to a healthy node.
//
// Not while the tail still reads the database: then the subscribers get every message anyway,
// a little late. Heartbeats time out when the database is slow, not only when it is gone, and
// closing tens of thousands of connections then sends them to nodes that are just as slow, as
// a reconnect storm that keeps the database slow.
func (s *Server) maybeCloseLocalSubscribers() {
if s.tail != nil && time.Since(s.tail.LastRead()) < tailStaleAfter {
log.Tag(tagTail).Info("Keeping local subscribers although the cluster registration is stale: the tail still reads the database")
return
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, t := range s.topics {
t.CancelAllSubscribers()
}
}