You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add WithAgentInactivityTimeout option for terminating stalled a… (#315)
Adds a new `RequestHandlerOption` that bounds how long the server will
wait for the next event from an agent's producer before terminating its
execution. The timer is reset whenever the agent writes an event to the
event pipe, so steady producers are unaffected.
Approach was discussed and confirmed on the issue: #78 (comment).
## Implementation
- New `ErrAgentInactivityTimeout` sentinel in `internal/taskexec`,
re-exported from `a2asrv` so callers can detect the condition with
`errors.Is`.
- New `activityTrackingWriter` wraps the producer's `eventpipe.Writer`
and signals on every successful `Write`. Wrapping is a no-op when the
option is not configured, so existing users pay no cost.
- New watcher goroutine inside `runProducerConsumer`, started only when
the inactivity timeout is positive. On firing, it returns
`ErrAgentInactivityTimeout` from the errgroup which becomes the
cancellation cause on the producer and consumer contexts via
`context.Cause`, exactly like the existing
`TestRunProducerConsumer_CausePropagation` pattern.
- Plumbed through `LocalManagerConfig.AgentInactivityTimeout` and
`DistributedManagerConfig.AgentInactivityTimeout` for cluster-mode
parity. Heartbeats from `workqueue.Heartbeater` remain independent.
## Usage
```go
handler := a2asrv.NewHandler(
myExecutor,
a2asrv.WithAgentInactivityTimeout(30 * time.Second),
)
```
When the agent stops emitting events for the configured duration, the
executor's context is canceled and the task is finalized via the
existing failure path. Callers can detect the cause:
```go
if errors.Is(err, a2asrv.ErrAgentInactivityTimeout) {
// handle inactivity termination
}
```
## Defaults and compatibility
- Default `0` disables the watcher, preserving prior behavior. Existing
users see no change.
- Applies to both `localManager` and the cluster-mode `workQueueHandler`
for parity.
- `workqueue.Heartbeater` heartbeats are independent — they continue to
fire on their own timer, unaffected by the inactivity watcher.
## Tests
- **7 unit tests** in `execution_handler_test.go`:
- Producer stall → `ErrAgentInactivityTimeout` returned from
`runProducerConsumer` and surfaced as `context.Cause` on the producer
ctx
- Activity signals reset the timer (producer emits at half the timeout
interval, watcher never fires)
- Zero timeout does not start the watcher goroutine
- `activityTrackingWriter`: nil tracker is a no-op, successful write
signals, failed write does not signal, non-blocking when signal channel
is full
- **1 end-to-end test** in `manager_test.go`
(`TestManager_AgentInactivityTimeout`) that constructs a `localManager`
with the option configured, runs a stalled `AgentExecutor`, and asserts
the executor sees `ErrAgentInactivityTimeout` via `context.Cause`.
All existing tests in `internal/taskexec/...` and `a2asrv/...` continue
to pass. `go vet ./...` clean. Full repo `go test ./...` green.
Fixes#78
---------
Co-authored-by: Yaroslav Shevchuk <yarolegovich@gmail.com>
0 commit comments