mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-11-11 23:20:56 +08:00
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
package server
|
|
|
|
import (
|
|
"time"
|
|
|
|
"heckel.io/ntfy/v2/log"
|
|
"heckel.io/ntfy/v2/metrics"
|
|
"heckel.io/ntfy/v2/model"
|
|
)
|
|
|
|
const (
|
|
tagTail = "tail"
|
|
tailInterval = time.Second // How often the tail reads what was written; bounds how late a missed message arrives
|
|
tailStaleAfter = 30 * time.Second // A tail that has not read for this long cannot stand in for the fan-out (see maybeCloseLocalSubscribers)
|
|
|
|
// unwrittenMarkerMargin is how far before an unwritten marker's delivery here its replay
|
|
// starts: the fan-out linger, and publish times having whole seconds (see sinceForTopic)
|
|
unwrittenMarkerMargin = 2 * time.Second
|
|
)
|
|
|
|
// runTail reads every message written to the cache, by any node, and sends it to the local
|
|
// subscribers that missed it live: its fan-out to this node was lost, or the subscriber was
|
|
// reconnecting while the message was not written yet, so the database read missed it.
|
|
func (s *Server) runTail() {
|
|
for {
|
|
select {
|
|
case <-time.After(tailInterval):
|
|
case <-s.tail.Done():
|
|
return
|
|
}
|
|
messages, err := s.tail.Next()
|
|
if err != nil {
|
|
log.Tag(tagTail).Err(err).Warn("Cannot read written messages")
|
|
continue
|
|
}
|
|
metrics.TailMessagesRead.Add(float64(len(messages)))
|
|
metrics.TailMessagesLagSeconds.Set(tailLag(messages, time.Now()).Seconds())
|
|
s.publishMissed(messages)
|
|
}
|
|
}
|
|
|
|
// publishMissed hands the messages to their topics, in write order per topic
|
|
func (s *Server) publishMissed(messages []*model.Message) {
|
|
byTopic := make(map[string][]*model.Message)
|
|
for _, m := range messages {
|
|
byTopic[m.Topic] = append(byTopic[m.Topic], m)
|
|
}
|
|
for topicID, topicMessages := range byTopic {
|
|
s.mu.RLock()
|
|
t, ok := s.topics[topicID]
|
|
s.mu.RUnlock()
|
|
if ok {
|
|
metrics.TailMessagesMissed.Add(float64(t.PublishMissed(topicMessages)))
|
|
}
|
|
}
|
|
}
|
|
|
|
// tailLag is how old the newest message of a read is; zero if there was none (caught up)
|
|
func tailLag(messages []*model.Message, now time.Time) time.Duration {
|
|
var newest int64
|
|
for _, m := range messages {
|
|
newest = max(newest, m.Time)
|
|
}
|
|
if newest == 0 {
|
|
return 0
|
|
}
|
|
return now.Sub(time.Unix(newest, 0))
|
|
}
|
|
|
|
// sinceForTopic returns the replay marker for a topic when since is not a since=<id> marker the
|
|
// cache knows. Such a marker was not written yet (it is in a write batch, or the node that
|
|
// accepted it is cut off from the database, its write hanging) or has expired. If the message
|
|
// went out on this node, the replay starts a little before then; otherwise it is the topic's
|
|
// whole history, as it always was. Nothing waits for the write: an expired marker looks the
|
|
// same, and waiting would delay every poll of a phone that was offline for longer than the
|
|
// cache duration.
|
|
func sinceForTopic(t *topic, since model.SinceMarker) model.SinceMarker {
|
|
if !since.IsID() {
|
|
return since
|
|
}
|
|
if at, ok := t.DeliveredAt(since.ID()); ok {
|
|
return model.NewSinceTime(at.Add(-unwrittenMarkerMargin).Unix())
|
|
}
|
|
return model.SinceAllMessages
|
|
}
|