X Tutup
Skip to content

Commit fb12f41

Browse files
authored
Merge pull request cli#181 from github/jg/poll-on-async-creation
ghcs create: poll for codespaces that are being retried by the server
2 parents a3c900c + 186b90b commit fb12f41

File tree

2 files changed

+89
-8
lines changed

2 files changed

+89
-8
lines changed

cmd/ghcs/create.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,15 @@ func create(opts *createOptions) error {
8181
return errors.New("there are no available machine types for this repository")
8282
}
8383

84-
log.Println("Creating your codespace...")
85-
86-
codespace, err := apiClient.CreateCodespace(ctx, userResult.User, repository, machine, branch, locationResult.Location)
84+
log.Print("Creating your codespace...")
85+
codespace, err := apiClient.CreateCodespace(ctx, log, &api.CreateCodespaceParams{
86+
User: userResult.User.Login,
87+
RepositoryID: repository.ID,
88+
Branch: branch,
89+
Machine: machine,
90+
Location: locationResult.Location,
91+
})
92+
log.Print("\n")
8793
if err != nil {
8894
return fmt.Errorf("error creating codespace: %w", err)
8995
}

internal/api/api.go

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"net/http"
3737
"strconv"
3838
"strings"
39+
"time"
3940

4041
"github.com/opentracing/opentracing-go"
4142
)
@@ -208,6 +209,10 @@ type getCodespaceTokenResponse struct {
208209
RepositoryToken string `json:"repository_token"`
209210
}
210211

212+
// ErrNotProvisioned is returned by GetCodespacesToken to indicate that the
213+
// creation of a codespace is not yet complete and that the caller should try again.
214+
var ErrNotProvisioned = errors.New("codespace not provisioned")
215+
211216
func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName string) (string, error) {
212217
reqBody, err := json.Marshal(getCodespaceTokenRequest{true})
213218
if err != nil {
@@ -236,6 +241,10 @@ func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName s
236241
}
237242

238243
if resp.StatusCode != http.StatusOK {
244+
if resp.StatusCode == http.StatusUnprocessableEntity {
245+
return "", ErrNotProvisioned
246+
}
247+
239248
return "", jsonErrorResponse(b)
240249
}
241250

@@ -395,20 +404,83 @@ func (a *API) GetCodespacesSKUs(ctx context.Context, user *User, repository *Rep
395404
return response.SKUs, nil
396405
}
397406

398-
type createCodespaceRequest struct {
407+
// CreateCodespaceParams are the required parameters for provisioning a Codespace.
408+
type CreateCodespaceParams struct {
409+
User string
410+
RepositoryID int
411+
Branch, Machine, Location string
412+
}
413+
414+
type logger interface {
415+
Print(v ...interface{}) (int, error)
416+
Println(v ...interface{}) (int, error)
417+
}
418+
419+
// CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
420+
// fails to create.
421+
func (a *API) CreateCodespace(ctx context.Context, log logger, params *CreateCodespaceParams) (*Codespace, error) {
422+
codespace, err := a.startCreate(
423+
ctx, params.User, params.RepositoryID, params.Machine, params.Branch, params.Location,
424+
)
425+
if err != errProvisioningInProgress {
426+
return codespace, err
427+
}
428+
429+
// errProvisioningInProgress indicates that codespace creation did not complete
430+
// within the GitHub API RPC time limit (10s), so it continues asynchronously.
431+
// We must poll the server to discover the outcome.
432+
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
433+
defer cancel()
434+
435+
ticker := time.NewTicker(1 * time.Second)
436+
defer ticker.Stop()
437+
438+
for {
439+
select {
440+
case <-ctx.Done():
441+
return nil, ctx.Err()
442+
case <-ticker.C:
443+
log.Print(".")
444+
token, err := a.GetCodespaceToken(ctx, params.User, codespace.Name)
445+
if err != nil {
446+
if err == ErrNotProvisioned {
447+
// Do nothing. We expect this to fail until the codespace is provisioned
448+
continue
449+
}
450+
451+
return nil, fmt.Errorf("failed to get codespace token: %w", err)
452+
}
453+
454+
codespace, err = a.GetCodespace(ctx, token, params.User, codespace.Name)
455+
if err != nil {
456+
return nil, fmt.Errorf("failed to get codespace: %w", err)
457+
}
458+
459+
return codespace, nil
460+
}
461+
}
462+
}
463+
464+
type startCreateRequest struct {
399465
RepositoryID int `json:"repository_id"`
400466
Ref string `json:"ref"`
401467
Location string `json:"location"`
402468
SkuName string `json:"sku_name"`
403469
}
404470

405-
func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repository, sku, branch, location string) (*Codespace, error) {
406-
requestBody, err := json.Marshal(createCodespaceRequest{repository.ID, branch, location, sku})
471+
var errProvisioningInProgress = errors.New("provisioning in progress")
472+
473+
// startCreate starts the creation of a codespace.
474+
// It may return success or an error, or errProvisioningInProgress indicating that the operation
475+
// did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller
476+
// must poll the server to learn the outcome.
477+
func (a *API) startCreate(ctx context.Context, user string, repository int, sku, branch, location string) (*Codespace, error) {
478+
requestBody, err := json.Marshal(startCreateRequest{repository, branch, location, sku})
407479
if err != nil {
408480
return nil, fmt.Errorf("error marshaling request: %w", err)
409481
}
410482

411-
req, err := http.NewRequest(http.MethodPost, githubAPI+"/vscs_internal/user/"+user.Login+"/codespaces", bytes.NewBuffer(requestBody))
483+
req, err := http.NewRequest(http.MethodPost, githubAPI+"/vscs_internal/user/"+user+"/codespaces", bytes.NewBuffer(requestBody))
412484
if err != nil {
413485
return nil, fmt.Errorf("error creating request: %w", err)
414486
}
@@ -425,8 +497,11 @@ func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repos
425497
return nil, fmt.Errorf("error reading response body: %w", err)
426498
}
427499

428-
if resp.StatusCode > http.StatusAccepted {
500+
switch {
501+
case resp.StatusCode > http.StatusAccepted:
429502
return nil, jsonErrorResponse(b)
503+
case resp.StatusCode == http.StatusAccepted:
504+
return nil, errProvisioningInProgress // RPC finished before result of creation known
430505
}
431506

432507
var response Codespace

0 commit comments

Comments
 (0)
X Tutup