|
|
@@ -8,6 +8,7 @@ import (
|
|
|
"net/netip"
|
|
|
"reflect"
|
|
|
"slices"
|
|
|
+ "sync/atomic"
|
|
|
"testing"
|
|
|
"time"
|
|
|
|
|
|
@@ -86,6 +87,7 @@ func TestUpdateRoutes(t *testing.T) {
|
|
|
|
|
|
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
|
|
|
a.updateRoutes(routes)
|
|
|
+ a.Wait(ctx)
|
|
|
|
|
|
slices.SortFunc(rc.Routes(), prefixCompare)
|
|
|
rc.SetRoutes(slices.Compact(rc.Routes()))
|
|
|
@@ -105,6 +107,7 @@ func TestUpdateRoutes(t *testing.T) {
|
|
|
}
|
|
|
|
|
|
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
|
|
|
+ ctx := context.Background()
|
|
|
for _, shouldStore := range []bool{false, true} {
|
|
|
rc := &appctest.RouteCollector{}
|
|
|
var a *AppConnector
|
|
|
@@ -117,6 +120,7 @@ func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
|
|
|
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
|
|
|
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
|
|
|
a.updateRoutes(routes)
|
|
|
+ a.Wait(ctx)
|
|
|
|
|
|
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
|
|
|
t.Fatalf("got %v, want %v", rc.Routes(), routes)
|
|
|
@@ -636,3 +640,57 @@ func TestMetricBucketsAreSorted(t *testing.T) {
|
|
|
t.Errorf("metricStoreRoutesNBuckets must be in order")
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// TestUpdateRoutesDeadlock is a regression test for a deadlock in
|
|
|
+// LocalBackend<->AppConnector interaction. When using real LocalBackend as the
|
|
|
+// routeAdvertiser, calls to Advertise/UnadvertiseRoutes can end up calling
|
|
|
+// back into AppConnector via authReconfig. If everything is called
|
|
|
+// synchronously, this results in a deadlock on AppConnector.mu.
|
|
|
+func TestUpdateRoutesDeadlock(t *testing.T) {
|
|
|
+ ctx := context.Background()
|
|
|
+ rc := &appctest.RouteCollector{}
|
|
|
+ a := NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
|
|
|
+
|
|
|
+ advertiseCalled := new(atomic.Bool)
|
|
|
+ unadvertiseCalled := new(atomic.Bool)
|
|
|
+ rc.AdvertiseCallback = func() {
|
|
|
+ // Call something that requires a.mu to be held.
|
|
|
+ a.DomainRoutes()
|
|
|
+ advertiseCalled.Store(true)
|
|
|
+ }
|
|
|
+ rc.UnadvertiseCallback = func() {
|
|
|
+ // Call something that requires a.mu to be held.
|
|
|
+ a.DomainRoutes()
|
|
|
+ unadvertiseCalled.Store(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ a.updateDomains([]string{"example.com"})
|
|
|
+ a.Wait(ctx)
|
|
|
+
|
|
|
+ // Trigger rc.AdveriseRoute.
|
|
|
+ a.updateRoutes(
|
|
|
+ []netip.Prefix{
|
|
|
+ netip.MustParsePrefix("127.0.0.1/32"),
|
|
|
+ netip.MustParsePrefix("127.0.0.2/32"),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ a.Wait(ctx)
|
|
|
+ // Trigger rc.UnadveriseRoute.
|
|
|
+ a.updateRoutes(
|
|
|
+ []netip.Prefix{
|
|
|
+ netip.MustParsePrefix("127.0.0.1/32"),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ a.Wait(ctx)
|
|
|
+
|
|
|
+ if !advertiseCalled.Load() {
|
|
|
+ t.Error("AdvertiseRoute was not called")
|
|
|
+ }
|
|
|
+ if !unadvertiseCalled.Load() {
|
|
|
+ t.Error("UnadvertiseRoute was not called")
|
|
|
+ }
|
|
|
+
|
|
|
+ if want := []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}; !slices.Equal(slices.Compact(rc.Routes()), want) {
|
|
|
+ t.Fatalf("got %v, want %v", rc.Routes(), want)
|
|
|
+ }
|
|
|
+}
|