| index 79a6629eb..8c1126e21 100644 |
| --- a/internal/activator/instancekey.go |
| +++ b/internal/activator/instancekey.go |
| @@ -145,12 +145,19 @@ func syncDir(dir string) error { |
| // adminTokenMu. |
| var instanceKeyWrite = func(f *os.File, b []byte) (int, error) { return f.Write(b) } |
| |
| -// instanceKeyBeforeOpen, when set, runs under adminTokenMu after the read |
| -// and the decision and before the ensure opens the file to append or |
| -// settle: a test hook to change the file between the two, so the open's |
| -// own symlink refusal is pinned and not only the read's. nil outside |
| -// tests. |
| -var instanceKeyBeforeOpen func(path string) |
| +// 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 |
| +// 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. |
| +var instanceKeyHooks struct { |
| + beforeOpen func(path string) |
| + replicaGate func() |
| +} |
| |
| func mintInstanceKey() (string, error) { |
| raw := make([]byte, instanceKeyBytes) |
| @@ -241,14 +248,7 @@ func ensureInstanceKeyBeneathNoConfig(dir, name string) (InstanceKeyOutcome, err |
| // mint would use. Every caller names the host around the error ("instance |
| // key for <host>: ..."), so the refusal does not. |
| func (b *ExecBackend) ensureInstanceKeyFor(ctx context.Context, rt registry.Route, p instancePaths) (InstanceKeyOutcome, error) { |
| - if rt.Replica { |
| - return InstanceKeyReplica, nil |
| - } |
| - switch p.layout { |
| - case LayoutFenced, LayoutConflict: |
| - return InstanceKeyFailed, fmt.Errorf("not minted: %s", p.reason) |
| - } |
| - return ensureInstanceKey(ctx, p.dataDir, rt.Host+".env", p.layout == LayoutNested, true, b.configuredKeyFor(rt)) |
| + return b.ensureInstanceKeyAt(ctx, rt, p, true, "minted") |
| } |
| |
| // BackfillInstanceKey is the startup backfill's step for one row: a key is |
| @@ -260,15 +260,34 @@ func (b *ExecBackend) ensureInstanceKeyFor(ctx context.Context, rt registry.Rout |
| // The agent's backfill names the host when it logs the error, so the |
| // refusal does not. |
| func (b *ExecBackend) BackfillInstanceKey(ctx context.Context, rt registry.Route) (InstanceKeyOutcome, error) { |
| + return b.ensureInstanceKeyAt(ctx, rt, b.pathsFor(rt), false, "backfilled") |
| +} |
| + |
| +// ensureInstanceKeyAt is the two entry points' one guard: a replica row is |
| +// never minted; a fenced or conflicting row is refused on p, the paths the |
| +// mint would use, with verb naming which caller refused ("not minted", |
| +// "not backfilled"); every other row is ensureInstanceKey on p's layout, |
| +// with the bucket reader for rt. |
| +func (b *ExecBackend) ensureInstanceKeyAt(ctx context.Context, rt registry.Route, p instancePaths, create bool, verb string) (InstanceKeyOutcome, error) { |
| if rt.Replica { |
| return InstanceKeyReplica, nil |
| } |
| - p := b.pathsFor(rt) |
| switch p.layout { |
| case LayoutFenced, LayoutConflict: |
| - return InstanceKeyFailed, fmt.Errorf("not backfilled: %s", p.reason) |
| + return InstanceKeyFailed, fmt.Errorf("not %s: %s", verb, p.reason) |
| + } |
| + return ensureInstanceKey(ctx, p.dataDir, rt.Host+".env", p.layout == LayoutNested, create, b.configuredKeyFor(rt)) |
| +} |
| + |
| +// openOwnEnv opens an own env file, <host>.env under dir: through |
| +// instancefs when beneath (a nested row's tenant-writable directory), else |
| +// through openOwnEnvNoFollow. Both refuse a link or a FIFO in the open. |
| +// The one place the two layouts' opens meet. |
| +func openOwnEnv(dir, name string, beneath bool, flags int, perm os.FileMode) (*os.File, error) { |
| + if beneath { |
| + return instancefs.OpenBeneath(dir, name, flags, perm) |
| } |
| - return ensureInstanceKey(ctx, p.dataDir, rt.Host+".env", p.layout == LayoutNested, false, b.configuredKeyFor(rt)) |
| + return openOwnEnvNoFollow(filepath.Join(dir, name), flags, perm) |
| } |
| |
| // openInstanceEnv opens an own env file for append. Only a create passes |
| @@ -279,10 +298,7 @@ func openInstanceEnv(dir, name string, beneath, create bool) (*os.File, error) { |
| if create { |
| flags, perm = flags|os.O_CREATE, 0o600 |
| } |
| - if beneath { |
| - return instancefs.OpenBeneath(dir, name, flags, perm) |
| - } |
| - return openOwnEnvNoFollow(filepath.Join(dir, name), flags, perm) |
| + return openOwnEnv(dir, name, beneath, flags, perm) |
| } |
| |
| // errOwnEnvSymlink is a flat row's own env file found to be a symlink. The |
| @@ -328,12 +344,19 @@ func readOwnEnv(path string) ([]byte, error) { |
| } |
| |
| // settleInstanceEnv is the durability step every successful ensure ends |
| -// with: 0600 on the descriptor, then the file, then its directory. Caller |
| -// holds adminTokenMu. |
| +// with: 0600 on the descriptor, then syncInstanceEnv. Caller holds |
| +// adminTokenMu. |
| func settleInstanceEnv(f *os.File, dir, path string) error { |
| if err := f.Chmod(0o600); err != nil { |
| return fmt.Errorf("chmod %s: %w", path, err) |
| } |
| + return syncInstanceEnv(f, dir, path) |
| +} |
| + |
| +// syncInstanceEnv fsyncs an own env file, then its directory: the two |
| +// syncs every durability step ends with, the claim's settle included. |
| +// Caller holds adminTokenMu. |
| +func syncInstanceEnv(f *os.File, dir, path string) error { |
| if err := instanceKeySync.file(f); err != nil { |
| return fmt.Errorf("sync %s: %w", path, err) |
| } |
| @@ -389,8 +412,8 @@ func ensureInstanceKey(ctx context.Context, dir, name string, beneath, create bo |
| // Present is not a no-op: a mint whose sync failed left these |
| // bytes behind, and this call is the retry that makes them |
| // durable before anything is started with them. |
| - if instanceKeyBeforeOpen != nil { |
| - instanceKeyBeforeOpen(path) |
| + if instanceKeyHooks.beforeOpen != nil { |
| + instanceKeyHooks.beforeOpen(path) |
| } |
| f, err := openInstanceEnv(dir, name, beneath, false) |
| if err != nil { |
| @@ -425,8 +448,8 @@ func ensureInstanceKey(ctx context.Context, dir, name string, beneath, create bo |
| if err != nil { |
| return InstanceKeyFailed, fmt.Errorf("mint instance key for %s: %w", path, err) |
| } |
| - if instanceKeyBeforeOpen != nil { |
| - instanceKeyBeforeOpen(path) |
| + if instanceKeyHooks.beforeOpen != nil { |
| + instanceKeyHooks.beforeOpen(path) |
| } |
| f, err := openInstanceEnv(dir, name, beneath, create) |
| if errors.Is(err, os.ErrNotExist) && !create { |
| @@ -492,16 +515,9 @@ var errInstanceEnvNeedsWrite = errors.New("activator: the instance env file need |
| // row is a replica: EnsureClaimAdminToken). |
| func (b *ExecBackend) settleClaimInstanceEnv(rt registry.Route) error { |
| p := b.pathsFor(rt) |
| - name := rt.Host + ".env" |
| adminTokenMu.Lock() |
| defer adminTokenMu.Unlock() |
| - var f *os.File |
| - var err error |
| - if p.layout == LayoutNested { |
| - f, err = instancefs.OpenBeneath(p.dataDir, name, os.O_RDONLY, 0) |
| - } else { |
| - f, err = openOwnEnvNoFollow(p.envFile, os.O_RDONLY, 0) |
| - } |
| + f, err := openOwnEnv(p.dataDir, rt.Host+".env", p.layout == LayoutNested, os.O_RDONLY, 0) |
| if err != nil { |
| return fmt.Errorf("open %s: %w", p.envFile, err) |
| } |
| @@ -513,11 +529,5 @@ func (b *ExecBackend) settleClaimInstanceEnv(rt registry.Route) error { |
| if st.Mode().Perm() != 0o600 { |
| return errInstanceEnvNeedsWrite |
| } |
| - if err := instanceKeySync.file(f); err != nil { |
| - return fmt.Errorf("sync %s: %w", p.envFile, err) |
| - } |
| - if err := instanceKeySync.dir(p.dataDir); err != nil { |
| - return fmt.Errorf("sync %s: %w", p.dataDir, err) |
| - } |
| - return nil |
| + return syncInstanceEnv(f, p.dataDir, p.envFile) |
| } |