mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(coderd/x/chatd/chattool): retry workspace name conflicts (#25668)
Retry Coder Agents workspace creation once with a generated random suffix when the requested workspace name already exists. This preserves structured errors for other conflicts and avoids surfacing avoidable name collisions. Closes CODAGT-386
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -238,7 +239,7 @@ func CreateWorkspace(db database.Store, organizationID, chatID uuid.UUID, option
|
||||
)
|
||||
}
|
||||
|
||||
workspace, err := options.CreateFn(ctx, ownerID, createReq)
|
||||
workspace, err := createWorkspaceWithNameRetry(ctx, ownerID, createReq, options.CreateFn)
|
||||
if err != nil {
|
||||
if responseErr, ok := httperror.IsResponder(err); ok {
|
||||
_, resp := responseErr.Response()
|
||||
@@ -703,6 +704,41 @@ func waitForAgentReady(
|
||||
}
|
||||
}
|
||||
|
||||
func createWorkspaceWithNameRetry(
|
||||
ctx context.Context,
|
||||
ownerID uuid.UUID,
|
||||
req codersdk.CreateWorkspaceRequest,
|
||||
createFn CreateWorkspaceFn,
|
||||
) (codersdk.Workspace, error) {
|
||||
workspace, err := createFn(ctx, ownerID, req)
|
||||
if err == nil {
|
||||
return workspace, nil
|
||||
}
|
||||
if !isWorkspaceNameConflict(err) {
|
||||
return codersdk.Workspace{}, err
|
||||
}
|
||||
|
||||
req.Name = generatedWorkspaceName(req.Name)
|
||||
return createFn(ctx, ownerID, req)
|
||||
}
|
||||
|
||||
func isWorkspaceNameConflict(err error) bool {
|
||||
responseErr, ok := httperror.IsResponder(err)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
status, resp := responseErr.Response()
|
||||
if status != http.StatusConflict {
|
||||
return false
|
||||
}
|
||||
for _, validation := range resp.Validations {
|
||||
if validation.Field == "name" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func generatedWorkspaceName(seed string) string {
|
||||
base := codersdk.UsernameFrom(strings.TrimSpace(strings.ToLower(seed)))
|
||||
if strings.TrimSpace(base) == "" {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -855,6 +856,70 @@ func TestCreateWorkspace_ResponderErrorPreservesStructuredFields(t *testing.T) {
|
||||
}}, result.Validations)
|
||||
}
|
||||
|
||||
func TestCreateWorkspaceWithNameRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NameConflictRetriesWithGeneratedName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var names []string
|
||||
workspace, err := createWorkspaceWithNameRetry(
|
||||
context.Background(),
|
||||
uuid.New(),
|
||||
codersdk.CreateWorkspaceRequest{Name: "fun-dashboard"},
|
||||
func(_ context.Context, _ uuid.UUID, req codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) {
|
||||
names = append(names, req.Name)
|
||||
if len(names) == 1 {
|
||||
return codersdk.Workspace{}, workspaceNameConflictError(req.Name)
|
||||
}
|
||||
|
||||
require.Regexp(t, `^fun-dashboard-[0-9a-f]{4}$`, req.Name)
|
||||
return codersdk.Workspace{Name: req.Name}, nil
|
||||
},
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, names, 2)
|
||||
require.Equal(t, "fun-dashboard", names[0])
|
||||
require.Equal(t, names[1], workspace.Name)
|
||||
})
|
||||
|
||||
t.Run("OtherConflictDoesNotRetry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
calls := 0
|
||||
wantErr := httperror.NewResponseError(http.StatusConflict, codersdk.Response{
|
||||
Message: "quota exceeded",
|
||||
Validations: []codersdk.ValidationError{{
|
||||
Field: "quota",
|
||||
Detail: "quota exceeded",
|
||||
}},
|
||||
})
|
||||
_, err := createWorkspaceWithNameRetry(
|
||||
context.Background(),
|
||||
uuid.New(),
|
||||
codersdk.CreateWorkspaceRequest{Name: "fun-dashboard"},
|
||||
func(context.Context, uuid.UUID, codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) {
|
||||
calls++
|
||||
return codersdk.Workspace{}, wantErr
|
||||
},
|
||||
)
|
||||
|
||||
require.Same(t, wantErr, err)
|
||||
require.Equal(t, 1, calls)
|
||||
})
|
||||
}
|
||||
|
||||
func workspaceNameConflictError(name string) error {
|
||||
return httperror.NewResponseError(http.StatusConflict, codersdk.Response{
|
||||
Message: fmt.Sprintf("Workspace %q already exists.", name),
|
||||
Validations: []codersdk.ValidationError{{
|
||||
Field: "name",
|
||||
Detail: "This value is already in use and should be unique.",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateWorkspace_GlobalTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user