Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0f3a1e9

Browse files
authored
fix(agent/agentcontainers): split Init into Init and Start for early API responses (#18640)
Previously in #18635 we delayed the containers API `Init` to avoid producing errors due to Docker and `@devcontainers/cli` not yet being installed by startup scripts. This had an adverse effect on the UX via UI responsiveness as the detection of devcontainers was greatly delayed. This change splits `Init` into `Init` and `Start` so that we can immediately after `Init` start serving known devcontainers (defined in Terraform), improving the UX. Related #18635 Related #18640
1 parent e46d892 commit 0f3a1e9

File tree

3 files changed

+66
-36
lines changed

3 files changed

+66
-36
lines changed

agent/agent.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,11 +1158,26 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11581158
}
11591159
}
11601160

1161-
scripts := manifest.Scripts
1161+
var (
1162+
scripts = manifest.Scripts
1163+
devcontainerScripts map[uuid.UUID]codersdk.WorkspaceAgentScript
1164+
)
11621165
if a.containerAPI != nil {
1166+
// Init the container API with the manifest and client so that
1167+
// we can start accepting requests. The final start of the API
1168+
// happens after the startup scripts have been executed to
1169+
// ensure the presence of required tools. This means we can
1170+
// return existing devcontainers but actual container detection
1171+
// and creation will be deferred.
1172+
a.containerAPI.Init(
1173+
agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName),
1174+
agentcontainers.WithDevcontainers(manifest.Devcontainers, manifest.Scripts),
1175+
agentcontainers.WithSubAgentClient(agentcontainers.NewSubAgentClientFromAPI(a.logger, aAPI)),
1176+
)
1177+
11631178
// Since devcontainer are enabled, remove devcontainer scripts
11641179
// from the main scripts list to avoid showing an error.
1165-
scripts, _ = agentcontainers.ExtractDevcontainerScripts(manifest.Devcontainers, manifest.Scripts)
1180+
scripts, devcontainerScripts = agentcontainers.ExtractDevcontainerScripts(manifest.Devcontainers, scripts)
11661181
}
11671182
err = a.scriptRunner.Init(scripts, aAPI.ScriptCompleted)
11681183
if err != nil {
@@ -1183,13 +1198,10 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11831198
err := a.scriptRunner.Execute(a.gracefulCtx, agentscripts.ExecuteStartScripts)
11841199

11851200
if a.containerAPI != nil {
1186-
a.containerAPI.Init(
1187-
agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName),
1188-
agentcontainers.WithDevcontainers(manifest.Devcontainers, manifest.Scripts),
1189-
agentcontainers.WithSubAgentClient(agentcontainers.NewSubAgentClientFromAPI(a.logger, aAPI)),
1190-
)
1191-
1192-
_, devcontainerScripts := agentcontainers.ExtractDevcontainerScripts(manifest.Devcontainers, manifest.Scripts)
1201+
// Start the container API after the startup scripts have
1202+
// been executed to ensure that the required tools can be
1203+
// installed.
1204+
a.containerAPI.Start()
11931205
for _, dc := range manifest.Devcontainers {
11941206
cErr := a.createDevcontainer(ctx, aAPI, dc, devcontainerScripts[dc.ID])
11951207
err = errors.Join(err, cErr)

agent/agentcontainers/api.go

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ type API struct {
5353
cancel context.CancelFunc
5454
watcherDone chan struct{}
5555
updaterDone chan struct{}
56-
initialUpdateDone chan struct{} // Closed after first update in updaterLoop.
5756
updateTrigger chan chan error // Channel to trigger manual refresh.
5857
updateInterval time.Duration // Interval for periodic container updates.
5958
logger slog.Logger
@@ -73,7 +72,8 @@ type API struct {
7372
workspaceName string
7473
parentAgent string
7574

76-
mu sync.RWMutex
75+
mu sync.RWMutex // Protects the following fields.
76+
initDone chan struct{} // Closed by Init.
7777
closed bool
7878
containers codersdk.WorkspaceAgentListContainersResponse // Output from the last list operation.
7979
containersErr error // Error from the last list operation.
@@ -270,7 +270,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
270270
api := &API{
271271
ctx: ctx,
272272
cancel: cancel,
273-
initialUpdateDone: make(chan struct{}),
273+
initDone: make(chan struct{}),
274274
updateTrigger: make(chan chan error),
275275
updateInterval: defaultUpdateInterval,
276276
logger: logger,
@@ -322,18 +322,37 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
322322
}
323323

324324
// Init applies a final set of options to the API and then
325-
// begins the watcherLoop and updaterLoop. This function
326-
// must only be called once.
325+
// closes initDone. This method can only be called once.
327326
func (api *API) Init(opts ...Option) {
328327
api.mu.Lock()
329328
defer api.mu.Unlock()
330329
if api.closed {
331330
return
332331
}
332+
select {
333+
case <-api.initDone:
334+
return
335+
default:
336+
}
337+
defer close(api.initDone)
333338

334339
for _, opt := range opts {
335340
opt(api)
336341
}
342+
}
343+
344+
// Start starts the API by initializing the watcher and updater loops.
345+
// This method calls Init, if it is desired to apply options after
346+
// the API has been created, it should be done by calling Init before
347+
// Start. This method must only be called once.
348+
func (api *API) Start() {
349+
api.Init()
350+
351+
api.mu.Lock()
352+
defer api.mu.Unlock()
353+
if api.closed {
354+
return
355+
}
337356

338357
api.watcherDone = make(chan struct{})
339358
api.updaterDone = make(chan struct{})
@@ -412,9 +431,6 @@ func (api *API) updaterLoop() {
412431
} else {
413432
api.logger.Debug(api.ctx, "initial containers update complete")
414433
}
415-
// Signal that the initial update attempt (successful or not) is done.
416-
// Other services can wait on this if they need the first data to be available.
417-
close(api.initialUpdateDone)
418434

419435
// We utilize a TickerFunc here instead of a regular Ticker so that
420436
// we can guarantee execution of the updateContainers method after
@@ -474,7 +490,7 @@ func (api *API) UpdateSubAgentClient(client SubAgentClient) {
474490
func (api *API) Routes() http.Handler {
475491
r := chi.NewRouter()
476492

477-
ensureInitialUpdateDoneMW := func(next http.Handler) http.Handler {
493+
ensureInitDoneMW := func(next http.Handler) http.Handler {
478494
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
479495
select {
480496
case <-api.ctx.Done():
@@ -485,9 +501,8 @@ func (api *API) Routes() http.Handler {
485501
return
486502
case <-r.Context().Done():
487503
return
488-
case <-api.initialUpdateDone:
489-
// Initial update is done, we can start processing
490-
// requests.
504+
case <-api.initDone:
505+
// API init is done, we can start processing requests.
491506
}
492507
next.ServeHTTP(rw, r)
493508
})
@@ -496,7 +511,7 @@ func (api *API) Routes() http.Handler {
496511
// For now, all endpoints require the initial update to be done.
497512
// If we want to allow some endpoints to be available before
498513
// the initial update, we can enable this per-route.
499-
r.Use(ensureInitialUpdateDoneMW)
514+
r.Use(ensureInitDoneMW)
500515

501516
r.Get("/", api.handleList)
502517
// TODO(mafredri): Simplify this route as the previous /devcontainers

agent/agentcontainers/api_test.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ func TestAPI(t *testing.T) {
437437
agentcontainers.WithContainerCLI(mLister),
438438
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
439439
)
440-
api.Init()
440+
api.Start()
441441
defer api.Close()
442442
r.Mount("/", api.Routes())
443443

@@ -627,7 +627,7 @@ func TestAPI(t *testing.T) {
627627
agentcontainers.WithDevcontainers(tt.setupDevcontainers, nil),
628628
)
629629

630-
api.Init()
630+
api.Start()
631631
defer api.Close()
632632
r.Mount("/", api.Routes())
633633

@@ -1068,7 +1068,7 @@ func TestAPI(t *testing.T) {
10681068
}
10691069

10701070
api := agentcontainers.NewAPI(logger, apiOptions...)
1071-
api.Init()
1071+
api.Start()
10721072
defer api.Close()
10731073

10741074
r.Mount("/", api.Routes())
@@ -1158,7 +1158,7 @@ func TestAPI(t *testing.T) {
11581158
[]codersdk.WorkspaceAgentScript{{LogSourceID: uuid.New(), ID: dc.ID}},
11591159
),
11601160
)
1161-
api.Init()
1161+
api.Start()
11621162
defer api.Close()
11631163

11641164
// Make sure the ticker function has been registered
@@ -1254,7 +1254,7 @@ func TestAPI(t *testing.T) {
12541254
agentcontainers.WithWatcher(fWatcher),
12551255
agentcontainers.WithClock(mClock),
12561256
)
1257-
api.Init()
1257+
api.Start()
12581258
defer api.Close()
12591259

12601260
r := chi.NewRouter()
@@ -1408,7 +1408,7 @@ func TestAPI(t *testing.T) {
14081408
agentcontainers.WithDevcontainerCLI(fakeDCCLI),
14091409
agentcontainers.WithManifestInfo("test-user", "test-workspace", "test-parent-agent"),
14101410
)
1411-
api.Init()
1411+
api.Start()
14121412
apiClose := func() {
14131413
closeOnce.Do(func() {
14141414
// Close before api.Close() defer to avoid deadlock after test.
@@ -1635,7 +1635,7 @@ func TestAPI(t *testing.T) {
16351635
agentcontainers.WithSubAgentClient(fakeSAC),
16361636
agentcontainers.WithDevcontainerCLI(&fakeDevcontainerCLI{}),
16371637
)
1638-
api.Init()
1638+
api.Start()
16391639
defer api.Close()
16401640

16411641
tickerTrap.MustWait(ctx).MustRelease(ctx)
@@ -1958,7 +1958,7 @@ func TestAPI(t *testing.T) {
19581958
agentcontainers.WithSubAgentURL("test-subagent-url"),
19591959
agentcontainers.WithWatcher(watcher.NewNoop()),
19601960
)
1961-
api.Init()
1961+
api.Start()
19621962
defer api.Close()
19631963

19641964
// Close before api.Close() defer to avoid deadlock after test.
@@ -2052,7 +2052,7 @@ func TestAPI(t *testing.T) {
20522052
agentcontainers.WithSubAgentURL("test-subagent-url"),
20532053
agentcontainers.WithWatcher(watcher.NewNoop()),
20542054
)
2055-
api.Init()
2055+
api.Start()
20562056
defer api.Close()
20572057

20582058
// Close before api.Close() defer to avoid deadlock after test.
@@ -2158,7 +2158,7 @@ func TestAPI(t *testing.T) {
21582158
agentcontainers.WithWatcher(watcher.NewNoop()),
21592159
agentcontainers.WithManifestInfo("test-user", "test-workspace", "test-parent-agent"),
21602160
)
2161-
api.Init()
2161+
api.Start()
21622162
defer api.Close()
21632163

21642164
// Close before api.Close() defer to avoid deadlock after test.
@@ -2228,7 +2228,7 @@ func TestAPI(t *testing.T) {
22282228
agentcontainers.WithExecer(fakeExec),
22292229
agentcontainers.WithCommandEnv(commandEnv),
22302230
)
2231-
api.Init()
2231+
api.Start()
22322232
defer api.Close()
22332233

22342234
// Call RefreshContainers directly to trigger CommandEnv usage.
@@ -2318,13 +2318,16 @@ func TestAPI(t *testing.T) {
23182318
agentcontainers.WithWatcher(fWatcher),
23192319
agentcontainers.WithClock(mClock),
23202320
)
2321-
api.Init()
2321+
api.Start()
23222322
defer func() {
23232323
close(fakeSAC.createErrC)
23242324
close(fakeSAC.deleteErrC)
23252325
api.Close()
23262326
}()
23272327

2328+
err := api.RefreshContainers(ctx)
2329+
require.NoError(t, err, "RefreshContainers should not error")
2330+
23282331
r := chi.NewRouter()
23292332
r.Mount("/", api.Routes())
23302333

@@ -2335,7 +2338,7 @@ func TestAPI(t *testing.T) {
23352338
require.Equal(t, http.StatusOK, rec.Code)
23362339

23372340
var response codersdk.WorkspaceAgentListContainersResponse
2338-
err := json.NewDecoder(rec.Body).Decode(&response)
2341+
err = json.NewDecoder(rec.Body).Decode(&response)
23392342
require.NoError(t, err)
23402343

23412344
assert.Empty(t, response.Devcontainers, "ignored devcontainer should not be in response when ignore=true")
@@ -2519,7 +2522,7 @@ func TestSubAgentCreationWithNameRetry(t *testing.T) {
25192522
agentcontainers.WithSubAgentClient(fSAC),
25202523
agentcontainers.WithWatcher(watcher.NewNoop()),
25212524
)
2522-
api.Init()
2525+
api.Start()
25232526
defer api.Close()
25242527

25252528
tickerTrap.MustWait(ctx).MustRelease(ctx)

0 commit comments

Comments
 (0)