Gate runner dispatches on cancellation
This commit is contained in:
@@ -119,6 +119,96 @@ func (a *countingInputAdapter) Parse(ctx context.Context, request contracts.Pars
|
||||
return a.InputAdapter.Parse(ctx, request)
|
||||
}
|
||||
|
||||
type cancelingDebugRecorder struct {
|
||||
cancel context.CancelFunc
|
||||
onName string
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (r *cancelingDebugRecorder) Enabled() bool { return true }
|
||||
|
||||
func (r *cancelingDebugRecorder) WriteJSON(name string, _ any) error {
|
||||
if name == r.onName {
|
||||
r.once.Do(r.cancel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*cancelingDebugRecorder) WriteBytes(string, []byte) error { return nil }
|
||||
|
||||
type cancelingOutputEncoder struct {
|
||||
contracts.OutputEncoder
|
||||
cancel context.CancelFunc
|
||||
calls atomic.Int32
|
||||
}
|
||||
|
||||
func (e *cancelingOutputEncoder) Encode(context.Context, contracts.OutputRequest) (contracts.OutputResult, error) {
|
||||
e.calls.Add(1)
|
||||
e.cancel()
|
||||
return contracts.OutputResult{
|
||||
Files: []contracts.OutputFile{{Name: "result.txt", ContentType: "text/plain", Bytes: []byte("result")}},
|
||||
Warnings: []contracts.Warning{{ReasonCode: "returned-after-cancel"}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestRunnerSkipsInputAdapterWhenContextIsAlreadyCanceled(t *testing.T) {
|
||||
prepared := preparedConcurrentPipeline(t, 1)
|
||||
adapter := &countingInputAdapter{InputAdapter: prepared.input}
|
||||
prepared.input = adapter
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, err := New().Run(ctx, RunInput{Prepared: prepared, RawInput: []byte("input")})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Run() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if got := adapter.calls.Load(); got != 0 {
|
||||
t.Fatalf("input Parse calls = %d, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerSkipsOutputEncodingAfterDebugCancellation(t *testing.T) {
|
||||
prepared := preparedConcurrentPipeline(t, 1)
|
||||
encoder := &countingOrderedOutput{}
|
||||
prepared.output = encoder
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
_, err := New().Run(ctx, RunInput{
|
||||
Prepared: prepared,
|
||||
RawInput: []byte("input"),
|
||||
Debug: &cancelingDebugRecorder{cancel: cancel, onName: "output/input.json"},
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Run() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if got := encoder.calls.Load(); got != 0 {
|
||||
t.Fatalf("output Encode calls = %d, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerDiscardsOutputReturnedAfterCancellation(t *testing.T) {
|
||||
prepared := preparedConcurrentPipeline(t, 1)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
encoder := &cancelingOutputEncoder{OutputEncoder: prepared.output, cancel: cancel}
|
||||
prepared.output = encoder
|
||||
|
||||
output, err := New().Run(ctx, RunInput{Prepared: prepared, RawInput: []byte("input")})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Run() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if got := encoder.calls.Load(); got != 1 {
|
||||
t.Fatalf("output Encode calls = %d, want one", got)
|
||||
}
|
||||
if len(output.OutputFiles) != 0 {
|
||||
t.Fatalf("output files = %#v, want none", output.OutputFiles)
|
||||
}
|
||||
for _, warning := range output.Warnings {
|
||||
if warning.ReasonCode == "returned-after-cancel" {
|
||||
t.Fatalf("output warnings include encoder warning after cancellation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerFailsGeneratedHandoffBeforeConsumerExtraction(t *testing.T) {
|
||||
prepared := preparedConcurrentPipeline(t, 1)
|
||||
input := &countingInputAdapter{InputAdapter: prepared.input}
|
||||
|
||||
Reference in New Issue
Block a user