mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(agent): reap exited processes and scope process list by chat ID (#22944)
This commit is contained in:
+201
-3
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
// postStart sends a POST /start request and returns the recorder.
|
||||
func postStart(t *testing.T, handler http.Handler, req workspacesdk.StartProcessRequest) *httptest.ResponseRecorder {
|
||||
func postStart(t *testing.T, handler http.Handler, req workspacesdk.StartProcessRequest, headers ...http.Header) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
@@ -38,6 +38,13 @@ func postStart(t *testing.T, handler http.Handler, req workspacesdk.StartProcess
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequestWithContext(ctx, http.MethodPost, "/start", bytes.NewReader(body))
|
||||
for _, h := range headers {
|
||||
for k, vals := range h {
|
||||
for _, v := range vals {
|
||||
r.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
@@ -140,10 +147,10 @@ func waitForExit(t *testing.T, handler http.Handler, id string) workspacesdk.Pro
|
||||
|
||||
// startAndGetID is a helper that starts a process and returns
|
||||
// the process ID.
|
||||
func startAndGetID(t *testing.T, handler http.Handler, req workspacesdk.StartProcessRequest) string {
|
||||
func startAndGetID(t *testing.T, handler http.Handler, req workspacesdk.StartProcessRequest, headers ...http.Header) string {
|
||||
t.Helper()
|
||||
|
||||
w := postStart(t, handler, req)
|
||||
w := postStart(t, handler, req, headers...)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp workspacesdk.StartProcessResponse
|
||||
@@ -333,6 +340,180 @@ func TestListProcesses(t *testing.T) {
|
||||
require.Empty(t, resp.Processes)
|
||||
})
|
||||
|
||||
t.Run("FilterByChatID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newTestAPI(t)
|
||||
|
||||
chatA := uuid.New().String()
|
||||
chatB := uuid.New().String()
|
||||
headersA := http.Header{workspacesdk.CoderChatIDHeader: {chatA}}
|
||||
headersB := http.Header{workspacesdk.CoderChatIDHeader: {chatB}}
|
||||
|
||||
// Start processes with different chat IDs.
|
||||
id1 := startAndGetID(t, handler, workspacesdk.StartProcessRequest{
|
||||
Command: "echo chat-a",
|
||||
}, headersA)
|
||||
waitForExit(t, handler, id1)
|
||||
|
||||
id2 := startAndGetID(t, handler, workspacesdk.StartProcessRequest{
|
||||
Command: "echo chat-b",
|
||||
}, headersB)
|
||||
waitForExit(t, handler, id2)
|
||||
|
||||
id3 := startAndGetID(t, handler, workspacesdk.StartProcessRequest{
|
||||
Command: "echo chat-a-2",
|
||||
}, headersA)
|
||||
waitForExit(t, handler, id3)
|
||||
|
||||
// List with chat A header should return 2 processes.
|
||||
w := getListWithChatHeader(t, handler, chatA)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp workspacesdk.ListProcessesResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Processes, 2)
|
||||
|
||||
ids := make(map[string]bool)
|
||||
for _, p := range resp.Processes {
|
||||
ids[p.ID] = true
|
||||
}
|
||||
require.True(t, ids[id1])
|
||||
require.True(t, ids[id3])
|
||||
|
||||
// List with chat B header should return 1 process.
|
||||
w2 := getListWithChatHeader(t, handler, chatB)
|
||||
require.Equal(t, http.StatusOK, w2.Code)
|
||||
|
||||
var resp2 workspacesdk.ListProcessesResponse
|
||||
err = json.NewDecoder(w2.Body).Decode(&resp2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp2.Processes, 1)
|
||||
require.Equal(t, id2, resp2.Processes[0].ID)
|
||||
|
||||
// List without chat header should return all 3.
|
||||
w3 := getList(t, handler)
|
||||
require.Equal(t, http.StatusOK, w3.Code)
|
||||
|
||||
var resp3 workspacesdk.ListProcessesResponse
|
||||
err = json.NewDecoder(w3.Body).Decode(&resp3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp3.Processes, 3)
|
||||
})
|
||||
|
||||
t.Run("ChatIDFiltering", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newTestAPI(t)
|
||||
chatID := uuid.New().String()
|
||||
headers := http.Header{workspacesdk.CoderChatIDHeader: {chatID}}
|
||||
|
||||
id := startAndGetID(t, handler, workspacesdk.StartProcessRequest{
|
||||
Command: "echo with-chat",
|
||||
}, headers)
|
||||
waitForExit(t, handler, id)
|
||||
|
||||
// Listing with the same chat header should return
|
||||
// the process.
|
||||
w := getListWithChatHeader(t, handler, chatID)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp workspacesdk.ListProcessesResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Processes, 1)
|
||||
require.Equal(t, id, resp.Processes[0].ID)
|
||||
|
||||
// Listing with a different chat header should not
|
||||
// return the process.
|
||||
w2 := getListWithChatHeader(t, handler, uuid.New().String())
|
||||
require.Equal(t, http.StatusOK, w2.Code)
|
||||
|
||||
var resp2 workspacesdk.ListProcessesResponse
|
||||
err = json.NewDecoder(w2.Body).Decode(&resp2)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, resp2.Processes)
|
||||
|
||||
// Listing without a chat header should return the
|
||||
// process (no filtering).
|
||||
w3 := getList(t, handler)
|
||||
require.Equal(t, http.StatusOK, w3.Code)
|
||||
|
||||
var resp3 workspacesdk.ListProcessesResponse
|
||||
err = json.NewDecoder(w3.Body).Decode(&resp3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp3.Processes, 1)
|
||||
})
|
||||
|
||||
t.Run("SortAndLimit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newTestAPI(t)
|
||||
|
||||
// Start 12 short-lived processes so we exceed the
|
||||
// limit of 10.
|
||||
for i := 0; i < 12; i++ {
|
||||
id := startAndGetID(t, handler, workspacesdk.StartProcessRequest{
|
||||
Command: fmt.Sprintf("echo proc-%d", i),
|
||||
})
|
||||
waitForExit(t, handler, id)
|
||||
}
|
||||
|
||||
w := getList(t, handler)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp workspacesdk.ListProcessesResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Processes, 10, "should be capped at 10")
|
||||
|
||||
// All returned processes are exited, so they should
|
||||
// be sorted by StartedAt descending (newest first).
|
||||
for i := 1; i < len(resp.Processes); i++ {
|
||||
require.GreaterOrEqual(t, resp.Processes[i-1].StartedAt, resp.Processes[i].StartedAt,
|
||||
"processes should be sorted by started_at descending")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RunningProcessesSortedFirst", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newTestAPI(t)
|
||||
|
||||
// Start an exited process first.
|
||||
exitedID := startAndGetID(t, handler, workspacesdk.StartProcessRequest{
|
||||
Command: "echo done",
|
||||
})
|
||||
waitForExit(t, handler, exitedID)
|
||||
|
||||
// Start a running process after.
|
||||
runningID := startAndGetID(t, handler, workspacesdk.StartProcessRequest{
|
||||
Command: "sleep 300",
|
||||
Background: true,
|
||||
})
|
||||
|
||||
w := getList(t, handler)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp workspacesdk.ListProcessesResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Processes, 2)
|
||||
|
||||
// Running process should come first regardless of
|
||||
// start order.
|
||||
require.Equal(t, runningID, resp.Processes[0].ID)
|
||||
require.True(t, resp.Processes[0].Running)
|
||||
require.Equal(t, exitedID, resp.Processes[1].ID)
|
||||
require.False(t, resp.Processes[1].Running)
|
||||
|
||||
// Clean up.
|
||||
postSignal(t, handler, runningID, workspacesdk.SignalProcessRequest{
|
||||
Signal: "kill",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("MixedRunningAndExited", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -381,6 +562,23 @@ func TestListProcesses(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// getListWithChatHeader sends a GET /list request with the
|
||||
// Coder-Chat-Id header set and returns the recorder.
|
||||
func getListWithChatHeader(t *testing.T, handler http.Handler, chatID string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequestWithContext(ctx, http.MethodGet, "/list", nil)
|
||||
if chatID != "" {
|
||||
r.Header.Set(workspacesdk.CoderChatIDHeader, chatID)
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestProcessOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user