Skip to content

Commit 5013974

Browse files
internal/grpcsync: add ScheduleAndWait to CallbackSerializer (#9162)
Adds `ScheduleAndWait` to `CallbackSerializer`. It schedules a callback and blocks until the callback has run. If the serializer was already closed before the callback could be scheduled, it returns `ErrSerializerClosed`, so callers can tell whether the callback actually ran. This replaces the common pattern of calling `ScheduleOr` with a `done` channel just to wait for a callback to finish. Implements the design discussed in #8501. Fixes #8501 RELEASE NOTES: none
1 parent bd58bc0 commit 5013974

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

internal/grpcsync/callback_serializer.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ package grpcsync
2020

2121
import (
2222
"context"
23+
"errors"
2324

2425
"google.golang.org/grpc/internal/buffer"
2526
)
2627

28+
// ErrSerializerClosed is returned by ScheduleAndWait if the CallbackSerializer
29+
// was closed before the callback could be scheduled.
30+
var ErrSerializerClosed = errors.New("callback serializer is closed")
31+
2732
// CallbackSerializer provides a mechanism to schedule callbacks in a
2833
// synchronized manner. It provides a FIFO guarantee on the order of execution
2934
// of scheduled callbacks. New callbacks can be scheduled by invoking the
@@ -77,6 +82,27 @@ func (cs *CallbackSerializer) ScheduleOr(f func(ctx context.Context), onFailure
7782
}
7883
}
7984

85+
// ScheduleAndWait schedules the provided callback function f to be executed in
86+
// the order it was added and blocks until f has run. If the context passed to
87+
// NewCallbackSerializer was canceled before this method is called, f is not run
88+
// and ScheduleAndWait returns ErrSerializerClosed.
89+
//
90+
// Callbacks are expected to honor the context when performing any blocking
91+
// operations, and should return early when the context is canceled.
92+
func (cs *CallbackSerializer) ScheduleAndWait(f func(ctx context.Context)) error {
93+
done := make(chan struct{})
94+
var err error
95+
cs.ScheduleOr(func(ctx context.Context) {
96+
f(ctx)
97+
close(done)
98+
}, func() {
99+
err = ErrSerializerClosed
100+
close(done)
101+
})
102+
<-done
103+
return err
104+
}
105+
80106
func (cs *CallbackSerializer) run(ctx context.Context) {
81107
defer close(cs.done)
82108

internal/grpcsync/callback_serializer_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,44 @@ func (s) TestCallbackSerializer_Schedule_Close(t *testing.T) {
204204
case <-done:
205205
}
206206
}
207+
208+
// TestCallbackSerializer_ScheduleAndWait verifies that ScheduleAndWait runs the
209+
// provided callback and blocks until it has finished executing, returning a nil
210+
// error.
211+
func (s) TestCallbackSerializer_ScheduleAndWait(t *testing.T) {
212+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
213+
defer cancel()
214+
cs := NewCallbackSerializer(ctx)
215+
216+
executed := false
217+
if err := cs.ScheduleAndWait(func(context.Context) {
218+
executed = true
219+
}); err != nil {
220+
t.Fatalf("ScheduleAndWait() returned error: %v, want nil", err)
221+
}
222+
223+
// The callback must have run by the time ScheduleAndWait returns. No
224+
// synchronization is needed here because ScheduleAndWait is expected to
225+
// block until the callback completes.
226+
if !executed {
227+
t.Fatal("ScheduleAndWait() returned before the callback was executed")
228+
}
229+
}
230+
231+
// TestCallbackSerializer_ScheduleAndWait_Closed verifies that ScheduleAndWait
232+
// does not run the callback and returns ErrSerializerClosed when the serializer
233+
// has already been closed.
234+
func (s) TestCallbackSerializer_ScheduleAndWait_Closed(t *testing.T) {
235+
serializerCtx, serializerCancel := context.WithCancel(context.Background())
236+
cs := NewCallbackSerializer(serializerCtx)
237+
238+
// Close the serializer and wait for it to finish shutting down.
239+
serializerCancel()
240+
<-cs.Done()
241+
242+
if err := cs.ScheduleAndWait(func(context.Context) {
243+
t.Fatal("Callback executed on a closed serializer")
244+
}); err != ErrSerializerClosed {
245+
t.Fatalf("ScheduleAndWait() returned error: %v, want %v", err, ErrSerializerClosed)
246+
}
247+
}

0 commit comments

Comments
 (0)