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(agent/agentcontainers): retry with longer name on failure#18513

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 15 commits intomainfromdm-devcontainer-retry
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
15 commits
Select commitHold shift + click to select a range
82ed807
feat(agent/agentcontainers): retry with longer name on failure
DanielleMaywoodJun 23, 2025
7cf8181
chore: listen to feedback
DanielleMaywoodJun 23, 2025
5873a47
fix: remove `arch: "amd64"`
DanielleMaywoodJun 23, 2025
fb5079c
chore: maybe use runtime.GOARCH?
DanielleMaywoodJun 23, 2025
b6b9cea
chore: skip test on windows
DanielleMaywoodJun 23, 2025
057360a
chore: fix logic and reduce flakiness of test
DanielleMaywoodJun 24, 2025
df43868
chore: `map[string]struct{}` to `map[string]bool`
DanielleMaywoodJun 24, 2025
c25f651
chore: delete from another place
DanielleMaywoodJun 24, 2025
4c79563
chore: mark as using folder name when not falling back to friendly name
DanielleMaywoodJun 24, 2025
15dafc7
chore: fallback if we're using a name already defined in terraform
DanielleMaywoodJun 24, 2025
08969ce
chore: use map of known names to help create an agent name
DanielleMaywoodJun 24, 2025
14bbb1d
chore: testing
DanielleMaywoodJun 24, 2025
b8fe8c5
Merge branch 'main' into dm-devcontainer-retry
DanielleMaywoodJun 24, 2025
257c62d
chore: appease linter
DanielleMaywoodJun 24, 2025
627fe9f
fix: maybe fix race condition?
DanielleMaywoodJun 24, 2025
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
132 changes: 111 additions & 21 deletionsagent/agentcontainers/api.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ const (
// read-write, which seems sensible for devcontainers.
coderPathInsideContainer = "/.coder-agent/coder"

maxAgentNameLength = 64
maxAgentNameLength = 64
maxAttemptsToNameAgent = 5
)

// API is responsible for container-related operations in the agent.
Expand DownExpand Up@@ -71,17 +72,18 @@ type API struct {
ownerName string
workspaceName string

mu sync.RWMutex
closed bool
containers codersdk.WorkspaceAgentListContainersResponse // Output from the last list operation.
containersErr error // Error from the last list operation.
devcontainerNames map[string]bool // By devcontainer name.
knownDevcontainers map[string]codersdk.WorkspaceAgentDevcontainer // By workspace folder.
configFileModifiedTimes map[string]time.Time // By config file path.
recreateSuccessTimes map[string]time.Time // By workspace folder.
recreateErrorTimes map[string]time.Time // By workspace folder.
injectedSubAgentProcs map[string]subAgentProcess // By workspace folder.
asyncWg sync.WaitGroup
mu sync.RWMutex
closed bool
containers codersdk.WorkspaceAgentListContainersResponse // Output from the last list operation.
containersErr error // Error from the last list operation.
devcontainerNames map[string]bool // By devcontainer name.
knownDevcontainers map[string]codersdk.WorkspaceAgentDevcontainer // By workspace folder.
configFileModifiedTimes map[string]time.Time // By config file path.
recreateSuccessTimes map[string]time.Time // By workspace folder.
recreateErrorTimes map[string]time.Time // By workspace folder.
injectedSubAgentProcs map[string]subAgentProcess // By workspace folder.
usingWorkspaceFolderName map[string]bool // By workspace folder.
asyncWg sync.WaitGroup

devcontainerLogSourceIDs map[string]uuid.UUID // By workspace folder.
}
Expand DownExpand Up@@ -278,6 +280,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
recreateErrorTimes: make(map[string]time.Time),
scriptLogger: func(uuid.UUID) ScriptLogger { return noopScriptLogger{} },
injectedSubAgentProcs: make(map[string]subAgentProcess),
usingWorkspaceFolderName: make(map[string]bool),
}
// The ctx and logger must be set before applying options to avoid
// nil pointer dereference.
Expand DownExpand Up@@ -622,7 +625,7 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
// folder's name. If it is not possible to generate a valid
// agent name based off of the folder name (i.e. no valid characters),
// we will instead fall back to using the container's friendly name.
dc.Name = safeAgentName(path.Base(filepath.ToSlash(dc.WorkspaceFolder)), dc.Container.FriendlyName)
dc.Name, api.usingWorkspaceFolderName[dc.WorkspaceFolder] = api.makeAgentName(dc.WorkspaceFolder, dc.Container.FriendlyName)
}
}

Expand DownExpand Up@@ -670,8 +673,10 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
var consecutiveHyphenRegex = regexp.MustCompile("-+")

// `safeAgentName` returns a safe agent name derived from a folder name,
// falling back to the container’s friendly name if needed.
func safeAgentName(name string, friendlyName string) string {
// falling back to the container’s friendly name if needed. The second
// return value will be `true` if it succeeded and `false` if it had
// to fallback to the friendly name.
func safeAgentName(name string, friendlyName string) (string, bool) {
// Keep only ASCII letters and digits, replacing everything
// else with a hyphen.
var sb strings.Builder
Expand All@@ -693,10 +698,10 @@ func safeAgentName(name string, friendlyName string) string {
name = name[:min(len(name), maxAgentNameLength)]

if provisioner.AgentNameRegex.Match([]byte(name)) {
return name
return name, true
}

return safeFriendlyName(friendlyName)
return safeFriendlyName(friendlyName), false
}

// safeFriendlyName returns a API safe version of the container's
Expand All@@ -711,6 +716,47 @@ func safeFriendlyName(name string) string {
return name
}

// expandedAgentName creates an agent name by including parent directories
// from the workspace folder path to avoid name collisions. Like `safeAgentName`,
// the second returned value will be true if using the workspace folder name,
// and false if it fell back to the friendly name.
func expandedAgentName(workspaceFolder string, friendlyName string, depth int) (string, bool) {
var parts []string
for part := range strings.SplitSeq(filepath.ToSlash(workspaceFolder), "/") {
if part = strings.TrimSpace(part); part != "" {
parts = append(parts, part)
}
}
if len(parts) == 0 {
return safeFriendlyName(friendlyName), false
}

components := parts[max(0, len(parts)-depth-1):]
expanded := strings.Join(components, "-")

return safeAgentName(expanded, friendlyName)
}

// makeAgentName attempts to create an agent name. It will first attempt to create an
// agent name based off of the workspace folder, and will eventually fallback to a
// friendly name. Like `safeAgentName`, the second returned value will be true if the
// agent name utilizes the workspace folder, and false if it falls back to the
// friendly name.
func (api *API) makeAgentName(workspaceFolder string, friendlyName string) (string, bool) {
for attempt := 0; attempt <= maxAttemptsToNameAgent; attempt++ {
agentName, usingWorkspaceFolder := expandedAgentName(workspaceFolder, friendlyName, attempt)
if !usingWorkspaceFolder {
return agentName, false
}

if !api.devcontainerNames[agentName] {
return agentName, true
}
}

return safeFriendlyName(friendlyName), false
}

// RefreshContainers triggers an immediate update of the container list
// and waits for it to complete.
func (api *API) RefreshContainers(ctx context.Context) (err error) {
Expand DownExpand Up@@ -1226,6 +1272,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
if provisioner.AgentNameRegex.Match([]byte(name)) {
subAgentConfig.Name = name
configOutdated = true
delete(api.usingWorkspaceFolderName, dc.WorkspaceFolder)
} else {
logger.Warn(ctx, "invalid name in devcontainer customization, ignoring",
slog.F("name", name),
Expand DownExpand Up@@ -1312,12 +1359,55 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
)

// Create new subagent record in the database to receive the auth token.
// If we get a unique constraint violation, try with expanded names that
// include parent directories to avoid collisions.
client := *api.subAgentClient.Load()
newSubAgent, err := client.Create(ctx, subAgentConfig)
if err != nil {
return xerrors.Errorf("create subagent failed: %w", err)

originalName := subAgentConfig.Name

for attempt := 1; attempt <= maxAttemptsToNameAgent; attempt++ {
if proc.agent, err = client.Create(ctx, subAgentConfig); err == nil {
if api.usingWorkspaceFolderName[dc.WorkspaceFolder] {
api.devcontainerNames[dc.Name] = true
delete(api.usingWorkspaceFolderName, dc.WorkspaceFolder)
}

break
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we can delete fromusingWorkspaceFolder here?

DanielleMaywood reacted with thumbs up emoji
}

// NOTE(DanielleMaywood):
// Ordinarily we'd use `errors.As` here, but it didn't appear to work. Not
// sure if this is because of the communication protocol? Instead I've opted
// for a slightly more janky string contains approach.
//
// We only care if sub agent creation has failed due to a unique constraint
// violation on the agent name, as we can _possibly_ rectify this.
if !strings.Contains(err.Error(), "workspace agent name") {
Copy link
Member

Choose a reason for hiding this comment

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

errors.As is a bit tricky but typically works if you have a pointer to the type like:

myErr:=&someError{}// or maybe: var myErr *someErroriferrors.As(err,&myErr) {// ...}

It's very possible I got something wrong there as well.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

That is what I was trying. I'll try again because I'm not happy with the solution I ended up with.

I'm not entirely sure why it didn't work but it could possibly be because it is wrapped inxerrors.Errorf and then transported over the wire via drpc. Is it possible that in the serialization and deserialization process some of that information was lost?

iferr!=nil {
returnnil,xerrors.Errorf("insert sub agent: %w",err)
}

return xerrors.Errorf("create subagent failed: %w", err)
}

// If there has been a unique constraint violation but the user is *not*
// using an auto-generated name, then we should error. This is because
// we do not want to surprise the user with a name they did not ask for.
if usingFolderName := api.usingWorkspaceFolderName[dc.WorkspaceFolder]; !usingFolderName {
return xerrors.Errorf("create subagent failed: %w", err)
}

if attempt == maxAttemptsToNameAgent {
return xerrors.Errorf("create subagent failed after %d attempts: %w", attempt, err)
}

// We increase how much of the workspace folder is used for generating
// the agent name. With each iteration there is greater chance of this
// being successful.
subAgentConfig.Name, api.usingWorkspaceFolderName[dc.WorkspaceFolder] = expandedAgentName(dc.WorkspaceFolder, dc.Container.FriendlyName, attempt)

logger.Debug(ctx, "retrying subagent creation with expanded name",
slog.F("original_name", originalName),
slog.F("expanded_name", subAgentConfig.Name),
slog.F("attempt", attempt+1),
)
}
proc.agent = newSubAgent

logger.Info(ctx, "created new subagent", slog.F("agent_id", proc.agent.ID))
} else {
Expand Down
159 changes: 158 additions & 1 deletionagent/agentcontainers/api_internal_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ func TestSafeAgentName(t *testing.T) {
name string
folderName string
expected string
fallback bool
}{
// Basic valid names
{
Expand DownExpand Up@@ -110,18 +111,22 @@ func TestSafeAgentName(t *testing.T) {
{
folderName: "",
expected: "friendly-fallback",
fallback: true,
},
{
folderName: "---",
expected: "friendly-fallback",
fallback: true,
},
{
folderName: "___",
expected: "friendly-fallback",
fallback: true,
},
{
folderName: "@#$",
expected: "friendly-fallback",
fallback: true,
},

// Additional edge cases
Expand DownExpand Up@@ -192,10 +197,162 @@ func TestSafeAgentName(t *testing.T) {
for _, tt := range tests {
t.Run(tt.folderName, func(t *testing.T) {
t.Parallel()
name := safeAgentName(tt.folderName, "friendly-fallback")
name, usingWorkspaceFolder := safeAgentName(tt.folderName, "friendly-fallback")

assert.Equal(t, tt.expected, name)
assert.True(t, provisioner.AgentNameRegex.Match([]byte(name)))
assert.Equal(t, tt.fallback, !usingWorkspaceFolder)
})
}
}

func TestExpandedAgentName(t *testing.T) {
t.Parallel()

tests := []struct {
name string
workspaceFolder string
friendlyName string
depth int
expected string
fallback bool
}{
{
name: "simple path depth 1",
workspaceFolder: "/home/coder/project",
friendlyName: "friendly-fallback",
depth: 0,
expected: "project",
},
{
name: "simple path depth 2",
workspaceFolder: "/home/coder/project",
friendlyName: "friendly-fallback",
depth: 1,
expected: "coder-project",
},
{
name: "simple path depth 3",
workspaceFolder: "/home/coder/project",
friendlyName: "friendly-fallback",
depth: 2,
expected: "home-coder-project",
},
{
name: "simple path depth exceeds available",
workspaceFolder: "/home/coder/project",
friendlyName: "friendly-fallback",
depth: 9,
expected: "home-coder-project",
},
// Cases with special characters that need sanitization
{
name: "path with spaces and special chars",
workspaceFolder: "/home/coder/My Project_v2",
friendlyName: "friendly-fallback",
depth: 1,
expected: "coder-my-project-v2",
},
{
name: "path with dots and underscores",
workspaceFolder: "/home/user.name/project_folder.git",
friendlyName: "friendly-fallback",
depth: 1,
expected: "user-name-project-folder-git",
},
// Edge cases
{
name: "empty path",
workspaceFolder: "",
friendlyName: "friendly-fallback",
depth: 0,
expected: "friendly-fallback",
fallback: true,
},
{
name: "root path",
workspaceFolder: "/",
friendlyName: "friendly-fallback",
depth: 0,
expected: "friendly-fallback",
fallback: true,
},
{
name: "single component",
workspaceFolder: "project",
friendlyName: "friendly-fallback",
depth: 0,
expected: "project",
},
{
name: "single component with depth 2",
workspaceFolder: "project",
friendlyName: "friendly-fallback",
depth: 1,
expected: "project",
},
// Collision simulation cases
{
name: "foo/project depth 1",
workspaceFolder: "/home/coder/foo/project",
friendlyName: "friendly-fallback",
depth: 0,
expected: "project",
},
{
name: "foo/project depth 2",
workspaceFolder: "/home/coder/foo/project",
friendlyName: "friendly-fallback",
depth: 1,
expected: "foo-project",
},
{
name: "bar/project depth 1",
workspaceFolder: "/home/coder/bar/project",
friendlyName: "friendly-fallback",
depth: 0,
expected: "project",
},
{
name: "bar/project depth 2",
workspaceFolder: "/home/coder/bar/project",
friendlyName: "friendly-fallback",
depth: 1,
expected: "bar-project",
},
// Path with trailing slashes
{
name: "path with trailing slash",
workspaceFolder: "/home/coder/project/",
friendlyName: "friendly-fallback",
depth: 1,
expected: "coder-project",
},
{
name: "path with multiple trailing slashes",
workspaceFolder: "/home/coder/project///",
friendlyName: "friendly-fallback",
depth: 1,
expected: "coder-project",
},
// Path with leading slashes
{
name: "path with multiple leading slashes",
workspaceFolder: "///home/coder/project",
friendlyName: "friendly-fallback",
depth: 1,
expected: "coder-project",
},
Comment on lines +339 to +345
Copy link
Member

Choose a reason for hiding this comment

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

For funsies, how about a Windows-style path?

C:\Documents and Settings\My Username\Documents\Code\Some Project Version 3\

We can skip if you don't think that's valuable.

}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
name, usingWorkspaceFolder := expandedAgentName(tt.workspaceFolder, tt.friendlyName, tt.depth)

assert.Equal(t, tt.expected, name)
assert.True(t, provisioner.AgentNameRegex.Match([]byte(name)))
assert.Equal(t, tt.fallback, !usingWorkspaceFolder)
})
}
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp