mirror of https://github.com/tailscale/tailscale/
ipn/ipnlocal: signal nodeBackend readiness and shutdown
We update LocalBackend to shut down the current nodeBackend when switching to a different node, and to mark the new node's nodeBackend as ready when the switch completes. Updates tailscale/corp#28014 Updates tailscale/corp#29543 Updates #12614 Signed-off-by: Nick Khyl <nickk@tailscale.com>pull/16271/head
parent
fe391d5694
commit
733bfaeffe
@ -0,0 +1,121 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNodeBackendReadiness(t *testing.T) {
|
||||
nb := newNodeBackend(t.Context())
|
||||
|
||||
// The node backend is not ready until [nodeBackend.ready] is called,
|
||||
// and [nodeBackend.Wait] should fail with [context.DeadlineExceeded].
|
||||
ctx, cancelCtx := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancelCtx()
|
||||
if err := nb.Wait(ctx); err != ctx.Err() {
|
||||
t.Fatalf("Wait: got %v; want %v", err, ctx.Err())
|
||||
}
|
||||
|
||||
// Start a goroutine to wait for the node backend to become ready.
|
||||
waitDone := make(chan struct{})
|
||||
go func() {
|
||||
if err := nb.Wait(context.Background()); err != nil {
|
||||
t.Errorf("Wait: got %v; want nil", err)
|
||||
}
|
||||
close(waitDone)
|
||||
}()
|
||||
|
||||
// Call [nodeBackend.ready] to indicate that the node backend is now ready.
|
||||
go nb.ready()
|
||||
|
||||
// Once the backend is called, [nodeBackend.Wait] should return immediately without error.
|
||||
if err := nb.Wait(context.Background()); err != nil {
|
||||
t.Fatalf("Wait: got %v; want nil", err)
|
||||
}
|
||||
// And any pending waiters should also be unblocked.
|
||||
<-waitDone
|
||||
}
|
||||
|
||||
func TestNodeBackendShutdown(t *testing.T) {
|
||||
nb := newNodeBackend(t.Context())
|
||||
|
||||
shutdownCause := errors.New("test shutdown")
|
||||
|
||||
// Start a goroutine to wait for the node backend to become ready.
|
||||
// This test expects it to block until the node backend shuts down
|
||||
// and then return the specified shutdown cause.
|
||||
waitDone := make(chan struct{})
|
||||
go func() {
|
||||
if err := nb.Wait(context.Background()); err != shutdownCause {
|
||||
t.Errorf("Wait: got %v; want %v", err, shutdownCause)
|
||||
}
|
||||
close(waitDone)
|
||||
}()
|
||||
|
||||
// Call [nodeBackend.shutdown] to indicate that the node backend is shutting down.
|
||||
nb.shutdown(shutdownCause)
|
||||
|
||||
// Calling it again is fine, but should not change the shutdown cause.
|
||||
nb.shutdown(errors.New("test shutdown again"))
|
||||
|
||||
// After shutdown, [nodeBackend.Wait] should return with the specified shutdown cause.
|
||||
if err := nb.Wait(context.Background()); err != shutdownCause {
|
||||
t.Fatalf("Wait: got %v; want %v", err, shutdownCause)
|
||||
}
|
||||
// The context associated with the node backend should also be cancelled
|
||||
// and its cancellation cause should match the shutdown cause.
|
||||
if err := nb.Context().Err(); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Context.Err: got %v; want %v", err, context.Canceled)
|
||||
}
|
||||
if cause := context.Cause(nb.Context()); cause != shutdownCause {
|
||||
t.Fatalf("Cause: got %v; want %v", cause, shutdownCause)
|
||||
}
|
||||
// And any pending waiters should also be unblocked.
|
||||
<-waitDone
|
||||
}
|
||||
|
||||
func TestNodeBackendReadyAfterShutdown(t *testing.T) {
|
||||
nb := newNodeBackend(t.Context())
|
||||
|
||||
shutdownCause := errors.New("test shutdown")
|
||||
nb.shutdown(shutdownCause)
|
||||
nb.ready() // Calling ready after shutdown is a no-op, but should not panic, etc.
|
||||
if err := nb.Wait(context.Background()); err != shutdownCause {
|
||||
t.Fatalf("Wait: got %v; want %v", err, shutdownCause)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeBackendParentContextCancellation(t *testing.T) {
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
nb := newNodeBackend(ctx)
|
||||
|
||||
cancelCtx()
|
||||
|
||||
// Cancelling the parent context should cause [nodeBackend.Wait]
|
||||
// to return with [context.Canceled].
|
||||
if err := nb.Wait(context.Background()); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Wait: got %v; want %v", err, context.Canceled)
|
||||
}
|
||||
|
||||
// And the node backend's context should also be cancelled.
|
||||
if err := nb.Context().Err(); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Context.Err: got %v; want %v", err, context.Canceled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeBackendConcurrentReadyAndShutdown(t *testing.T) {
|
||||
nb := newNodeBackend(t.Context())
|
||||
|
||||
// Calling [nodeBackend.ready] and [nodeBackend.shutdown] concurrently
|
||||
// should not cause issues, and [nodeBackend.Wait] should unblock,
|
||||
// but the result of [nodeBackend.Wait] is intentionally undefined.
|
||||
go nb.ready()
|
||||
go nb.shutdown(errors.New("test shutdown"))
|
||||
|
||||
nb.Wait(context.Background())
|
||||
}
|
||||
Loading…
Reference in New Issue