feat: make process output blocking-capable (#23312)

Replace the 200ms polling loop in chatd's execute and
process_output tools with server-side blocking via sync.Cond
on HeadTailBuffer.

The agent's GET /{id}/output endpoint accepts ?wait=true to
block until the process exits or a 5-minute server cap expires.
The process_output tool blocks by default for 10s (overridable
via wait_timeout), and falls back to a non-blocking snapshot on
timeout. The execute tool's foreground path makes a single
blocking call instead of polling.

Related #23316
This commit is contained in:
Mathias Fredriksson
2026-03-20 14:33:55 +02:00
committed by GitHub
parent c8e58575e0
commit 41e15ae440
10 changed files with 375 additions and 81 deletions
+15 -3
View File
@@ -70,7 +70,7 @@ type AgentConn interface {
ListeningPorts(ctx context.Context) (codersdk.WorkspaceAgentListeningPortsResponse, error)
Netcheck(ctx context.Context) (healthsdk.AgentNetcheckReport, error)
Ping(ctx context.Context) (time.Duration, bool, *ipnstate.PingResult, error)
ProcessOutput(ctx context.Context, id string) (ProcessOutputResponse, error)
ProcessOutput(ctx context.Context, id string, opts *ProcessOutputOptions) (ProcessOutputResponse, error)
PrometheusMetrics(ctx context.Context) ([]byte, error)
ReconnectingPTY(ctx context.Context, id uuid.UUID, height uint16, width uint16, command string, initOpts ...AgentReconnectingPTYInitOption) (net.Conn, error)
DeleteDevcontainer(ctx context.Context, devcontainerID string) error
@@ -715,6 +715,14 @@ type ProcessOutputResponse struct {
ExitCode *int `json:"exit_code,omitempty"`
}
// ProcessOutputOptions configures blocking behavior for
// process output retrieval.
type ProcessOutputOptions struct {
// Wait enables blocking mode. When true, the request
// blocks until the process exits or the context expires.
Wait bool
}
// ProcessTruncation describes how process output was truncated.
type ProcessTruncation struct {
OriginalBytes int `json:"original_bytes"`
@@ -946,10 +954,14 @@ func (c *agentConn) ListProcesses(ctx context.Context) (ListProcessesResponse, e
}
// ProcessOutput returns the output of a tracked process on the agent.
func (c *agentConn) ProcessOutput(ctx context.Context, id string) (ProcessOutputResponse, error) {
func (c *agentConn) ProcessOutput(ctx context.Context, id string, opts *ProcessOutputOptions) (ProcessOutputResponse, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
res, err := c.apiRequest(ctx, http.MethodGet, "/api/v0/processes/"+id+"/output", nil)
path := "/api/v0/processes/" + id + "/output"
if opts != nil && opts.Wait {
path += "?wait=true"
}
res, err := c.apiRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return ProcessOutputResponse{}, xerrors.Errorf("do request: %w", err)
}
@@ -308,18 +308,18 @@ func (mr *MockAgentConnMockRecorder) Ping(ctx any) *gomock.Call {
}
// ProcessOutput mocks base method.
func (m *MockAgentConn) ProcessOutput(ctx context.Context, id string) (workspacesdk.ProcessOutputResponse, error) {
func (m *MockAgentConn) ProcessOutput(ctx context.Context, id string, opts *workspacesdk.ProcessOutputOptions) (workspacesdk.ProcessOutputResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ProcessOutput", ctx, id)
ret := m.ctrl.Call(m, "ProcessOutput", ctx, id, opts)
ret0, _ := ret[0].(workspacesdk.ProcessOutputResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ProcessOutput indicates an expected call of ProcessOutput.
func (mr *MockAgentConnMockRecorder) ProcessOutput(ctx, id any) *gomock.Call {
func (mr *MockAgentConnMockRecorder) ProcessOutput(ctx, id, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessOutput", reflect.TypeOf((*MockAgentConn)(nil).ProcessOutput), ctx, id)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessOutput", reflect.TypeOf((*MockAgentConn)(nil).ProcessOutput), ctx, id, opts)
}
// PrometheusMetrics mocks base method.