knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/leaderelection/context_test.go (about)

     1  /*
     2  Copyright 2020 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package leaderelection
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	fakekube "k8s.io/client-go/kubernetes/fake"
    30  	ktesting "k8s.io/client-go/testing"
    31  
    32  	"knative.dev/pkg/reconciler"
    33  	_ "knative.dev/pkg/system/testing"
    34  )
    35  
    36  func TestWithBuilder(t *testing.T) {
    37  	const buckets = 3
    38  	cc := ComponentConfig{
    39  		Component:     "the-component",
    40  		Buckets:       buckets,
    41  		LeaseDuration: 15 * time.Second,
    42  		RenewDeadline: 10 * time.Second,
    43  		RetryPeriod:   2 * time.Second,
    44  	}
    45  	kc := fakekube.NewSimpleClientset()
    46  	ctx := context.Background()
    47  
    48  	gotNames := make(sets.Set[string], buckets)
    49  	promoted := make(chan string)
    50  	demoted := make(chan struct{})
    51  	laf := &reconciler.LeaderAwareFuncs{
    52  		PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
    53  			promoted <- bkt.Name()
    54  			return nil
    55  		},
    56  		DemoteFunc: func(bkt reconciler.Bucket) {
    57  			demoted <- struct{}{}
    58  		},
    59  	}
    60  	enq := func(reconciler.Bucket, types.NamespacedName) {}
    61  
    62  	created := make(chan struct{})
    63  	kc.PrependReactor("create", "leases",
    64  		func(action ktesting.Action) (bool, runtime.Object, error) {
    65  			created <- struct{}{}
    66  			return false, nil, nil
    67  		},
    68  	)
    69  
    70  	updated := make(chan struct{})
    71  	kc.PrependReactor("update", "leases",
    72  		func(action ktesting.Action) (bool, runtime.Object, error) {
    73  			// Only close updated once.
    74  			select {
    75  			case <-updated:
    76  			default:
    77  				close(updated)
    78  			}
    79  			return false, nil, nil
    80  		},
    81  	)
    82  
    83  	if HasLeaderElection(ctx) {
    84  		t.Error("HasLeaderElection() = true, wanted false")
    85  	}
    86  	if le, err := BuildElector(ctx, laf, "name", enq); err != nil {
    87  		t.Errorf("BuildElector() = %v, wanted an unopposedElector", err)
    88  	} else if _, ok := le.(*unopposedElector); !ok {
    89  		t.Errorf("BuildElector() = %T, wanted an unopposedElector", le)
    90  	}
    91  
    92  	ctx = WithDynamicLeaderElectorBuilder(ctx, kc, cc)
    93  	if !HasLeaderElection(ctx) {
    94  		t.Error("HasLeaderElection() = false, wanted true")
    95  	}
    96  
    97  	le, err := BuildElector(ctx, laf, "name", enq)
    98  	if err != nil {
    99  		t.Fatal("BuildElector() =", err)
   100  	}
   101  
   102  	// We shouldn't see leases until we Run the elector.
   103  	select {
   104  	case s := <-promoted:
   105  		gotNames.Insert(s)
   106  		t.Error("Got promoted, want no actions.")
   107  	case <-demoted:
   108  		t.Error("Got demoted, want no actions.")
   109  	case <-created:
   110  		t.Error("Got created, want no actions.")
   111  	case <-updated:
   112  		t.Error("Got updated, want no actions.")
   113  	default:
   114  	}
   115  
   116  	ctx, cancel := context.WithCancel(context.Background())
   117  	t.Cleanup(cancel)
   118  	go le.Run(ctx)
   119  
   120  	// We expect 3 lease to be created.
   121  	for range buckets {
   122  		select {
   123  		case <-created:
   124  		case <-time.After(1 * time.Second):
   125  			t.Fatal("Timed out waiting for lease creation.")
   126  		}
   127  	}
   128  	// We expect to have been promoted 3 times.
   129  	for range buckets {
   130  		select {
   131  		case s := <-promoted:
   132  			gotNames.Insert(s)
   133  		case <-time.After(time.Second):
   134  			t.Fatal("Timed out waiting for promotion.")
   135  		}
   136  	}
   137  
   138  	// Cancelling the context should case us to give up leadership.
   139  	cancel()
   140  
   141  	select {
   142  	case <-updated:
   143  		// We expect the lease to be updated.
   144  	case <-time.After(time.Second):
   145  		t.Fatal("Timed out waiting for lease update.")
   146  	}
   147  	// We expect to have been demoted 3 times.
   148  	for range buckets {
   149  		select {
   150  		case <-demoted:
   151  		case <-time.After(time.Second):
   152  			t.Fatal("Timed out waiting for demotion.")
   153  		}
   154  	}
   155  
   156  	want := sets.New[string](
   157  		"the-component.name.00-of-03",
   158  		"the-component.name.01-of-03",
   159  		"the-component.name.02-of-03",
   160  	)
   161  	if !gotNames.Equal(want) {
   162  		t.Errorf("BucketSet.BucketList() = %q, want: %q", gotNames, want)
   163  	}
   164  }
   165  
   166  func TestBuilderWithCustomizedLeaseName(t *testing.T) {
   167  	const buckets = 3
   168  	cc := ComponentConfig{
   169  		Component:     "the-component",
   170  		Buckets:       buckets,
   171  		LeaseDuration: 15 * time.Second,
   172  		RenewDeadline: 10 * time.Second,
   173  		RetryPeriod:   2 * time.Second,
   174  		LeaseName: func(i uint32) string {
   175  			return fmt.Sprintf("bucket-%02d", i)
   176  		},
   177  	}
   178  	kc := fakekube.NewSimpleClientset()
   179  	ctx := context.Background()
   180  
   181  	gotNames := make(sets.Set[string], buckets)
   182  	promoted := make(chan string)
   183  	laf := &reconciler.LeaderAwareFuncs{
   184  		PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
   185  			promoted <- bkt.Name()
   186  			return nil
   187  		},
   188  	}
   189  	enq := func(reconciler.Bucket, types.NamespacedName) {}
   190  	ctx = WithDynamicLeaderElectorBuilder(ctx, kc, cc)
   191  	le, err := BuildElector(ctx, laf, "name", enq)
   192  	if err != nil {
   193  		t.Fatal("BuildElector() =", err)
   194  	}
   195  
   196  	ctx, cancel := context.WithCancel(context.Background())
   197  	t.Cleanup(cancel)
   198  	go le.Run(ctx)
   199  
   200  	// We expect to have been promoted 3 times.
   201  	for range buckets {
   202  		select {
   203  		case s := <-promoted:
   204  			gotNames.Insert(s)
   205  		case <-time.After(time.Second):
   206  			t.Fatal("Timed out waiting for promotion.")
   207  		}
   208  	}
   209  
   210  	want := sets.New[string](
   211  		"bucket-00",
   212  		"bucket-01",
   213  		"bucket-02",
   214  	)
   215  	if !gotNames.Equal(want) {
   216  		t.Errorf("BucketSet.BucketList() = %q, want: %q", gotNames, want)
   217  	}
   218  }
   219  
   220  func TestNewStatefulSetBucketAndSet(t *testing.T) {
   221  	wantNames := []string{
   222  		"http://as-0.autoscaler.knative-testing.svc.cluster.local:80",
   223  		"http://as-1.autoscaler.knative-testing.svc.cluster.local:80",
   224  		"http://as-2.autoscaler.knative-testing.svc.cluster.local:80",
   225  	}
   226  
   227  	t.Setenv(controllerOrdinalEnv, "as-2")
   228  	t.Setenv(serviceNameEnv, "autoscaler")
   229  
   230  	_, _, err := NewStatefulSetBucketAndSet(2)
   231  	if err == nil {
   232  		// Ordinal 2 should be range [0, 2)
   233  		t.Fatal("Expected error from NewStatefulSetBucketAndSet but got nil")
   234  	}
   235  
   236  	bkt, bs, err := NewStatefulSetBucketAndSet(3)
   237  	if err != nil {
   238  		// Ordinal 2 should be range [0, 2)
   239  		t.Fatal("NewStatefulSetBucketAndSet() = ", err)
   240  	}
   241  
   242  	if got, want := bkt.Name(), wantNames[2]; got != want {
   243  		t.Errorf("Bucket.Name() = %s, want = %s", got, want)
   244  	}
   245  
   246  	gotNames := bs.BucketList()
   247  	if !cmp.Equal(gotNames, wantNames) {
   248  		t.Errorf("BucketSet.BucketList() = %q, want: %q", gotNames, wantNames)
   249  	}
   250  }
   251  
   252  func TestWithStatefulSetBuilder(t *testing.T) {
   253  	cc := ComponentConfig{
   254  		Component: "the-component",
   255  		Buckets:   3,
   256  	}
   257  	const podDNS = "http://as-2.autoscaler.knative-testing.svc.cluster.local:80"
   258  	ctx := context.Background()
   259  
   260  	promoted := make(chan struct{})
   261  	laf := &reconciler.LeaderAwareFuncs{
   262  		PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
   263  			close(promoted)
   264  			return nil
   265  		},
   266  	}
   267  	enq := func(reconciler.Bucket, types.NamespacedName) {}
   268  
   269  	t.Setenv(controllerOrdinalEnv, "as-2")
   270  	t.Setenv(serviceNameEnv, "autoscaler")
   271  
   272  	ctx = WithDynamicLeaderElectorBuilder(ctx, nil, cc)
   273  	if !HasLeaderElection(ctx) {
   274  		t.Error("HasLeaderElection() = false, wanted true")
   275  	}
   276  
   277  	b := ctx.Value(builderKey{})
   278  	ssb, ok := b.(*statefulSetBuilder)
   279  	if !ok || ssb == nil {
   280  		t.Fatal("StatefulSetBuilder not found on context")
   281  	}
   282  
   283  	le, err := BuildElector(ctx, laf, "name", enq)
   284  	if err != nil {
   285  		t.Fatal("BuildElector() =", err)
   286  	}
   287  
   288  	ule, ok := le.(*unopposedElector)
   289  	if !ok {
   290  		t.Fatalf("BuildElector() = %T, wanted an unopposedElector", le)
   291  	}
   292  	if got, want := ule.bkt.Name(), podDNS; got != want {
   293  		t.Errorf("bkt.Name() = %s, wanted %s", got, want)
   294  	}
   295  
   296  	// Shouldn't be promoted until we Run the elector.
   297  	select {
   298  	case <-promoted:
   299  		t.Error("Got promoted, want no actions.")
   300  	default:
   301  	}
   302  
   303  	ctx, cancel := context.WithCancel(context.Background())
   304  	t.Cleanup(cancel)
   305  	go le.Run(ctx)
   306  
   307  	select {
   308  	case <-promoted:
   309  		// We expect to have been promoted.
   310  	case <-time.After(time.Second):
   311  		t.Fatal("Timed out waiting for promotion.")
   312  	}
   313  }
   314  
   315  func TestWithUnopposedElector(t *testing.T) {
   316  	laf := &reconciler.LeaderAwareFuncs{
   317  		PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
   318  			t.Error("Unexpected call to PromoteFunc with unopposedElector")
   319  			return nil
   320  		},
   321  	}
   322  
   323  	ctx := context.Background()
   324  	if HasLeaderElection(ctx) {
   325  		t.Error("HasLeaderElection() = true, wanted false")
   326  	}
   327  
   328  	le, err := BuildElector(ctx, laf, "name", func(reconciler.Bucket, types.NamespacedName) {
   329  		t.Error("Unexpected call to enqueue function.")
   330  	})
   331  	if err != nil {
   332  		t.Fatal("BuildElector() =", err)
   333  	}
   334  
   335  	if _, ok := le.(*unopposedElector); !ok {
   336  		t.Fatalf("BuildElector() = %T, wanted an unopposedElector", le)
   337  	}
   338  
   339  	ctx, cancel := context.WithCancel(context.Background())
   340  	t.Cleanup(cancel)
   341  	go le.Run(ctx)
   342  
   343  	// Wait to see if PromoteFunc is called with nil or our enq function.
   344  	<-time.After(time.Second)
   345  }
   346  
   347  func TestStandardBucketName(t *testing.T) {
   348  	tests := []struct {
   349  		name      string
   350  		ordinal   uint32
   351  		queueName string
   352  		cc        ComponentConfig
   353  		want      string
   354  	}{
   355  		{
   356  			name:      "identity",
   357  			ordinal:   0,
   358  			queueName: "queue-queue",
   359  			cc: ComponentConfig{
   360  				Component: "my-comp",
   361  			},
   362  			want: "my-comp.queue-queue.00-of-00",
   363  		},
   364  		{
   365  			name:      "remapping",
   366  			ordinal:   0,
   367  			queueName: "queue-queue",
   368  			cc: ComponentConfig{
   369  				Component: "my-comp",
   370  				LeaseNamesPrefixMapping: map[string]string{
   371  					"my-comp.queue-queue": "my-comp-2.queue",
   372  				},
   373  			},
   374  			want: "my-comp-2.queue.00-of-00",
   375  		},
   376  	}
   377  	for _, tt := range tests {
   378  		t.Run(tt.name, func(t *testing.T) {
   379  			if got := standardBucketName(tt.ordinal, tt.queueName, tt.cc); got != tt.want {
   380  				t.Errorf("got %v, want %v", got, tt.want)
   381  			}
   382  		})
   383  	}
   384  }
   385  
   386  func TestUnopposedElectorInitialBucket(t *testing.T) {
   387  	u := &unopposedElector{
   388  		bkt: reconciler.UniversalBucket(),
   389  	}
   390  	if diff := cmp.Diff(u.InitialBuckets(), []reconciler.Bucket{reconciler.UniversalBucket()}); diff != "" {
   391  		t.Errorf("unexpected u.InitialBuckets() (-want,+got): %s", diff)
   392  	}
   393  }