Unify entity-store full scans behind a bounded paged primitive
Cleanup trailing #871 and #872 (Jeff Casimir's startup-stall fixes), capturing the follow-on work so it doesn't fall on a community contributor.
Context: a coordinator on a self-hosted node parked in starting entity migration for ~7.5h after a restart, because the startup maintenance phase reads the entire entity keyspace with a single unbounded client.Get(prefix, WithPrefix()), and on an etcd with bloated MVCC history that read stalled indefinitely. It runs before the edge listener binds, so the node stayed dark while the app containers kept running fine. #871 wraps the phase in a 2-min timeout (the net); #872 paginates two of the reads (the fix). Both are good and merging.
1. Lower-layer paged scan
#872 adds listEntitiesPaged as a package helper in pkg/entity/scan.go over a raw *clientv3.Client. The principled home is a method on EtcdStore (a paged scan / iterator), so every full-keyspace read inherits bounding instead of each call site open-coding Get(WithPrefix()). Five such reads in the startup path: MigrateEntityStore, MigrateShortIds (main scan + unique-key set), and reindex's ListAllEntityIDs (store.go:1790) plus its CleanupStale collection scan (reindex.go:98). #872 covers the first two; the rest stay unbounded. Route them all through one primitive.
While in scan.go: the page cursor indexes Kvs[-1] if a page ever returns More == true with empty Kvs — unreachable with WithLimit > 0 today, but a len == 0 break future-proofs it.
2. Reindex timeout nesting
#871 passes its 2-min maintCtx into checkAndReindex, which already self-bounds reindex at 5 min (coordinate.go:1536). The inner deadline now inherits the outer, so reindex — heaviest step, running last — gets min(remaining-of-2min, 5min) and never its full budget. Decide: exclude reindex from the shared ceiling and keep its 5 min, or collapse to one phase budget and drop the dead inner timeout.
Root cause underneath both (untouched by either PR)
etcd compaction falling behind (MVCC bloat, self-reinforced by the restart loop). Even fully bounded and paged, a chronically-behind cluster skips migration every boot, just quickly instead of hanging. Compaction health is the real lever if it recurs.
Safety note
Skipping these scans indefinitely is safe — Entity.UnmarshalCBOR (entity.go:130-135) migrates old-format entities on read, so MigrateEntityStore is write-back, not a correctness gate. Cost of skipping is the per-read migration tax and un-backfilled short-ids (short-id lookups degrade, full IDs work), not breakage.