carlos / platform Public

release: a retry's fresh stamp is judged against the guarded pointer

Final review F7 (Plan 1 D1). PromoteOnLadder checked timestamp freshness against its first pointer read; a concurrent retry that renewed the same version before sg.promote's read passed the version guard and the signer could reuse that pointer's second-truncated PromotedAt, leaving a refusing box blocked behind a successful retry.

With expectVersion set, sg.promote now waits (ctx-aware, capped at 2 s) until now-truncated is strictly after the PromotedAt of the pointer whose etag guards the write, and refuses if the cap is not enough (clock skew) rather than write a stamp that is not newer. The earlier wait in PromoteOnLadder is gone.

Tests: TestPromoteRetryStampsAfterTheGuardedPointer, TestConcurrentRetriesNeverShareAStamp (both fail with the wait disabled).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 959f752499625636035a9c30bba27f18a5b7a7ff parent 595ac83
2 files changed, +112 −13
  • internal/release/release.go +36 −13
  • internal/release/release_test.go +76 −0
diff --git a/internal/release/release.go b/internal/release/release.go
index a542170f6..ac7667a6f 100644
--- a/internal/release/release.go
+++ b/internal/release/release.go
@@ -769,6 +769,17 @@ func (sg Signer) promote(ctx context.Context, s Store, acct, app, channel, versi
if expectVersion != "" && old.Version != expectVersion {
return fmt.Errorf("promote --retry renews a promotion: %s now names %s, not %s", channel, old.Version, expectVersion)
}
+ // A retry's whole effect is a new PromotedAt, so freshness is checked
+ // HERE, against the pointer whose etag guards the write, not against an
+ // earlier read: a concurrent retry that renewed the same version in
+ // between would otherwise be re-stamped with its own second-truncated
+ // time, and a box that refused that promotion would stay blocked behind
+ // a "successful" retry (final review F7).
+ if expectVersion != "" {
+ if err := waitPastPromotedAt(ctx, channel, old.PromotedAt); err != nil {
+ return err
+ }
+ }
if authorize != nil {
// Between the pointer read and the conditional write, so the write
// lands only if the pointer is still what was authorized against
@@ -802,6 +813,29 @@ func (sg Signer) promote(ctx context.Context, s Store, acct, app, channel, versi
return err
}
+// waitPastPromotedAt returns once the stamp signPointer would produce (now,
+// truncated to the second) is strictly after prev, waiting into the next
+// second if it must. The wait is bounded at 2 s: a prev further ahead than
+// that is clock skew, and it refuses rather than write a stamp that is not
+// newer.
+func waitPastPromotedAt(ctx context.Context, channel string, prev time.Time) error {
+ if time.Now().UTC().Truncate(time.Second).After(prev) {
+ return nil
+ }
+ t := time.NewTimer(min(time.Until(prev.Add(time.Second)), 2*time.Second))
+ select {
+ case <-ctx.Done():
+ t.Stop()
+ return ctx.Err()
+ case <-t.C:
+ }
+ if !time.Now().UTC().Truncate(time.Second).After(prev) {
+ return fmt.Errorf("promote --retry: %s was promoted at %s, ahead of this clock; nothing was written",
+ channel, prev.Format(time.RFC3339))
+ }
+ return nil
+}
+
// PromoteOptions carries what the ladder needs to know about a promotion
// beyond "which version, which channel".
type PromoteOptions struct {
@@ -947,19 +981,8 @@ func (sg Signer) PromoteOnLadder(ctx context.Context, s Store, acct, app, channe
if opt.Retry {
res.From = res.To
rec := Promotion{From: res.To, To: res.To, Actor: opt.Actor, Retry: true}
- if !time.Now().UTC().Truncate(time.Second).After(p.PromotedAt) {
- // Wait into the next second, bounded: a PromotedAt further
- // ahead than that is clock skew, and hanging on it would
- // help nobody. The write below is guarded either way.
- wait := min(time.Until(p.PromotedAt.Add(time.Second)), 2*time.Second)
- t := time.NewTimer(wait)
- select {
- case <-ctx.Done():
- t.Stop()
- return res, ctx.Err()
- case <-t.C:
- }
- }
+ // The version guard and the fresh-stamp wait both live in
+ // sg.promote, on the pointer its write is conditional on.
if err := sg.promote(ctx, s, acct, app, channel, version, opt.Authorize, version, rec); err != nil {
return res, err
}
diff --git a/internal/release/release_test.go b/internal/release/release_test.go
index c868996d2..3a208eada 100644
--- a/internal/release/release_test.go
+++ b/internal/release/release_test.go
@@ -882,6 +882,82 @@ func TestPromoteRetryWriteRefusesWhenTheChannelMoved(t *testing.T) {
}
}
+// Final review F7: the guarded write's own pointer was stamped this very
+// second. The retry must still produce a strictly later PromotedAt.
+func TestPromoteRetryStampsAfterTheGuardedPointer(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("a"))
+ sg := RootSigner(priv)
+ if err := sg.Promote(ctx, s, "bab", "console", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ before, _ := Channel(ctx, s, pub, "bab", "console", "canary/x")
+ if err := sg.promote(ctx, s, "bab", "console", "canary/x", "v1", nil, "v1", Promotion{Retry: true}); err != nil {
+ t.Fatalf("retry: %v", err)
+ }
+ after, _ := Channel(ctx, s, pub, "bab", "console", "canary/x")
+ if !after.PromotedAt.After(before.PromotedAt) {
+ t.Fatalf("retry stamped %v over %v, want strictly later", after.PromotedAt, before.PromotedAt)
+ }
+}
+
+// getHook runs onGet before every Get, so a test can land a competing write
+// between a caller's first pointer read and its guarded one.
+type getHook struct {
+ Store
+ onGet func(key string)
+}
+
+func (h *getHook) Get(ctx context.Context, key string) ([]byte, string, error) {
+ h.onGet(key)
+ return h.Store.Get(ctx, key)
+}
+
+// Final review F7, the race itself: PromoteOnLadder's first read sees an old
+// pointer (no wait needed), then a concurrent retry renews it before the
+// guarded read. The stamp is judged against the pointer the write is
+// conditional on, so it is still strictly later than the competitor's.
+func TestConcurrentRetriesNeverShareAStamp(t *testing.T) {
+ inner := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, inner, "bab", "console", "v1", adoptPlatform, []byte("a"))
+ sg := RootSigner(priv)
+ if err := sg.Promote(ctx, inner, "bab", "console", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ time.Sleep(1100 * time.Millisecond) // the first read's pointer is a second old
+ key := ChannelKey("bab", "console", "canary/x")
+ var competitor time.Time
+ gets := 0
+ s := &getHook{Store: inner}
+ s.onGet = func(k string) {
+ if k != key {
+ return
+ }
+ gets++
+ if gets == 2 { // sg.promote's guarded read
+ if err := sg.promote(ctx, inner, "bab", "console", "canary/x", "v1", nil, "v1", Promotion{Retry: true}); err != nil {
+ t.Errorf("competing retry: %v", err)
+ }
+ cp, _ := Channel(ctx, inner, pub, "bab", "console", "canary/x")
+ competitor = cp.PromotedAt
+ }
+ }
+ if _, err := sg.PromoteOnLadder(ctx, s, "bab", "console", "canary/x", "v1", PromoteOptions{Retry: true}); err != nil {
+ t.Fatalf("retry: %v", err)
+ }
+ if gets < 2 || competitor.IsZero() {
+ t.Fatalf("the competing retry never ran (gets=%d)", gets)
+ }
+ after, _ := Channel(ctx, inner, pub, "bab", "console", "canary/x")
+ if !after.PromotedAt.After(competitor) {
+ t.Fatalf("retry stamped %v, the competitor %v: want strictly later", after.PromotedAt, competitor)
+ }
+}
+
func TestPromoteRetryNeverChangesTheVersion(t *testing.T) {
s := NewDirStore(t.TempDir())
ctx := context.Background()