fix(create_project): make the write the branch-readiness gate (#42)
BranchExists returns true before the generated branch is writable, so waitForBranch didn't help and a 2.5s retry budget was too short (branch became writable ~30s post-generate under load in live testing). Drop waitForBranch; let upsertRetry be the gate — retry the write on the transient "branch does not exist" not-found for up to 60s, early-exit on success. Once the first write lands the branch is writable and the rest succeed immediately. Refs #42. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,12 +151,6 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag
|
||||
}
|
||||
result.DefaultBranch = branch
|
||||
|
||||
// Wait for the generated branch to become writable. gitea's /generate is
|
||||
// async: it returns (and serves reads) before the branch ref is committed, so
|
||||
// the first writes 404 "branch does not exist" for a beat. Poll until the
|
||||
// branch is present.
|
||||
t.waitForBranch(ctx, args.Owner, args.Name, branch)
|
||||
|
||||
// Substitute across the WHOLE tree: content in every blob, plus a path rename
|
||||
// for any file whose path carries a placeholder (e.g. cmd/__PROJECT_NAME__/main.go).
|
||||
// A fixed known-files list can't rename directories or cover every templated
|
||||
@@ -192,27 +186,16 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag
|
||||
return textOK(result)
|
||||
}
|
||||
|
||||
// waitForBranch polls until the freshly-generated branch is present, or a short
|
||||
// budget elapses. Best-effort — upsertRetry covers the remaining read-vs-write
|
||||
// consistency window where the branch reads but does not yet write.
|
||||
func (t *CreateProjectFromTemplate) waitForBranch(ctx context.Context, owner, name, branch string) {
|
||||
for i := 0; i < 20; i++ {
|
||||
if ok, err := t.c.BranchExists(ctx, owner, name, branch); ok && err == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// upsertRetry retries UpsertFile through the brief post-generate window where the
|
||||
// branch is readable but writes still 404 "branch does not exist".
|
||||
// upsertRetry is the readiness gate for the async-generate branch race: gitea's
|
||||
// /generate returns (and serves reads) before the branch ref is committed, so the
|
||||
// first writes 404 "branch does not exist" until the initial commit lands (observed
|
||||
// up to ~30s under load). BranchExists is not a usable signal — it reports the
|
||||
// branch present before writes succeed. So the write itself is the probe: retry on
|
||||
// the transient not-found until it takes. Once the first write lands, the branch is
|
||||
// writable and the rest succeed on the first try.
|
||||
func (t *CreateProjectFromTemplate) upsertRetry(ctx context.Context, owner, name, path string, args gitea.UpsertFileArgs) error {
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := 0; i < 60; i++ {
|
||||
if _, err = t.c.UpsertFile(ctx, owner, name, path, args); err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -222,7 +205,7 @@ func (t *CreateProjectFromTemplate) upsertRetry(ctx context.Context, owner, name
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return err
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -69,10 +69,6 @@ func (f *fakeTemplateServer) handler(t *testing.T, tmpl, dest string) http.Handl
|
||||
_, _ = fmt.Fprintf(w, `{"name":%q,"full_name":"mathias/%s","default_branch":%q,"clone_url":"http://gitea.example.com/mathias/%s.git","html_url":"http://gitea.example.com/mathias/%s","template":false}`,
|
||||
dest, dest, f.genBranch, dest, dest)
|
||||
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/repos/mathias/"+dest+"/branches/"):
|
||||
// waitForBranch readiness probe — branch is present.
|
||||
_, _ = fmt.Fprint(w, `{"name":"main","commit":{"id":"c"}}`)
|
||||
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/repos/mathias/"+dest+"/git/trees/"):
|
||||
var entries []string
|
||||
for path := range f.files {
|
||||
|
||||
Reference in New Issue
Block a user