Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(site): use websocket connection for devcontainer updates#18808

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
DanielleMaywood merged 33 commits intomainfromdanielle/container-push
Jul 14, 2025

Conversation

DanielleMaywood
Copy link
Contributor

Instead of polling every 10 seconds, we instead use a WebSocket connection for more timely updates.

BrunoQuaresma reacted with heart emoji
Copilot

This comment was marked as outdated.

@DanielleMaywoodDanielleMaywood marked this pull request as ready for reviewJuly 9, 2025 13:39
@DanielleMaywoodDanielleMaywood marked this pull request as draftJuly 9, 2025 13:50
@DanielleMaywoodDanielleMaywood marked this pull request as ready for reviewJuly 10, 2025 08:21
Copy link
Contributor

@CopilotCopilotAI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Pull Request Overview

Adds real-time devcontainer updates over WebSocket instead of polling, and updates related client and server code.

  • IntroduceuseAgentContainers hook leveraging WebSockets in the frontend
  • Add WebSocket watch endpoints and SDK methods for container updates across site API, SDK, and agent code
  • Replace polling inAgentRow with the new hook and update documentation and tests accordingly

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.

Show a summary per file
FileDescription
site/src/modules/resources/useAgentContainers.tsNew React hook subscribing to container updates via WS
site/src/modules/resources/useAgentContainers.test.tsxTests for the hook’s initial fetch behavior
site/src/modules/resources/AgentRow.tsxSwitched from polling to WebSocket hook
site/src/api/api.tsExportwatchAgentContainers for WebSocket route
docs/reference/api/agents.mdAdd API reference for the new watch endpoint
codersdk/workspacesdk/agentconn.goAddWatchContainers method to agent connection SDK
codersdk/workspaceagents.goAddWatchWorkspaceAgentContainers to SDK client
coderd/workspaceagents.goImplement WebSocket handler for watch endpoint
coderd/coderd.goRegister/containers/watch route
coderd/apidoc/swagger.json & docs.goDocument new watch route in Swagger and docs template
agent/agentcontainers/api.goAddwatchContainers WS handler in agent API
Comments suppressed due to low confidence (5)

docs/reference/api/agents.md:908

  • This endpoint upgrades to a WebSocket connection rather than a plain HTTP GET; consider updating the code sample to show a WebSocket client (e.g.,wscat or JS) for clarity.
curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/containers/watch \

site/src/modules/resources/useAgentContainers.test.tsx:85

  • Tests currently cover only the initial fetch; add a test that mocks WebSocket messages and verifies thatuseAgentContainers updates when a message arrives.
});

site/src/modules/resources/AgentRow.tsx:75

  • ThequeryClient variable is declared but never used; consider removing this import and assignment to reduce clutter.
const queryClient = useQueryClient();

agent/agentcontainers/api.go:585

  • Theslices package is referenced here but not imported, causing a compile error. Addimport "slices" or reference the correct package.
api.updateChans = slices.DeleteFunc(api.updateChans, func(ch chan struct{}) bool {

site/src/modules/resources/useAgentContainers.ts:28

  • CallinginvalidateQueries aftersetQueryData will trigger an extra network request on each WebSocket message. You can remove the invalidation to prevent unnecessary refetches.
await queryClient.invalidateQueries({ queryKey });

Copy link
Member

@johnstcnjohnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Backend changes LGTM 👍

Copy link
Member

@mafredrimafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Pointed out a few gotchas but otherwise looks good, nice work!

updateCh := make(chan struct{}, 1)
defer func() {
api.mu.Lock()
close(updateCh)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It'd make sense to move this to the defer below to keep the related code close to each other.

DanielleMaywood reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed in6ce5c19

return
}

ctx = api.ctx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We could do this here instead:

ctx,wsNetConn:=codersdk.WebsocketNetConn(ctx,conn,websocket.MessageBinary)deferwsNetConn.Close()

Note that we're not passingapi.ctx toWebsocketNetConn but that's OK because of the final select (see below).

Then a few important changes following:

  • Heartbeat doesn't change (needsPing fromconn)
  • We can omitconn.Close (handled viawsNetConn.Close)
  • Encoder useswsNetConn
  • Final select has one entry forapi.ctx and another forctx

This follows practice we use elsewhere in the code-base.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is this possible?wsjson.NewEncoder requires a*websocket.Conn butwsNetConn is anet.Conn

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

wsjson.NewEncoder is a tiny wrapper, i'll just write the underlying logic usingwsNetConn

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed in04a92a4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Oh I see, I hadn't actually seen/usedwsjson.NewEncoder before. TheCloseRead it calls would be ideal to retain.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Have re-added this in 😄096a85e

@@ -583,8 +647,32 @@ func (api *API) updateContainers(ctx context.Context) error {
api.mu.Lock()
defer api.mu.Unlock()

var knownDevcontainers map[string]codersdk.WorkspaceAgentDevcontainer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Suggestion: Naming so nobody accidentally updates this map rather thanapi.knownDevcontainers. PerhapsknownDevcontainersBackup or something along those lines those lines.

DanielleMaywood reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed incd0c2d5

return
}

ctx = api.ctx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

See related comments onapi.go.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed in04a92a4

deferres.Body.Close()
}

d:=wsjson.NewDecoder[codersdk.WorkspaceAgentListContainersResponse](conn,websocket.MessageText, slog.Logger{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What's the repercussion of passingslog.Logger here, unstructured output on stdout?

If it's required, perhaps we should require it as aWatchContainers parameter if we can't put it on theAgentConn struct.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I completely forgot to update this when I was tinkering 🤦‍♀️ Will resolve this

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed in3e50965

socket.addEventListener("message", (event) => {
if (event.parseError) {
displayError(
"Unable to process latest data from the server. Please try refreshing the page.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Could this message be more specific? I.e. mention what operation failed?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Possibly although I'm just following existing logic here. I'm not sure what context about the error exists to make this more useful.

mafredri reacted with thumbs up emoji
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I know that you are just following the existing logic, but I see this as an opportunity to do better with those messages, having in mind they are displayed to the users.

ThedisplayError function accepts a message and a description, so to make this error better to the user I think we can make the message shorter and tell to the user what is the impact of it, instead of the techinical reason, and give it an way to possible solve the problem as you already put in the message.

displayError("Failed to update containers","Please try refreshing the page")

Wdyt?


socket.addEventListener("error", () => {
displayError(
"Unable to get workspace containers. Connection has been closed.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Do we just give up here? Is there precedent for trying to reconnect later?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

For the frontend changes I was following logic I found elsewhere.

mafredri reacted with thumbs up emoji
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Same as my previous comment.

displayError("Failed to load containers","Please try refreshing the page")

d.WorkspaceFolder == other.WorkspaceFolder &&
d.Status == other.Status &&
d.Dirty == other.Dirty &&
d.Error == other.Error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We should also check container and agent, so something like this?

Suggested change
d.Error== other.Error
d.Error==other.Error&&
(d.Container==nil&&other.Container==nil|| (d.Container!=nil&&other.Continer!=nil&&d.Container.ID==other.Container.ID))&&
(d.Agent==nil&&...d.Agent.ID==...

DanielleMaywood reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed in001ccda

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I don't seed.Agent checked? IMO we should propagate changes to it as well since that's how the frontend associates the sub agent.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Oh silly me I only read half the sentence 🤦‍♀️

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed again in971f9d6 😅


// If the agent is unreachable, the request will hang. Assume that if we
// don't get a response after 30s that the agent is unreachable.
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Suggestion: call thisdialCtx to avoid re-use. I believe we will always have a disconnect after 30s currently?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Addressed in04a92a4

Copy link
Collaborator

@BrunoQuaresmaBrunoQuaresma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@DanielleMaywood I'm missing some screenshots in the PR description showing the errors being displayed since we don't have stories in Storybook for that. Could you please add that? 🙏

I left a few comments related to the error messages.

socket.addEventListener("message", (event) => {
if (event.parseError) {
displayError(
"Unable to process latest data from the server. Please try refreshing the page.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I know that you are just following the existing logic, but I see this as an opportunity to do better with those messages, having in mind they are displayed to the users.

ThedisplayError function accepts a message and a description, so to make this error better to the user I think we can make the message shorter and tell to the user what is the impact of it, instead of the techinical reason, and give it an way to possible solve the problem as you already put in the message.

displayError("Failed to update containers","Please try refreshing the page")

Wdyt?


socket.addEventListener("error", () => {
displayError(
"Unable to get workspace containers. Connection has been closed.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Same as my previous comment.

displayError("Failed to load containers","Please try refreshing the page")

expect(result.current).toBeUndefined();
});
});
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think it would be a plus to have tests to verify:

  • Parsing error
  • Socket error

I know how annoying it can be to have this setup working properly without causing any flakes, so I wanted to let you know in case you are curious about, but it is not a blocket at all.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I've tried to add those.

I just want to give a fair warning: These tests are mostly written by Claude Sonnet 4 as my frontend testing experience is lacking. Please let me know if it has made any obvious blunders 🙏

BrunoQuaresma reacted with thumbs up emoji
@mafredri
Copy link
Member

I tried out this branch and it looks like there's still XHR fetches happening in addition to the websocket pushing data. It'd be great if we could stop the XHR from happening and only rely on the websocket.

See:

image

There's no polling though, which is great.

(Cleared network history so websocket isn't visible here, but it's still there and sending data as well.)

Copy link
Member

@mafredrimafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Approving BE with minor suggestions, as per my earlier comment FE XHR might still need some love.

@DanielleMaywood
Copy link
ContributorAuthor

DanielleMaywood commentedJul 14, 2025
edited
Loading

I tried out this branch and it looks like there's still XHR fetches happening in addition to the websocket pushing data. It'd be great if we could stop the XHR from happening and only rely on the websocket.

See:

imageThere's no polling though, which is great.

(Cleared network history so websocket isn't visible here, but it's still there and sending data as well.)

I've removed the query invalidation logic. This shouldreduce the number of XHR requests now. The only area left to handle is the/recreate flow. I'm not sure I want to tackle that in this PR though, so will leave that as a follow up.

I've attempted to reduce as many XHR requests as possible without entirely removing them.

@DanielleMaywoodDanielleMaywood merged commit43b0bb7 intomainJul 14, 2025
33 of 35 checks passed
@DanielleMaywoodDanielleMaywood deleted the danielle/container-push branchJuly 14, 2025 20:35
@github-actionsgithub-actionsbot locked and limited conversation to collaboratorsJul 14, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.
Reviewers

Copilot code reviewCopilotCopilot left review comments

@mafredrimafredrimafredri approved these changes

@BrunoQuaresmaBrunoQuaresmaBrunoQuaresma approved these changes

@johnstcnjohnstcnjohnstcn approved these changes

Assignees

@DanielleMaywoodDanielleMaywood

Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

4 participants
@DanielleMaywood@mafredri@BrunoQuaresma@johnstcn

[8]ページ先頭

©2009-2025 Movatter.jp