-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: add task status reporting load generator runner #20538
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package taskstatus | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "net/url" | ||
|
|
||
| "github.com/google/uuid" | ||
| "golang.org/x/xerrors" | ||
|
|
||
| "cdr.dev/slog" | ||
| "github.com/coder/coder/v2/codersdk" | ||
| "github.com/coder/coder/v2/codersdk/agentsdk" | ||
| ) | ||
|
|
||
| // createExternalWorkspaceResult contains the results from creating an external workspace. | ||
| type createExternalWorkspaceResult struct { | ||
| WorkspaceID uuid.UUID | ||
| AgentToken string | ||
| } | ||
|
|
||
| // client abstracts the details of using codersdk.Client for workspace operations. | ||
| // This interface allows for easier testing by enabling mock implementations and | ||
| // provides a cleaner separation of concerns. | ||
| // | ||
| // The interface is designed to be initialized in two phases: | ||
| // 1. Create the client with newClient(coderClient) | ||
| // 2. Configure logging when the io.Writer is available in Run() | ||
| type client interface { | ||
| // createExternalWorkspace creates an external workspace and returns the workspace ID | ||
| // and agent token for the first external agent found in the workspace resources. | ||
| createExternalWorkspace(ctx context.Context, req codersdk.CreateWorkspaceRequest) (createExternalWorkspaceResult, error) | ||
|
|
||
| // watchWorkspace watches for updates to a workspace. | ||
| watchWorkspace(ctx context.Context, workspaceID uuid.UUID) (<-chan codersdk.Workspace, error) | ||
|
|
||
| // initialize sets up the client with the provided logger, which is only available after Run() is called. | ||
| initialize(logger slog.Logger) | ||
| } | ||
|
|
||
| // appStatusPatcher abstracts the details of using agentsdk.Client for updating app status. | ||
| // This interface is separate from client because it requires an agent token which is only | ||
| // available after creating an external workspace. | ||
| type appStatusPatcher interface { | ||
| // patchAppStatus updates the status of a workspace app. | ||
| patchAppStatus(ctx context.Context, req agentsdk.PatchAppStatus) error | ||
|
|
||
| // initialize sets up the patcher with the provided logger and agent token. | ||
| initialize(logger slog.Logger, agentToken string) | ||
| } | ||
|
|
||
| // sdkClient is the concrete implementation of the client interface using | ||
| // codersdk.Client. | ||
| type sdkClient struct { | ||
| coderClient *codersdk.Client | ||
| } | ||
|
|
||
| // newClient creates a new client implementation using the provided codersdk.Client. | ||
| func newClient(coderClient *codersdk.Client) client { | ||
| return &sdkClient{ | ||
| coderClient: coderClient, | ||
| } | ||
| } | ||
|
|
||
| func (c *sdkClient) createExternalWorkspace(ctx context.Context, req codersdk.CreateWorkspaceRequest) (createExternalWorkspaceResult, error) { | ||
| // Create the workspace | ||
| workspace, err := c.coderClient.CreateUserWorkspace(ctx, codersdk.Me, req) | ||
| if err != nil { | ||
| return createExternalWorkspaceResult{}, err | ||
| } | ||
|
|
||
| // Get the workspace with latest build details | ||
| workspace, err = c.coderClient.WorkspaceByOwnerAndName(ctx, codersdk.Me, workspace.Name, codersdk.WorkspaceOptions{}) | ||
| if err != nil { | ||
| return createExternalWorkspaceResult{}, err | ||
| } | ||
|
|
||
| // Find external agents in resources | ||
| for _, resource := range workspace.LatestBuild.Resources { | ||
| if resource.Type != "coder_external_agent" || len(resource.Agents) == 0 { | ||
| continue | ||
| } | ||
|
|
||
| // Get credentials for the first agent | ||
| agent := resource.Agents[0] | ||
| credentials, err := c.coderClient.WorkspaceExternalAgentCredentials(ctx, workspace.ID, agent.Name) | ||
| if err != nil { | ||
| return createExternalWorkspaceResult{}, err | ||
| } | ||
|
|
||
| return createExternalWorkspaceResult{ | ||
| WorkspaceID: workspace.ID, | ||
| AgentToken: credentials.AgentToken, | ||
| }, nil | ||
| } | ||
|
|
||
| return createExternalWorkspaceResult{}, xerrors.Errorf("no external agent found in workspace") | ||
| } | ||
|
|
||
| func (c *sdkClient) watchWorkspace(ctx context.Context, workspaceID uuid.UUID) (<-chan codersdk.Workspace, error) { | ||
| return c.coderClient.WatchWorkspace(ctx, workspaceID) | ||
| } | ||
|
|
||
| func (c *sdkClient) initialize(logger slog.Logger) { | ||
| // Configure the coder client logging | ||
| c.coderClient.SetLogger(logger) | ||
| c.coderClient.SetLogBodies(true) | ||
| } | ||
|
|
||
| // sdkAppStatusPatcher is the concrete implementation of the appStatusPatcher interface | ||
| // using agentsdk.Client. | ||
| type sdkAppStatusPatcher struct { | ||
| agentClient *agentsdk.Client | ||
| url *url.URL | ||
| httpClient *http.Client | ||
| } | ||
|
|
||
| // newAppStatusPatcher creates a new appStatusPatcher implementation. | ||
| func newAppStatusPatcher(client *codersdk.Client) appStatusPatcher { | ||
| return &sdkAppStatusPatcher{ | ||
| url: client.URL, | ||
| httpClient: client.HTTPClient, | ||
| } | ||
| } | ||
|
|
||
| func (p *sdkAppStatusPatcher) patchAppStatus(ctx context.Context, req agentsdk.PatchAppStatus) error { | ||
| if p.agentClient == nil { | ||
| panic("agentClient not initialized - call initialize first") | ||
| } | ||
| return p.agentClient.PatchAppStatus(ctx, req) | ||
| } | ||
|
|
||
| func (p *sdkAppStatusPatcher) initialize(logger slog.Logger, agentToken string) { | ||
| // Create and configure the agent client with the provided token | ||
| p.agentClient = agentsdk.New( | ||
| p.url, | ||
| agentsdk.WithFixedToken(agentToken), | ||
| codersdk.WithHTTPClient(p.httpClient), | ||
| codersdk.WithLogger(logger), | ||
| codersdk.WithLogBodies(), | ||
| ) | ||
| } | ||
|
|
||
| // Ensure sdkClient implements the client interface. | ||
| var _ client = (*sdkClient)(nil) | ||
|
|
||
| // Ensure sdkAppStatusPatcher implements the appStatusPatcher interface. | ||
| var _ appStatusPatcher = (*sdkAppStatusPatcher)(nil) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package taskstatus | ||
|
|
||
| import ( | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/google/uuid" | ||
| "golang.org/x/xerrors" | ||
| ) | ||
|
|
||
| type Config struct { | ||
| // TemplateID is the template ID to use for creating the external workspace. | ||
| TemplateID uuid.UUID `json:"template_id"` | ||
|
|
||
| // WorkspaceName is the name for the external workspace to create. | ||
| WorkspaceName string `json:"workspace_name"` | ||
|
|
||
| // AppSlug is the slug of the app designated as the AI Agent. | ||
| AppSlug string `json:"app_slug"` | ||
|
|
||
| // When the runner has connected to the watch-ws endpoint, it will call Done once on this wait group. Used to | ||
| // coordinate multiple runners from the higher layer. | ||
| ConnectedWaitGroup *sync.WaitGroup `json:"-"` | ||
|
|
||
| // We read on this channel before starting to report task statuses. Used to coordinate multiple runners from the | ||
| // higher layer. | ||
| StartReporting chan struct{} `json:"-"` | ||
|
|
||
| // Time between reporting task statuses. | ||
| ReportStatusPeriod time.Duration `json:"report_status_period"` | ||
|
|
||
| // Total time to report task statuses, starting from when we successfully read from the StartReporting channel. | ||
| ReportStatusDuration time.Duration `json:"report_status_duration"` | ||
|
|
||
| Metrics *Metrics `json:"-"` | ||
| MetricLabelValues []string `json:"metric_label_values"` | ||
| } | ||
|
|
||
| func (c *Config) Validate() error { | ||
| if c.TemplateID == uuid.Nil { | ||
| return xerrors.Errorf("validate template_id: must not be nil") | ||
| } | ||
|
|
||
| if c.WorkspaceName == "" { | ||
| return xerrors.Errorf("validate workspace_name: must not be empty") | ||
| } | ||
|
|
||
| if c.AppSlug == "" { | ||
| return xerrors.Errorf("validate app_slug: must not be empty") | ||
| } | ||
|
|
||
| if c.ConnectedWaitGroup == nil { | ||
| return xerrors.Errorf("validate connected_wait_group: must not be nil") | ||
| } | ||
|
|
||
| if c.StartReporting == nil { | ||
| return xerrors.Errorf("validate start_reporting: must not be nil") | ||
| } | ||
|
|
||
| if c.ReportStatusPeriod <= 0 { | ||
| return xerrors.Errorf("validate report_status_period: must be greater than zero") | ||
| } | ||
|
|
||
| if c.ReportStatusDuration <= 0 { | ||
| return xerrors.Errorf("validate report_status_duration: must be greater than zero") | ||
| } | ||
|
|
||
| if c.Metrics == nil { | ||
| return xerrors.Errorf("validate metrics: must not be nil") | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package taskstatus | ||
|
|
||
| import "github.com/prometheus/client_golang/prometheus" | ||
|
|
||
| type Metrics struct { | ||
| TaskStatusToWorkspaceUpdateLatencySeconds prometheus.HistogramVec | ||
| MissingStatusUpdatesTotal prometheus.CounterVec | ||
| ReportTaskStatusErrorsTotal prometheus.CounterVec | ||
| } | ||
|
|
||
| func NewMetrics(reg prometheus.Registerer, labelNames ...string) *Metrics { | ||
| m := &Metrics{ | ||
| TaskStatusToWorkspaceUpdateLatencySeconds: *prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
| Namespace: "coderd", | ||
| Subsystem: "scaletest", | ||
| Name: "task_status_to_workspace_update_latency_seconds", | ||
| Help: "Time in seconds between reporting a task status and receiving the workspace update.", | ||
| }, labelNames), | ||
| MissingStatusUpdatesTotal: *prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Namespace: "coderd", | ||
| Subsystem: "scaletest", | ||
| Name: "missing_status_updates_total", | ||
| Help: "Total number of missing status updates.", | ||
| }, labelNames), | ||
| ReportTaskStatusErrorsTotal: *prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Namespace: "coderd", | ||
| Subsystem: "scaletest", | ||
| Name: "report_task_status_errors_total", | ||
| Help: "Total number of errors when reporting task status.", | ||
| }, labelNames), | ||
| } | ||
| reg.MustRegister(m.TaskStatusToWorkspaceUpdateLatencySeconds) | ||
| reg.MustRegister(m.MissingStatusUpdatesTotal) | ||
| reg.MustRegister(m.ReportTaskStatusErrorsTotal) | ||
| return m | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks unused