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 }