Retain execution controls in prepared handles
This commit is contained in:
@@ -218,6 +218,8 @@ errors while the operation child is active must not gain the marker.
|
|||||||
|
|
||||||
## Stage 3: Retain Controls Across Split Preparation
|
## Stage 3: Retain Controls Across Split Preparation
|
||||||
|
|
||||||
|
Status: complete.
|
||||||
|
|
||||||
Outcome: prepared execution carries the selected duration and notification,
|
Outcome: prepared execution carries the selected duration and notification,
|
||||||
without keeping a live operation scope while idle.
|
without keeping a live operation scope while idle.
|
||||||
|
|
||||||
|
|||||||
@@ -38,3 +38,44 @@ func TestRunProviderDeadlineIsNotOperationTimeout(t *testing.T) {
|
|||||||
t.Fatalf("provider deadline attribution: %v", err)
|
t.Fatalf("provider deadline attribution: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreparedExecutionRetainsTimeoutWithoutIdleTimer(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
engine := observationEngine(t, observationClientFunc(func(ctx context.Context, _ promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}))
|
||||||
|
req := promptkit.RunRequest{PromptID: "prompt", OperationTimeout: time.Second}
|
||||||
|
handle, err := engine.PrepareExecution(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.OperationTimeout = 0
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
start := time.Now()
|
||||||
|
_, err = engine.RunPrepared(context.Background(), handle)
|
||||||
|
if time.Since(start) != time.Second || !errors.Is(err, promptkit.ErrOperationTimeout) || !errors.Is(err, context.DeadlineExceeded) || !errors.Is(err, promptkit.ErrLLMGenerate) {
|
||||||
|
t.Fatalf("elapsed=%v error=%v", time.Since(start), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparedExecutionRespectsCallerDeadline(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
engine := observationEngine(t, observationClientFunc(func(ctx context.Context, _ promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}))
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
handle, err := engine.PrepareExecution(ctx, promptkit.RunRequest{PromptID: "prompt", OperationTimeout: 5 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
_, err = engine.RunPrepared(ctx, handle)
|
||||||
|
if !errors.Is(err, context.DeadlineExceeded) || errors.Is(err, promptkit.ErrOperationTimeout) {
|
||||||
|
t.Fatalf("caller deadline attribution: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ type PreparedExecution struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type preparedExecutionPayload struct {
|
type preparedExecutionPayload struct {
|
||||||
|
operationTimeout time.Duration
|
||||||
|
onPrepared func(context.Context, *domain.PreparedRun)
|
||||||
prepared *domain.PreparedRun
|
prepared *domain.PreparedRun
|
||||||
capture bool
|
capture bool
|
||||||
parent *domain.Exchange
|
parent *domain.Exchange
|
||||||
@@ -56,6 +58,8 @@ func (r *Runner) PrepareExecution(ctx context.Context, req domain.RunRequest) (*
|
|||||||
state: preparedExecutionReady,
|
state: preparedExecutionReady,
|
||||||
details: operation.run,
|
details: operation.run,
|
||||||
payload: &preparedExecutionPayload{
|
payload: &preparedExecutionPayload{
|
||||||
|
operationTimeout: req.OperationTimeout,
|
||||||
|
onPrepared: req.OnPrepared,
|
||||||
prepared: operation.run,
|
prepared: operation.run,
|
||||||
capture: operation.capture,
|
capture: operation.capture,
|
||||||
parent: operation.parent,
|
parent: operation.parent,
|
||||||
@@ -100,12 +104,16 @@ func (p *PreparedExecution) Discard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunPrepared claims and executes one prepared execution owned by this runner.
|
// RunPrepared claims and executes one prepared execution owned by this runner.
|
||||||
func (r *Runner) RunPrepared(ctx context.Context, prepared *PreparedExecution) (*domain.RunResult, error) {
|
func (r *Runner) RunPrepared(ctx context.Context, prepared *PreparedExecution) (result *domain.RunResult, err error) {
|
||||||
payload, err := prepared.claim(r)
|
payload, err := prepared.claim(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer payload.clear()
|
defer payload.clear()
|
||||||
|
scope := NewOperationScope(ctx, payload.operationTimeout)
|
||||||
|
defer scope.Close()
|
||||||
|
defer func() { err = scope.Finalize(err) }()
|
||||||
|
ctx = scope.Context
|
||||||
|
|
||||||
var recorder *observation.Recorder
|
var recorder *observation.Recorder
|
||||||
var runID string
|
var runID string
|
||||||
@@ -185,6 +193,8 @@ func (p *preparedExecutionPayload) clear() {
|
|||||||
p.directKey = ""
|
p.directKey = ""
|
||||||
p.transientRetries = 0
|
p.transientRetries = 0
|
||||||
p.observer = nil
|
p.observer = nil
|
||||||
|
p.operationTimeout = 0
|
||||||
|
p.onPrepared = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type noOpPreparedValidation struct {
|
type noOpPreparedValidation struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user