package syncapi import ( "context" "sync" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/roomserver" ) // Notifier wakes the long-polling syncs that something concerns. // // It is the other half of the roomserver's emit stage: the input pipeline // is the only thing that knows an event has landed, and a client blocked in // /sync for thirty seconds has to hear about it in milliseconds rather than // when its timeout expires. Polling the database instead would mean every // connected client querying every second, which is the cost this avoids. // // Waking is by room and by user rather than globally. A sync registers the // rooms it just read plus its own user ID; an event wakes the listeners of // its room, and a membership event also wakes the listener of the user it // names — which is how an invite reaches a client that is not in the room // yet and so is not listening for it. An event in a room nobody is polling // wakes nobody. // // A wake-up is only ever a hint. The listener re-reads the streams from the // database and works out for itself what changed, so a spurious wake-up // costs one query and a missed one costs at most the sync timeout. That is // what makes it safe for this to hold no state about what each client has // seen. type Notifier struct { mu sync.Mutex rooms map[string]map[*Listener]struct{} users map[string]map[*Listener]struct{} } // NewNotifier returns a notifier with no listeners. func NewNotifier() *Notifier { return &Notifier{ rooms: map[string]map[*Listener]struct{}{}, users: map[string]map[*Listener]struct{}{}, } } // Listener is one blocked sync's wake-up channel. type Listener struct { notifier *Notifier wake chan struct{} userID string rooms []string } // Listen registers interest in a user and a set of rooms. // // The caller must Close the listener. Registering before reading the // streams is what closes the race: an event that lands between the // registration and the read leaves the channel already signalled, so the // following Wait returns at once and the sync reads again rather than // blocking on a change it just missed. func (n *Notifier) Listen(userID string, roomIDs []string) *Listener { l := &Listener{ notifier: n, // One slot, so a wake-up is never lost and never queued: the // listener re-reads everything anyway, so two wake-ups and one are // the same instruction. wake: make(chan struct{}, 1), userID: userID, rooms: roomIDs, } n.mu.Lock() defer n.mu.Unlock() if userID != "" { add(n.users, userID, l) } for _, roomID := range roomIDs { add(n.rooms, roomID, l) } return l } // Wait returns the channel a wake-up arrives on. func (l *Listener) Wait() <-chan struct{} { return l.wake } // Close deregisters the listener. It is safe to call more than once. func (l *Listener) Close() { if l == nil || l.notifier == nil { return } n := l.notifier l.notifier = nil n.mu.Lock() defer n.mu.Unlock() if l.userID != "" { remove(n.users, l.userID, l) } for _, roomID := range l.rooms { remove(n.rooms, roomID, l) } } // EventPersisted implements roomserver.Notifier: it is called once per // event the input pipeline has committed, in the order they were committed. // // It must not block. The call happens with the room's write lock still // held, so anything slow here would serialise the room's writers behind the // slowest reader of it — which is why waking is a non-blocking send onto a // one-slot channel and nothing else. func (n *Notifier) EventPersisted(_ context.Context, notification roomserver.Notification) { n.NotifyRoom(notification.RoomID) // A membership event also concerns the user it names, who for an // invite is not in the room and so is not listening for it. e := notification.Event if e == nil || e.Type() != event.TypeMember { return } if stateKey, ok := e.StateKey(); ok && stateKey != "" { n.NotifyUser(stateKey) } } // NotifyRoom wakes the listeners of one room. It is what the ephemeral // streams — typing and receipts — use, since those are about a room but are // not events. func (n *Notifier) NotifyRoom(roomID string) { n.mu.Lock() defer n.mu.Unlock() wakeAll(n.rooms[roomID]) } // NotifyUser wakes the listeners of one user: their to-device messages, // their account data and the invites addressed to them. func (n *Notifier) NotifyUser(userID string) { n.mu.Lock() defer n.mu.Unlock() wakeAll(n.users[userID]) } // NotifyUsers wakes several users at once, under one lock. func (n *Notifier) NotifyUsers(userIDs []string) { n.mu.Lock() defer n.mu.Unlock() for _, userID := range userIDs { wakeAll(n.users[userID]) } } // Listeners reports how many syncs are currently blocked, for metrics and // for the tests that would otherwise have to guess. func (n *Notifier) Listeners() int { n.mu.Lock() defer n.mu.Unlock() seen := map[*Listener]struct{}{} for _, set := range n.rooms { for l := range set { seen[l] = struct{}{} } } for _, set := range n.users { for l := range set { seen[l] = struct{}{} } } return len(seen) } // wakeAll signals every listener in a set without blocking on any of them. func wakeAll(set map[*Listener]struct{}) { for l := range set { select { case l.wake <- struct{}{}: default: // Already signalled and not yet read: the listener will see // this change when it acts on the signal it has. } } } func add(index map[string]map[*Listener]struct{}, key string, l *Listener) { set, ok := index[key] if !ok { set = map[*Listener]struct{}{} index[key] = set } set[l] = struct{}{} } func remove(index map[string]map[*Listener]struct{}, key string, l *Listener) { set, ok := index[key] if !ok { return } delete(set, l) if len(set) == 0 { // An empty set left behind would make the index grow with every // room any client has ever polled. delete(index, key) } } // Compile-time proof that the notifier is what the roomserver's emit stage // expects. var _ roomserver.Notifier = (*Notifier)(nil)