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

Skip to content

chore: Add watch workspace endpoint #1493

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
chore: Add watch workspace endpoint
  • Loading branch information
f0ssel committed May 18, 2022
commit 0760f8d00d2f0ecbcd6c1c4533596f3e912d714f
1 change: 1 addition & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ func New(options *Options) (http.Handler, func()) {
r.Put("/", api.putWorkspaceAutostop)
})
})
r.HandleFunc("/watch", api.watchWorkspace)
})
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
r.Use(
Expand Down
92 changes: 92 additions & 0 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import (
"errors"
"fmt"
"net/http"
"time"

"cdr.dev/slog"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"

"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/database"
Expand Down Expand Up @@ -535,6 +539,94 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) {
}
}

func (api *api) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)

c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
// Fix for Safari 15.1:
// There is a bug in latest Safari in which compressed web socket traffic
// isn't handled correctly. Turning off compression is a workaround:
// https://github.com/nhooyr/websocket/issues/218
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
api.Logger.Warn(r.Context(), "accept websocket connection", slog.Error(err))
return
}
defer c.Close(websocket.StatusInternalError, "internal error")

ctx := c.CloseRead(r.Context())

// Send a heartbeat every 15 seconds to avoid the websocket being killed.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this actually necessary if we're already sending updates every second?

Copy link
Contributor Author

@f0ssel f0ssel May 17, 2022

Choose a reason for hiding this comment

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

This is a great callout, I haven't considered it but you are correct.

That said, I'd like to have the precedent that we always do a heartbeat for websockets as a best practice. If the behavior of this websocket changes to only send updates and we go longer than 15 seconds without an update the browser could close the connection on us.

go func() {
ticker := time.NewTicker(time.Second * 15)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
err := c.Ping(ctx)
if err != nil {
return
}
}
}
}()

t := time.NewTicker(time.Second * 1)
defer t.Stop()
for {
select {
case <-t.C:
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID)
if err != nil {
_ = wsjson.Write(ctx, c, httpapi.Response{
Message: fmt.Sprintf("get workspace: %s", err),
})
return
}
build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err != nil {
_ = wsjson.Write(ctx, c, httpapi.Response{
Message: fmt.Sprintf("get workspace build: %s", err),
})
return
}
var (
group errgroup.Group
job database.ProvisionerJob
template database.Template
owner database.User
)
group.Go(func() (err error) {
job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
return err
})
group.Go(func() (err error) {
template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
return err
})
group.Go(func() (err error) {
owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID)
return err
})
err = group.Wait()
if err != nil {
_ = wsjson.Write(ctx, c, httpapi.Response{
Message: fmt.Sprintf("fetch resource: %s", err),
})
return
}

_ = wsjson.Write(ctx, c, convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
case <-ctx.Done():
return
}
}
}

func convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) {
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
templateIDs := make([]uuid.UUID, 0, len(workspaces))
Expand Down