| /* |
| * |
| * Copyright 2022 gRPC authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| package grpcsync |
| |
| import ( |
| "context" |
| "sync" |
| "testing" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| ) |
| |
| const ( |
| defaultTestTimeout = 5 * time.Second |
| defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen. |
| ) |
| |
| // TestCallbackSerializer_Schedule_FIFO verifies that callbacks are executed in |
| // the same order in which they were scheduled. |
| func (s) TestCallbackSerializer_Schedule_FIFO(t *testing.T) { |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| cs := NewCallbackSerializer(ctx) |
| defer cancel() |
| |
| // We have two channels, one to record the order of scheduling, and the |
| // other to record the order of execution. We spawn a bunch of goroutines |
| // which record the order of scheduling and call the actual Schedule() |
| // method as well. The callbacks record the order of execution. |
| // |
| // We need to grab a lock to record order of scheduling to guarantee that |
| // the act of recording and the act of calling Schedule() happen atomically. |
| const numCallbacks = 100 |
| var mu sync.Mutex |
| scheduleOrderCh := make(chan int, numCallbacks) |
| executionOrderCh := make(chan int, numCallbacks) |
| for i := 0; i < numCallbacks; i++ { |
| go func(id int) { |
| mu.Lock() |
| defer mu.Unlock() |
| scheduleOrderCh <- id |
| cs.TrySchedule(func(ctx context.Context) { |
| select { |
| case <-ctx.Done(): |
| return |
| case executionOrderCh <- id: |
| } |
| }) |
| }(i) |
| } |
| |
| // Spawn a couple of goroutines to capture the order or scheduling and the |
| // order of execution. |
| scheduleOrder := make([]int, numCallbacks) |
| executionOrder := make([]int, numCallbacks) |
| var wg sync.WaitGroup |
| wg.Add(2) |
| go func() { |
| defer wg.Done() |
| for i := 0; i < numCallbacks; i++ { |
| select { |
| case <-ctx.Done(): |
| return |
| case id := <-scheduleOrderCh: |
| scheduleOrder[i] = id |
| } |
| } |
| }() |
| go func() { |
| defer wg.Done() |
| for i := 0; i < numCallbacks; i++ { |
| select { |
| case <-ctx.Done(): |
| return |
| case id := <-executionOrderCh: |
| executionOrder[i] = id |
| } |
| } |
| }() |
| wg.Wait() |
| |
| if diff := cmp.Diff(executionOrder, scheduleOrder); diff != "" { |
| t.Fatalf("Callbacks are not executed in scheduled order. diff(-want, +got):\n%s", diff) |
| } |
| } |
| |
| // TestCallbackSerializer_Schedule_Concurrent verifies that all concurrently |
| // scheduled callbacks get executed. |
| func (s) TestCallbackSerializer_Schedule_Concurrent(t *testing.T) { |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| cs := NewCallbackSerializer(ctx) |
| defer cancel() |
| |
| // Schedule callbacks concurrently by calling Schedule() from goroutines. |
| // The execution of the callbacks call Done() on the waitgroup, which |
| // eventually unblocks the test and allows it to complete. |
| const numCallbacks = 100 |
| var wg sync.WaitGroup |
| wg.Add(numCallbacks) |
| for i := 0; i < numCallbacks; i++ { |
| go func() { |
| cs.TrySchedule(func(context.Context) { |
| wg.Done() |
| }) |
| }() |
| } |
| |
| // We call Wait() on the waitgroup from a goroutine so that we can select on |
| // the Wait() being unblocked and the overall test deadline expiring. |
| done := make(chan struct{}) |
| go func() { |
| wg.Wait() |
| close(done) |
| }() |
| |
| select { |
| case <-ctx.Done(): |
| t.Fatal("Timeout waiting for all scheduled callbacks to be executed") |
| case <-done: |
| } |
| } |
| |
| // TestCallbackSerializer_Schedule_Close verifies that callbacks in the queue |
| // are not executed once Close() returns. |
| func (s) TestCallbackSerializer_Schedule_Close(t *testing.T) { |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| defer cancel() |
| |
| serializerCtx, serializerCancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| cs := NewCallbackSerializer(serializerCtx) |
| |
| // Schedule a callback which blocks until the context passed to it is |
| // canceled. It also closes a channel to signal that it has started. |
| firstCallbackStartedCh := make(chan struct{}) |
| cs.TrySchedule(func(ctx context.Context) { |
| close(firstCallbackStartedCh) |
| <-ctx.Done() |
| }) |
| |
| // Schedule a bunch of callbacks. These should be executed since they are |
| // scheduled before the serializer is closed. |
| const numCallbacks = 10 |
| callbackCh := make(chan int, numCallbacks) |
| for i := 0; i < numCallbacks; i++ { |
| num := i |
| callback := func(context.Context) { callbackCh <- num } |
| onFailure := func() { t.Fatal("Schedule failed to accept a callback when the serializer is yet to be closed") } |
| cs.ScheduleOr(callback, onFailure) |
| } |
| |
| // Ensure that none of the newer callbacks are executed at this point. |
| select { |
| case <-time.After(defaultTestShortTimeout): |
| case <-callbackCh: |
| t.Fatal("Newer callback executed when older one is still executing") |
| } |
| |
| // Wait for the first callback to start before closing the scheduler. |
| <-firstCallbackStartedCh |
| |
| // Cancel the context which will unblock the first callback. All of the |
| // other callbacks (which have not started executing at this point) should |
| // be executed after this. |
| serializerCancel() |
| |
| // Ensure that the newer callbacks are executed. |
| for i := 0; i < numCallbacks; i++ { |
| select { |
| case <-ctx.Done(): |
| t.Fatal("Timeout when waiting for callback scheduled before close to be executed") |
| case num := <-callbackCh: |
| if num != i { |
| t.Fatalf("Executing callback %d, want %d", num, i) |
| } |
| } |
| } |
| <-cs.Done() |
| |
| // Ensure that a callback cannot be scheduled after the serializer is |
| // closed. |
| done := make(chan struct{}) |
| callback := func(context.Context) { t.Fatal("Scheduled a callback after closing the serializer") } |
| onFailure := func() { close(done) } |
| cs.ScheduleOr(callback, onFailure) |
| select { |
| case <-time.After(defaultTestTimeout): |
| t.Fatal("Successfully scheduled callback after serializer is closed") |
| case <-done: |
| } |
| } |
| |
| // TestCallbackSerializer_ScheduleAndWait verifies that ScheduleAndWait runs the |
| // provided callback and blocks until it has finished executing, returning a nil |
| // error. |
| func (s) TestCallbackSerializer_ScheduleAndWait(t *testing.T) { |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| defer cancel() |
| cs := NewCallbackSerializer(ctx) |
| |
| executed := false |
| if err := cs.ScheduleAndWait(func(context.Context) { |
| executed = true |
| }); err != nil { |
| t.Fatalf("ScheduleAndWait() returned error: %v, want nil", err) |
| } |
| |
| // The callback must have run by the time ScheduleAndWait returns. No |
| // synchronization is needed here because ScheduleAndWait is expected to |
| // block until the callback completes. |
| if !executed { |
| t.Fatal("ScheduleAndWait() returned before the callback was executed") |
| } |
| } |
| |
| // TestCallbackSerializer_ScheduleAndWait_Closed verifies that ScheduleAndWait |
| // does not run the callback and returns ErrSerializerClosed when the serializer |
| // has already been closed. |
| func (s) TestCallbackSerializer_ScheduleAndWait_Closed(t *testing.T) { |
| serializerCtx, serializerCancel := context.WithCancel(context.Background()) |
| cs := NewCallbackSerializer(serializerCtx) |
| |
| // Close the serializer and wait for it to finish shutting down. |
| serializerCancel() |
| <-cs.Done() |
| |
| if err := cs.ScheduleAndWait(func(context.Context) { |
| t.Fatal("Callback executed on a closed serializer") |
| }); err != ErrSerializerClosed { |
| t.Fatalf("ScheduleAndWait() returned error: %v, want %v", err, ErrSerializerClosed) |
| } |
| } |