carlos / platform Public

activator: the two-phase ensure's unasked-mint deferral is pinned by a test, and the slow-read test waits for host A to be inside its read before host B starts.

Re-review 2 found the deferral for a mint that looks next only under the
lock (a key seen by the first read and gone by the locked one, or a
keyless file appearing after the backfill's first read) survived having
its condition disabled: no test reached it. instanceKeyHooks gains
betweenPhases, nil in production, run after the first read and before
adminTokenMu, and TestAMintThatLooksNextOnlyUnderTheLockDefers drives
both cases: deferred, the bucket unread, the file untouched. The
slow-read test replaced its 50 ms sleep with a signal from host A's
reader, so a loaded box cannot let host B finish first and pass without
proving anything.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 114ef7412b8138399bab566397dae35f6fc62e4c parent 5fe080b
2 files changed, +70 −8
  • internal/activator/instancekey.go +14 −7
  • internal/activator/instancekey_delivery_test.go +56 −1
diff --git a/internal/activator/instancekey.go b/internal/activator/instancekey.go
index 9573777eb..b683e65c3 100644
--- a/internal/activator/instancekey.go
+++ b/internal/activator/instancekey.go
@@ -145,18 +145,22 @@ func syncDir(dir string) error {
// adminTokenMu.
var instanceKeyWrite = func(f *os.File, b []byte) (int, error) { return f.Write(b) }
-// instanceKeyHooks are the two test seams on the launch path's hot path,
-// both nil in production; the sync and write seams above are what the
+// instanceKeyHooks are the three test seams on the launch path's hot path,
+// all nil in production; the sync and write seams above are what the
// durability tests need and stay on their own. beforeOpen runs under
// adminTokenMu after the read and the decision and before the ensure
// opens the file to append or settle, so a test can change the file
// between the two and pin the open's own symlink refusal, not only the
-// read's. replicaGate runs at the start of every replica gate, after the
-// env was read, so a test can change the env files between the read and
-// the gate, as a concurrent config sync can.
+// read's. betweenPhases runs after the ensure's first read (and its
+// bucket read, if any) and before it takes adminTokenMu, so a test can
+// change the files between the two phases as a concurrent writer can.
+// replicaGate runs at the start of every replica gate, after the env was
+// read, so a test can change the env files between the read and the
+// gate, as a concurrent config sync can.
var instanceKeyHooks struct {
- beforeOpen func(path string)
- replicaGate func()
+ beforeOpen func(path string)
+ betweenPhases func(path string)
+ replicaGate func()
}
func mintInstanceKey() (string, error) {
@@ -447,6 +451,9 @@ func ensureInstanceKey(ctx context.Context, dir, name string, beneath, create bo
}
}
}
+ if instanceKeyHooks.betweenPhases != nil {
+ instanceKeyHooks.betweenPhases(path)
+ }
// Phase two, under the lock: the decision, from this read only.
adminTokenMu.Lock()
defer adminTokenMu.Unlock()
diff --git a/internal/activator/instancekey_delivery_test.go b/internal/activator/instancekey_delivery_test.go
index 5b5b0f97b..35ffc7d9d 100644
--- a/internal/activator/instancekey_delivery_test.go
+++ b/internal/activator/instancekey_delivery_test.go
@@ -192,8 +192,10 @@ func TestFirstStartWaitsForTheConfiguredKey(t *testing.T) {
func TestASlowBucketReadForOneHostDoesNotBlockAnothersMint(t *testing.T) {
b := &ExecBackend{DataDir: t.TempDir()}
release := make(chan struct{})
+ entered := make(chan struct{})
b.ConfiguredKey = func(ctx context.Context, rt registry.Route) (string, error) {
if rt.Host == "a.example.com" {
+ close(entered)
select {
case <-release:
case <-ctx.Done():
@@ -214,9 +216,13 @@ func TestASlowBucketReadForOneHostDoesNotBlockAnothersMint(t *testing.T) {
_, err := b.ensureInstanceKeyFor(context.Background(), a, b.pathsFor(a))
aDone <- err
}()
+ select {
+ case <-entered: // A is inside its bucket read before B starts
+ case <-time.After(2 * time.Second):
+ t.Fatal("host A never reached its bucket read")
+ }
bDone := make(chan error, 1)
go func() {
- time.Sleep(50 * time.Millisecond) // let A reach its read first
_, err := b.ensureInstanceKeyFor(context.Background(), bb, b.pathsFor(bb))
bDone <- err
}()
@@ -304,3 +310,52 @@ func TestAKeyThatAppearsDuringTheBucketReadIsKept(t *testing.T) {
})
}
}
+
+// The other side of the two phases: a mint that looks next only under the
+// lock has no bucket verdict, and defers rather than mint on nothing. The
+// first read saw a key in the own file (or, on the backfill, no file at
+// all), so the bucket was not asked; by the locked read the key is gone
+// (or a keyless file has appeared). Nothing is minted, the bucket is
+// still unasked, and the next attempt asks it.
+func TestAMintThatLooksNextOnlyUnderTheLockDefers(t *testing.T) {
+ key := strings.Repeat("K", 43) + "="
+ for _, tc := range []struct {
+ name string
+ before string // the own file at the first read; "" is no file
+ after string // the own file at the locked read
+ create bool
+ }{
+ {"a key seen by the first read is gone", "X=1\n" + InstanceKeyVar + "=" + key + "\n", "X=1\n", true},
+ {"a keyless file appears after the backfill's first read", "", "X=1\n", false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ b := &ExecBackend{DataDir: t.TempDir()}
+ bucket := &bucketKey{}
+ b.ConfiguredKey = bucket.read
+ rt := registry.Route{Host: "shop.example.com", Kind: "instance"}
+ own := filepath.Join(b.DataDir, rt.Host+".env")
+ if tc.before != "" {
+ if err := os.WriteFile(own, []byte(tc.before), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ instanceKeyHooks.betweenPhases = func(string) {
+ if err := os.WriteFile(own, []byte(tc.after), 0o600); err != nil {
+ t.Error(err)
+ }
+ }
+ t.Cleanup(func() { instanceKeyHooks.betweenPhases = nil })
+ got, err := b.ensureInstanceKeyAt(context.Background(), rt, b.pathsFor(rt), tc.create, "minted")
+ if got != InstanceKeyFailed || !errors.Is(err, ErrInstanceKeyDeferred) {
+ t.Fatalf("ensure = %v, %v; want deferred", got, err)
+ }
+ if bucket.reads != 0 {
+ t.Fatalf("the bucket was read %d times, want 0", bucket.reads)
+ }
+ raw, _ := os.ReadFile(own)
+ if string(raw) != tc.after {
+ t.Fatalf("own file changed by the deferred ensure (%d bytes, want %d)", len(raw), len(tc.after))
+ }
+ })
+ }
+}