Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion pkg/cmd/gpucreate/gpucreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ func (c *createContext) handleCreateError(workerID int, instanceType, instanceNa
c.logf("[Worker %d] %s Failed: %s\n", workerID+1, c.colorize(instanceType, c.t.Yellow), errStr)

result.hadFailure = true
if strings.Contains(errStr, "duplicate workspace") {
if store.IsHTTPErrorCode(err, http.StatusBadRequest, "WORKSPACE_ALREADY_EXISTS") {
result.fatalError = fmt.Errorf("workspace '%s' already exists. Use a different name or delete the existing workspace", instanceName)
}
}
Expand Down
82 changes: 82 additions & 0 deletions pkg/cmd/gpucreate/gpucreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gpucreate
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
Expand All @@ -16,6 +17,7 @@ import (
breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/store"
"github.com/brevdev/brev-cli/pkg/terminal"
resty "github.com/go-resty/resty/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -109,6 +111,86 @@ func (m *MockGPUCreateStore) CreateWorkspace(organizationID string, options *sto
return ws, nil
}

type retryTrackingStore struct {
*MockGPUCreateStore
attemptedTypes []string
}

func (s *retryTrackingStore) CreateWorkspace(organizationID string, options *store.CreateWorkspacesOptions) (*entity.Workspace, error) {
s.attemptedTypes = append(s.attemptedTypes, options.InstanceType)
return s.MockGPUCreateStore.CreateWorkspace(organizationID, options)
}

func TestRunGPUCreateRetryDecisions(t *testing.T) {
httpError := func(t *testing.T, status int, body string) error {
t.Helper()
response := (&resty.Response{
RawResponse: &http.Response{StatusCode: status, Status: http.StatusText(status)},
Request: resty.New().R(),
}).SetBody([]byte(body))
return store.NewHTTPResponseError(response)
}

tests := []struct {
name string
firstError func(*testing.T) error
wantAttempts []string
wantError bool
}{
{
name: "duplicate workspace code stops before another candidate",
firstError: func(t *testing.T) error {
return httpError(t, http.StatusBadRequest, `{"errors":[{"type":"BadRequestError","code":"WORKSPACE_ALREADY_EXISTS","message":"Bad Request"}]}`)
},
wantAttempts: []string{"g5.xlarge"},
wantError: true,
},
{
name: "capacity failure tries another candidate",
firstError: func(t *testing.T) error {
return httpError(t, http.StatusServiceUnavailable, `{"errors":[{"type":"ServiceUnavailableError","message":"Service Unavailable"}]}`)
},
wantAttempts: []string{"g5.xlarge", "test.fail.quota"},
},
{
name: "unrelated provider message with duplicate words tries another candidate",
firstError: func(*testing.T) error {
return errors.New("provider capacity pool for duplicate workspace placement is unavailable")
},
wantAttempts: []string{"g5.xlarge", "test.fail.quota"},
},
{
name: "unknown code tries another candidate",
firstError: func(t *testing.T) error {
return httpError(t, http.StatusBadRequest, `{"errors":[{"type":"BadRequestError","code":"OTHER_REASON","message":"Bad Request"}]}`)
},
wantAttempts: []string{"g5.xlarge", "test.fail.quota"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &retryTrackingStore{MockGPUCreateStore: NewMockGPUCreateStore()}
mock.CreateErrorTypes["g5.xlarge"] = tt.firstError(t)

err := RunGPUCreate(context.Background(), terminal.New(), mock, GPUCreateOptions{
Name: "repro",
Count: 1,
Parallel: 1,
Detached: true,
InstanceTypes: []InstanceSpec{{Type: "g5.xlarge"}, {Type: "test.fail.quota"}},
})

if tt.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantAttempts, mock.attemptedTypes)
})
}
}

func (m *MockGPUCreateStore) DeleteWorkspace(workspaceID string) (*entity.Workspace, error) {
m.DeletedWorkspaceIDs = append(m.DeletedWorkspaceIDs, workspaceID)
return nil, nil
Expand Down
Loading