mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-07 08:07:42 +00:00
ipn/{ipnlocal,localapi}: ensure watcher is installed before /watch-ipn-bus/ responds with 200
This change delays the first flush in the /watch-ipn-bus/ handler until after the watcher has been successfully installed on the IPN bus. It does this by adding a new onWatchAdded callback to LocalBackend.WatchNotifications(). Without this, the endpoint returns a 200 almost immediatly, and only then installs a watcher for IPN events. This means there's a small window where events could be missed by clients after calling WatchIPNBus(). Fixes tailscale/corp#8594. Signed-off-by: salman <salman@tailscale.com>
This commit is contained in:
parent
3eb986fe05
commit
eec734a578
@ -1802,13 +1802,17 @@ func (b *LocalBackend) readPoller() {
|
|||||||
//
|
//
|
||||||
// WatchNotifications blocks until ctx is done.
|
// WatchNotifications blocks until ctx is done.
|
||||||
//
|
//
|
||||||
// The provided fn will only be called with non-nil pointers. The caller must
|
// The provided onWatchAdded, if non-nil, will be called once the watcher
|
||||||
// not modify roNotify. If fn returns false, the watch also stops.
|
// is installed.
|
||||||
|
//
|
||||||
|
// The provided fn will be called for each notification. It will only be
|
||||||
|
// called with non-nil pointers. The caller must not modify roNotify. If
|
||||||
|
// fn returns false, the watch also stops.
|
||||||
//
|
//
|
||||||
// Failure to consume many notifications in a row will result in dropped
|
// Failure to consume many notifications in a row will result in dropped
|
||||||
// notifications. There is currently (2022-11-22) no mechanism provided to
|
// notifications. There is currently (2022-11-22) no mechanism provided to
|
||||||
// detect when a message has been dropped.
|
// detect when a message has been dropped.
|
||||||
func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWatchOpt, fn func(roNotify *ipn.Notify) (keepGoing bool)) {
|
func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWatchOpt, onWatchAdded func(), fn func(roNotify *ipn.Notify) (keepGoing bool)) {
|
||||||
ch := make(chan *ipn.Notify, 128)
|
ch := make(chan *ipn.Notify, 128)
|
||||||
|
|
||||||
origFn := fn
|
origFn := fn
|
||||||
@ -1858,6 +1862,10 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
|||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if onWatchAdded != nil {
|
||||||
|
onWatchAdded()
|
||||||
|
}
|
||||||
|
|
||||||
if ini != nil {
|
if ini != nil {
|
||||||
if !fn(ini) {
|
if !fn(ini) {
|
||||||
return
|
return
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -820,3 +821,38 @@ type legacyBackend interface {
|
|||||||
// Verify that LocalBackend still implements the legacyBackend interface
|
// Verify that LocalBackend still implements the legacyBackend interface
|
||||||
// for now, at least until the macOS and iOS clients move off of it.
|
// for now, at least until the macOS and iOS clients move off of it.
|
||||||
var _ legacyBackend = (*LocalBackend)(nil)
|
var _ legacyBackend = (*LocalBackend)(nil)
|
||||||
|
|
||||||
|
func TestWatchNotificationsCallbacks(t *testing.T) {
|
||||||
|
b := new(LocalBackend)
|
||||||
|
n := new(ipn.Notify)
|
||||||
|
b.WatchNotifications(context.Background(), 0, func() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Ensure a watcher has been installed.
|
||||||
|
if len(b.notifyWatchers) != 1 {
|
||||||
|
t.Fatalf("unexpected number of watchers in new LocalBackend, want: 1 got: %v", len(b.notifyWatchers))
|
||||||
|
}
|
||||||
|
// Send a notification. Range over notifyWatchers to get the channel
|
||||||
|
// because WatchNotifications doesn't expose the handle for it.
|
||||||
|
for _, c := range b.notifyWatchers {
|
||||||
|
select {
|
||||||
|
case c <- n:
|
||||||
|
default:
|
||||||
|
t.Fatalf("could not send notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, func(roNotify *ipn.Notify) bool {
|
||||||
|
if roNotify != n {
|
||||||
|
t.Fatalf("unexpected notification received. want: %v got: %v", n, roNotify)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure watchers have been cleaned up.
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if len(b.notifyWatchers) != 0 {
|
||||||
|
t.Fatalf("unexpected number of watchers in new LocalBackend, want: 0 got: %v", len(b.notifyWatchers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -721,7 +721,6 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
f.Flush()
|
|
||||||
|
|
||||||
var mask ipn.NotifyWatchOpt
|
var mask ipn.NotifyWatchOpt
|
||||||
if s := r.FormValue("mask"); s != "" {
|
if s := r.FormValue("mask"); s != "" {
|
||||||
@ -733,7 +732,7 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
|
|||||||
mask = ipn.NotifyWatchOpt(v)
|
mask = ipn.NotifyWatchOpt(v)
|
||||||
}
|
}
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
h.b.WatchNotifications(ctx, mask, func(roNotify *ipn.Notify) (keepGoing bool) {
|
h.b.WatchNotifications(ctx, mask, f.Flush, func(roNotify *ipn.Notify) (keepGoing bool) {
|
||||||
js, err := json.Marshal(roNotify)
|
js, err := json.Marshal(roNotify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logf("json.Marshal: %v", err)
|
h.logf("json.Marshal: %v", err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user