mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: send workspace pubsub events by owner id (#14964)
We currently send empty payloads to pubsub channels of the form `workspace:<workspace_id>` to notify listeners of updates to workspaces (such as for refreshing the workspace dashboard). To support https://github.com/coder/coder/issues/14716, we'll instead send `WorkspaceEvent` payloads to pubsub channels of the form `workspace_owner:<owner_id>`. This enables a listener to receive events for all workspaces owned by a user. This PR replaces the usage of the old channels without modifying any existing behaviors. ``` type WorkspaceEvent struct { Kind WorkspaceEventKind `json:"kind"` WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` // AgentID is only set for WorkspaceEventKindAgent* events // (excluding AgentTimeout) AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"` } ``` We've defined `WorkspaceEventKind`s based on how the old channel was used, but it's not yet necessary to inspect the types of any of the events, as the existing listeners are designed to fire off any of them. ``` WorkspaceEventKindStateChange WorkspaceEventKind = "state_change" WorkspaceEventKindStatsUpdate WorkspaceEventKind = "stats_update" WorkspaceEventKindMetadataUpdate WorkspaceEventKind = "mtd_update" WorkspaceEventKindAppHealthUpdate WorkspaceEventKind = "app_health" WorkspaceEventKindAgentLifecycleUpdate WorkspaceEventKind = "agt_lifecycle_update" WorkspaceEventKindAgentLogsUpdate WorkspaceEventKind = "agt_logs_update" WorkspaceEventKindAgentConnectionUpdate WorkspaceEventKind = "agt_connection_update" WorkspaceEventKindAgentLogsOverflow WorkspaceEventKind = "agt_logs_overflow" WorkspaceEventKindAgentTimeout WorkspaceEventKind = "agt_timeout" ```
This commit is contained in:
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/telemetry"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/wspubsub"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/drpc"
|
||||
"github.com/coder/coder/v2/provisioner"
|
||||
@@ -493,7 +494,15 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
|
||||
for _, group := range ownerGroups {
|
||||
ownerGroupNames = append(ownerGroupNames, group.Group.Name)
|
||||
}
|
||||
err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspace.ID), []byte{})
|
||||
|
||||
msg, err := json.Marshal(wspubsub.WorkspaceEvent{
|
||||
Kind: wspubsub.WorkspaceEventKindStateChange,
|
||||
WorkspaceID: workspace.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("marshal workspace update event: %s", err))
|
||||
}
|
||||
err = s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg)
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("publish workspace update: %s", err))
|
||||
}
|
||||
@@ -1023,9 +1032,16 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
|
||||
|
||||
s.notifyWorkspaceBuildFailed(ctx, workspace, build)
|
||||
|
||||
err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), []byte{})
|
||||
msg, err := json.Marshal(wspubsub.WorkspaceEvent{
|
||||
Kind: wspubsub.WorkspaceEventKindStateChange,
|
||||
WorkspaceID: workspace.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("update workspace: %w", err)
|
||||
return nil, xerrors.Errorf("marshal workspace update event: %s", err)
|
||||
}
|
||||
err = s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("publish workspace update: %w", err)
|
||||
}
|
||||
case *proto.FailedJob_TemplateImport_:
|
||||
}
|
||||
@@ -1369,9 +1385,6 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
return nil, xerrors.Errorf("update provisioner job: %w", err)
|
||||
}
|
||||
s.Logger.Debug(ctx, "marked import job as completed", slog.F("job_id", jobID))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("complete job: %w", err)
|
||||
}
|
||||
case *proto.CompletedJob_WorkspaceBuild_:
|
||||
var input WorkspaceProvisionJob
|
||||
err = json.Unmarshal(job.Input, &input)
|
||||
@@ -1491,7 +1504,15 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
return
|
||||
case <-wait:
|
||||
// Wait for the next potential timeout to occur.
|
||||
if err := s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceBuild.WorkspaceID), []byte{}); err != nil {
|
||||
msg, err := json.Marshal(wspubsub.WorkspaceEvent{
|
||||
Kind: wspubsub.WorkspaceEventKindAgentTimeout,
|
||||
WorkspaceID: workspace.ID,
|
||||
})
|
||||
if err != nil {
|
||||
s.Logger.Error(ctx, "marshal workspace update event", slog.Error(err))
|
||||
break
|
||||
}
|
||||
if err := s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg); err != nil {
|
||||
if s.lifecycleCtx.Err() != nil {
|
||||
// If the server is shutting down, we don't want to log this error, nor wait around.
|
||||
s.Logger.Debug(ctx, "stopping notifications due to server shutdown",
|
||||
@@ -1608,7 +1629,14 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
})
|
||||
}
|
||||
|
||||
err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceBuild.WorkspaceID), []byte{})
|
||||
msg, err := json.Marshal(wspubsub.WorkspaceEvent{
|
||||
Kind: wspubsub.WorkspaceEventKindStateChange,
|
||||
WorkspaceID: workspace.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("marshal workspace update event: %s", err)
|
||||
}
|
||||
err = s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("update workspace: %w", err)
|
||||
}
|
||||
@@ -1639,9 +1667,6 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
return nil, xerrors.Errorf("update provisioner job: %w", err)
|
||||
}
|
||||
s.Logger.Debug(ctx, "marked template dry-run job as completed", slog.F("job_id", jobID))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("complete job: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
if completed.Type == nil {
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
"github.com/coder/coder/v2/coderd/telemetry"
|
||||
"github.com/coder/coder/v2/coderd/wspubsub"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionerd/proto"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
@@ -295,12 +296,19 @@ func TestAcquireJob(t *testing.T) {
|
||||
|
||||
startPublished := make(chan struct{})
|
||||
var closed bool
|
||||
closeStartSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, _ []byte) {
|
||||
if !closed {
|
||||
close(startPublished)
|
||||
closed = true
|
||||
}
|
||||
})
|
||||
closeStartSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID),
|
||||
wspubsub.HandleWorkspaceEvent(
|
||||
func(_ context.Context, e wspubsub.WorkspaceEvent, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID {
|
||||
if !closed {
|
||||
close(startPublished)
|
||||
closed = true
|
||||
}
|
||||
}
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer closeStartSubscribe()
|
||||
|
||||
@@ -398,9 +406,16 @@ func TestAcquireJob(t *testing.T) {
|
||||
})
|
||||
|
||||
stopPublished := make(chan struct{})
|
||||
closeStopSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, _ []byte) {
|
||||
close(stopPublished)
|
||||
})
|
||||
closeStopSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID),
|
||||
wspubsub.HandleWorkspaceEvent(
|
||||
func(_ context.Context, e wspubsub.WorkspaceEvent, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID {
|
||||
close(stopPublished)
|
||||
}
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer closeStopSubscribe()
|
||||
|
||||
@@ -874,12 +889,11 @@ func TestFailJob(t *testing.T) {
|
||||
auditor: auditor,
|
||||
})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
workspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
|
||||
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
ID: uuid.New(),
|
||||
AutomaticUpdates: database.AutomaticUpdatesNever,
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
buildID := uuid.New()
|
||||
input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
|
||||
WorkspaceBuildID: buildID,
|
||||
@@ -889,6 +903,7 @@ func TestFailJob(t *testing.T) {
|
||||
job, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
Input: input,
|
||||
InitiatorID: workspace.OwnerID,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
@@ -897,6 +912,7 @@ func TestFailJob(t *testing.T) {
|
||||
err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
|
||||
ID: buildID,
|
||||
WorkspaceID: workspace.ID,
|
||||
InitiatorID: workspace.OwnerID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
JobID: job.ID,
|
||||
@@ -913,9 +929,16 @@ func TestFailJob(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
publishedWorkspace := make(chan struct{})
|
||||
closeWorkspaceSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, _ []byte) {
|
||||
close(publishedWorkspace)
|
||||
})
|
||||
closeWorkspaceSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID),
|
||||
wspubsub.HandleWorkspaceEvent(
|
||||
func(_ context.Context, e wspubsub.WorkspaceEvent, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID {
|
||||
close(publishedWorkspace)
|
||||
}
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer closeWorkspaceSubscribe()
|
||||
publishedLogs := make(chan struct{})
|
||||
@@ -1279,13 +1302,15 @@ func TestCompleteJob(t *testing.T) {
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: workspaceTable.ID,
|
||||
InitiatorID: user.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
Transition: c.transition,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
FileID: file.ID,
|
||||
InitiatorID: user.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
|
||||
WorkspaceBuildID: build.ID,
|
||||
})),
|
||||
@@ -1302,9 +1327,16 @@ func TestCompleteJob(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
publishedWorkspace := make(chan struct{})
|
||||
closeWorkspaceSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), func(_ context.Context, _ []byte) {
|
||||
close(publishedWorkspace)
|
||||
})
|
||||
closeWorkspaceSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspaceTable.OwnerID),
|
||||
wspubsub.HandleWorkspaceEvent(
|
||||
func(_ context.Context, e wspubsub.WorkspaceEvent, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspaceTable.ID {
|
||||
close(publishedWorkspace)
|
||||
}
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
defer closeWorkspaceSubscribe()
|
||||
publishedLogs := make(chan struct{})
|
||||
@@ -1643,8 +1675,9 @@ func TestNotifications(t *testing.T) {
|
||||
Reason: tc.deletionReason,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
FileID: file.ID,
|
||||
InitiatorID: initiator.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
|
||||
WorkspaceBuildID: build.ID,
|
||||
})),
|
||||
@@ -1761,8 +1794,9 @@ func TestNotifications(t *testing.T) {
|
||||
Reason: tc.buildReason,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
FileID: file.ID,
|
||||
InitiatorID: initiator.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
|
||||
WorkspaceBuildID: build.ID,
|
||||
})),
|
||||
@@ -1833,6 +1867,7 @@ func TestNotifications(t *testing.T) {
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
|
||||
FileID: dbgen.File(t, db, database.File{CreatedBy: user.ID}).ID,
|
||||
InitiatorID: user.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{WorkspaceBuildID: build.ID})),
|
||||
OrganizationID: pd.OrganizationID,
|
||||
|
||||
Reference in New Issue
Block a user