Repository navigation
api: split the Kubernetes substrate store, adapter and pod_ops modules (pure move) - #256
Conversation
Move the `impl K8sStore` methods of `kubernetes/store.rs` into submodules of `kubernetes/store/` grouped by concern: execution rows, started recovery, restore lease, activation fence, pod placement, node inventory, restore source, suspend CAS, and pod tracking cleanup. The row and outcome types stay in the module root, so no field visibility changes and no caller outside the store module changes. Function bodies, SQL text and error messages are unchanged. Moved `pub(super)` methods keep their original scope as `pub(in crate::controllers::substrate::kubernetes)`; private methods called from a sibling submodule or the store tests become `pub(super)` (the original store scope). `super::` paths in moved code gain one level.
Move the contents of `kubernetes/adapter.rs` into submodules of `kubernetes/adapter/`: warm-pool adoption (`pooled`), the suspend flow (`suspend`), node eligibility predicates and node-api endpoint resolution (`node_eligibility`), the fenced delete flow (`teardown`), and the `SubstrateAdapter` trait implementation (`substrate_adapter`). The module root keeps the imports and constants and re-exports the moved items its siblings and tests name, so no caller outside the adapter module changes. The trait implementation moves as one block: a trait impl cannot be split across modules without adding delegating wrappers, which would change function bodies. Function bodies and messages are unchanged. Moved `pub(super)` items keep their original scope as `pub(in crate::controllers::substrate::kubernetes)`; private items used from a sibling submodule or the adapter tests become `pub(super)` (the original adapter scope). `super::` paths in moved code gain one level.
Move the contents of `kubernetes/pod_ops.rs` into submodules of `kubernetes/pod_ops/` by path: pod deletes (`delete`), pod reads and pod IP resolution (`read`), cross-node restore redrive (`redrive`), restore hydration and plans (`restore`), started-recovery pod replacement (`started_recovery`), node service clients (`worker_clients`), the selected-execution wake (`wake`), building the pod create request (`create_request`), and `ensure_pod_exists` with its create-claim helpers (`ensure_pod`). The materialization ownership types, the create-context types and their parser, the kube error mappers, and the structs with private fields shared across these paths stay in the module root, so no field visibility changes. The module root re-exports the moved items its callers and tests name, so no file outside the pod_ops module changes. Function bodies and messages are unchanged. Moved `pub(super)` items keep their original scope as `pub(in crate::controllers::substrate::kubernetes)`; private items used from a sibling submodule or the pod_ops tests become `pub(super)` (the original pod_ops scope).
Review — 2 findingsLow
I'm not counting Sequencing, not a defect: eight other open PRs touch Verified against
I relied on CI for compile, clippy and the test run. I didn't rerun your Kind suites or the per-widening |
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 2 non-blocking findings in the review comment above.
Summary
Mechanical, behavior-preserving split of the three largest files of the api Kubernetes substrate (
crates/libs/api/src/controllers/substrate/kubernetes/) into concern-scoped submodules, extending the existingstore/,adapter/andpod_ops/sibling directories. There is one commit per source file. Function bodies, signatures, SQL text after thesql!locator comment, error/log/metric strings and tests are unchanged; the changed locators and tracing metadata are listed under "Observable changes". No file outside the three modules changes, and the test files themselves are untouched.store.rsadapter.rspod_ops.rsWhat moved where
Each new file starts with a
//!doc naming what it owns and pulls in its parent's namespace withuse super::*;. Names therefore resolve to the same items, traits and macros as in the original module. Items move whole, contiguous and in their original order. Moved inherent methods sit in animplblock per original block.store/(commit 1)The module root keeps the store vocabulary:
K8sStore, every row/outcome/claim type with itsDisplay/FromStr/FromRowimpls, the shared free helpers (classify_activation_route_publication,normalize_pod_uid,record_route_identity_decision,recovery_lease_ttl_secs,emit_started_suspend_recovery_event) and the constructor/accessor methods. These types stay put because their fields are private orpub(super). Moving them would widen every field, and keeping them means the moved methods use them unchanged.impl K8sStoremethods unless noted)store/execution_rows.rssandbox_k8s_executionsrow claims, locks, finishes and reads (execution_pod_name…ensure_active_execution_claim_in,replace_active_execution_claim_if_generation,ensure_active_execution_row,finish_active_execution_if_generation, theabandon_*unmaterialized_create_claim*family,locked_lease_owner_matches,reset_active_execution_to_creating_if_generation,active_execution_is_generation,active_execution_status)store/started_recovery.rsstore/restore_lease.rsbegin_restore_lifecycle, markerless restore lease,release_user_restore_lease,prepare_wake_sandboxstore/activation_fence.rsclear_command_ready_pending,reconcile_activation_fencestore/pod_placement.rsstore/node_inventory.rsupsert_k8s_node_role, free fnis_node_k8s_tombstonedstore/restore_source.rsstore/suspend.rspersist_or_accept_suspend_commit,claim_suspend_execution, backup pending, committed artifact, missing suspending-pod finalizationstore/pod_tracking.rsforget_*pod_tracking*,backfill_missing_pod_uid, failed-recovery pod-absence convergence,clear_pod_tracking_inand the started-suspend recovery helpers it drivesadapter/(commit 2)The module root keeps the imports and constants. It re-exports what siblings, the kubernetes module and the adapter tests name.
adapter/pooled.rsadmitted_pool_pod_placement_admits,apply_pooled_execution_auto_lifecycle_with_client,apply_adopted_per_sandbox_setupadapter/suspend.rsawait_suspend_continuation,suspend_execution_tracked,await_generation_suspend,poll_generation_suspend, RPC retry classifier, published artifact, pod-deletion confirmation,finish_suspend_handoffadapter/node_eligibility.rsNetworkSlotCapacityPreflight, node readiness/schedulability/placement-block/network-slot predicates and preflight,preservable_create_node,try_resolve_node_api_endpointadapter/teardown.rsFenceReadFreshness,FencedTeardown,PodFenceCheck), fenced reads,teardown_execution, lost-tracking classification, no-active-execution retireadapter/substrate_adapter.rsimpl SubstrateAdapter for KubernetesSubstrate, moved as one blockadapter/substrate_adapter.rsis over the ~2,000-line goal. A traitimplcan't span modules, so splitting it further means delegating each trait method to an inherent method elsewhere. That changes function bodies, which this pure move forbids (see open questions).pod_ops/(commit 3)The module root keeps the shared materialization vocabulary:
PodCreateContext/RestoreContext/SandboxPodRequestInputswithparse_pod_create_context,StartedRecoveryOwnership,ActivationAttemptAssignment,PodMaterializationOwnership,EvacuationContext, the kube error mappers, and two structs with private fields used by more than one path (RoutablePodIdentity,StartedRecoveryPodReplacement).pod_ops/delete.rslingering_sandbox_pod_names, UID-preconditioned blocking deletes,delete_pod_and_forget_trackingpod_ops/read.rslookup_sandbox_pod, cached/uncached pod reads,remember_pod_node_placement,resolve_reachable_pod_ip,resolve_pod_ipand its terminal/succeeded-pod helperspod_ops/redrive.rsRedriveOutcome, redrive metric/claim guards (with theirDropimpls),PreconditionReread,await_scheduling_outcome,redrive_unschedulable_restore_cross_node(_recorded)pod_ops/restore.rsSuspendRestorePlan,ExecutionHydrationOutcome(stillpub, re-exported), routability checks,hydrate_restored_execution, suspend/stopped restore planspod_ops/started_recovery.rsreplace_started_recovery_podpod_ops/worker_clients.rspod_ops/wake.rsSelectedWakeOutcome,WakeDispatchStatus,ActivationWakeFailure,SelectedWakeSettings, selected wake pod IP/binding,execute_activation_wake_action,wake_selected_executionpod_ops/create_request.rssandbox_pod_request,pod_create_context_for_claimpod_ops/ensure_pod.rsensure_pod_exists,restore_placement_sample,require_observed_create_claim_in_flight, stale-pod reason and cleanupMove check
python3 move_check.py origin/main(the multiset script from the task) over the whole PR:Every residual line falls into one of these classes:
pub(super)→pub(in crate::controllers::substrate::kubernetes)pub(super)names the parent one level deeper after the move. The explicit path keeps the original scope.pub(super)pub(super)from the child equals the original private scope (the original module and its descendants).super::paths in moved code, one level deeper (super::X→super::super::X,super::super::X→super::super::super::X, one intra-doc link)PublishedLocalArtifact,ActivationLeaseCompletion, …).&self,,) -> bool {,node: &Node,,F,/D,/>(,== target_state.as_str(), …)usere-export lists in the module rootsusestatement's first line as structural.There are no other residual lines. In
git diff --color-moved=zebra --color-moved-ws=allow-indentation-change origin/main...HEAD, everything outside these classes shows as moved except a few short fragments (self.error, a closing brace) where a rewritten visibility line splits a block below git's 20-character moved-block minimum. The multiset check matches those fragments.Visibility changes
No item became
pubthat wasn't already, and no struct field changed visibility. Every change below preserves the item's original scope after it moved one module deeper.pub(super)→pub(in crate::controllers::substrate::kubernetes)(114 lines): everypub(super)item, method, enum or field that moved into a submodule. That covers all movedpub(super)K8sStoremethods; adapterpooled_route_identity_adoption_request,generation_suspend_rpc_should_retry,published_generation_suspend_artifact,suspend_pod_deletion_confirmation,finish_suspend_handoff,NetworkSlotCapacityPreflight,node_schedulable,node_not_placement_blocked,node_services_not_ready,node_network_slot_availableandnetwork_slot_capacity_preflight; and pod_opsRedriveOutcome,SuspendRestorePlan,SelectedWakeOutcome/WakeDispatchStatus/ActivationWakeFailure(with theirpub(super)fields and methods) plus the movedpub(super)KubernetesSubstratemethods.private →
pub(super)(60 items):execution_rows::{execution_pod_name, active_execution_row, insert_active_execution_claim_in, update_active_execution_pod_metadata, lock_active_execution_state_in, update_active_execution_node_name, finish_active_execution_in, identity_binds, finish_fenced_execution_in, finish_active_execution_generation_in, finish_active_execution_generation_attempt_in, lock_sandbox_for_execution_mutation_in, lock_sandbox_execution_lifecycle_in, lock_sandbox_for_execution_lifecycle_in, ensure_active_execution_claim_in},restore_lease::restore_ownership_collision,activation_fence::assign_activation_attempt_in,node_inventory::upsert_k8s_node_role,suspend::suspend_owns_pod_teardown_in,pod_tracking::{clear_pod_tracking_in, recover_markerless_restoring_after_execution_cleanup_in, recover_started_after_suspend_cleanup_in, recover_started_after_failed_recovery_in}.pooled::{pooled_wake_request, wait_for_pooled_split_receipt_capability, pooled_metadata_update_error_is_reclaimable, admitted_pool_pod_placement_admits, apply_pooled_execution_auto_lifecycle_with_client, apply_adopted_per_sandbox_setup},suspend::{await_suspend_continuation, await_generation_suspend, poll_generation_suspend},node_eligibility::preservable_create_node,teardown::{FenceReadFreshness, FencedTeardown, PodFenceCheck, check_pod_fence, teardown_execution}.delete::{K8S_THROTTLE_DELETE_RETRY_ATTEMPTS, retry_kube_delete_throttling, delete_sandbox_pod_with_throttle_retry, lingering_sandbox_pod_names, delete_lingering_prior_generation_pod, delete_sandbox_pod_for_redrive, k8s_blocking_delete_sandbox_pod_for_operation},read::{terminal_cleanup_phase_after_uncached_check, succeeded_pod_publication_grace_remaining},redrive::{PreconditionReread, classify_precondition_reread, await_scheduling_outcome, redrive_unschedulable_restore_cross_node_recorded},restore::{pod_matches_execution_generation, restored_pod_routability, stopped_restore_plan},started_recovery::{started_recovery_replacement_event_details, replace_started_recovery_pod},create_request::{effective_network_policy_for_sandbox, resolve_node_group_placement_for_claim, pod_create_context_for_claim},ensure_pod::restore_placement_sample.Each widening was proven necessary: reverting it alone and running
cargo check -p pplx-space-api --all-targets --all-featuresfails. Two were found by the compiler rather than by name search:admitted_pool_pod_placement_admits(inside a wholly movedimplblock) andFencedTeardown(named only through a type it is a field of).Re-exports in the module roots keep every existing path working. They use the original visibility (
pub(super) use,pub useforExecutionHydrationOutcome, privateusefor module-internal names). Names used only by the module's tests are re-exported under#[cfg(test)]so non-test builds have no unused imports.Observable changes
Code bodies are unchanged, but two compile-time call-site markers derive from
module_path!()/file!()/line!(), and they now reflect each moved item's new module and line. No explicittarget:arguments or locator overrides were added.sql!locatorspplx_space_common_core::sql!prepends/*module_path!():line!()*/to each statement.store.rs, 1 fromadapter.rs(none inpod_ops.rs). Their locator now names the child module and new line, e.g./*pplx_space_api::controllers::substrate::kubernetes::store::suspend:NNN*/instead of…::kubernetes::store:NNN.store.rs(template_network_policy). Its module path is unchanged, but its line shifts from 646 to 656 because of the insertedmoddeclarations.pg_stat_activity, slow-query logs, Datadog DBM samples, and thedb.statementspan attribute.crates/libs/observability/src/sqlx_otel.rs).pg_stat_statements:queryidignores comments, so per-statement aggregation is unaffected.tracingcall sitesThe 117 moved call sites are 109 events and 8
tracing::info_span!spans. The spans are in adapterpooled(3) andsubstrate_adapter(4), and inpod_ops/redrive.rs(1). The three original files had 12 (store), 60 (adapter) and 45 (pod_ops) call sites, and all moved; the module roots keep none. Events per new module:started_recovery2,activation_fence3,node_inventory1,restore_source1,suspend1,pod_tracking4.pooled4,suspend6,teardown8,substrate_adapter35.delete2,read9,redrive13,wake1,create_request4,ensure_pod15.None of the moved code uses
#[instrument]or an explicittarget:.SandboxEvent::emit_withrecords are emitted from the events crate, so their target doesn't change.For both events and spans:
module_path!(), so records now carry the child-module target, e.g.pplx_space_api::controllers::substrate::kubernetes::pod_opsbecomes…::kubernetes::pod_ops::ensure_pod, and…::kubernetes::adapterbecomes…::kubernetes::adapter::substrate_adapter.code.namespace,code.filepathandcode.linenoattributes, and default event names, which embedfile:line.RUST_LOG/EnvFilterdirectives match targets by prefix, so a directive forpplx_space_api,…::substrate::kubernetes, or any of the three original module paths still selects these records.infra/,docs/andscripts/for@target,target=and the module paths (controllers::substrate,substrate::kubernetes,pplx_space_api::controllers,kubernetes::{store,adapter,pod_ops}), then all non-Rust files and Rust string literals repo-wide for the module paths. Thetarget=hits are unrelated (Tempo/Loki-target=all, Terraform-target=, shell/Python variables, netgtw probe targets). No test filters on or asserts a tracing target.Hazard self-check
use super::*, and its only local items are the ones it moved. Those were local to the original module too, so no name now resolves to a different item. The roots' import lists are unchanged.K8sStoreimplements onlyClone, so a private inherent method can't fall back to a same-named trait method. The adapter/pod_ops inherent method names don't collide withSubstrateAdaptermethods.cfggates: the three#[cfg(test)]K8sStoremethods moved with their attributes.#[cfg(test)] mod tests;stays where it was in each root. Checked with--all-features --all-targetsand the default-feature build (where thekubernetesmodule is gated off).include_str!/include_bytes!/env!/sqlx::query!paths ormacro_rules!in the moved code.sql!andtracingcall-site metadata (module_path!/line!) do change; see "Observable changes".Droporder is unchanged.store::k8s_store_postgres_tests) is unchanged, soinstasnapshot names are unchanged.sqlx::query!macros and the repo has no.sqlxoffline data, so there's nothing to regenerate.cargo doc -p pplx-space-api --no-deps --document-private-items --all-featuresgives the same 21 warnings as the base commit, so no intra-doc link broke.Validation
cargo fmt --all --checkcargo clippy -p pplx-space-api --all-targets --all-features --locked -- -D warnings(after every commit)cargo clippy --workspace --all-targets --all-features --locked -- -D warnings(CI clippy tier)cargo clippy -p pplx-space-api --all-targets --locked -- -D warnings(default features,kubernetesmodule gated off)substrate/mod.rsunusedpod_names,fork_operation_executor.rsunconstructedSubstrateNodeId). Identical on base15a2209, so pre-existing; CI does not lint this configuration.SPACE_CI_REQUIRE_TEST_DEPS=1 cargo nextest run -p pplx-space-api --all-features --all-targets --run-ignored all --no-fail-fast(nextest Postgres + Redis setup scripts)store::k8s_store_postgres_testsran and passedcargo test -p pplx-space-api --all-features --doccargo doc -p pplx-space-api --no-deps --document-private-items --all-featuresvs basepython3 move_check.py origin/mainrustfmt --edition 2024 --check scripts/generate-env-contract.rs,scripts/generate-env-contract.sh --check,scripts/check-rust-error-policy.sh(the CI fmt step's guards)pre-commit run --all-files(gitleaks)scripts/check_no_inline_rust_test_modules.py,scripts/source_guards/thin_service_mains.pycrates/libs/observability,crates/apps/*/main.rs); output byte-identical on base, so pre-existing. The otherscripts/source_guards/*.pypass.sqlx::query!macros and no.sqlxdirectory in the repoKind
Environment: the repo Kind stack (
infra/space/kind/scripts/bootstrap.sh, then Tilt throughscripts/tests/smoke-kind-e2e.sh) on a Linux/KVM devbox, with CI's smoke settingsSPACE_KIND_OBSERVABILITY=off SPACE_KIND_TELEMETRY_MODE=datadog.Deployed binary is this branch. HEAD
ca4032bwith a clean tree. The Kind build's source mirror (~/.space-devbox/kind/rust-src/crates/libs/api/src) is byte-identical to the worktree. Thespace-apiDeployment ran imagespace-api:tilt-e1e07d92c1afca65(IDsha256:e1e07d92c1af…, built after the last commit).sha256(/space/bin/space-api-svc)in the running pod was9a176c0b6dfd…, equal to the staged artifact. The binary embeds source paths that exist only on this branch (substrate/kubernetes/pod_ops/ensure_pod.rs,adapter/substrate_adapter.rs,store/pod_tracking.rs, …).Quiet gate. Tilt reported no pending or in-progress builds and no ReplicaSet younger than 180 s, followed by a 120 s settle, before the suite started.
smoke-kind-e2e.sh(bring-up baseline)smoke-kind-typed.sh all, ECW passsmoke-kind-typed.sh all, native passecw.cold_create_environment,ecw.warm_pool_environmentlocal.encrypted_resuspend_preserves_cowClassification of the first-pass non-passes:
ecw.cold_create_environment,ecw.warm_pool_environment: ENVIRONMENTAL. Both were inconclusive because the run lacked the receipts-directory input (SPACE_SMOKE_KIND_COLD_RECEIPTS) thatkind-ci.ymlprovides. Rerun with the CI-equivalent input, both pass.local.encrypted_resuspend_preserves_cow: PRE-EXISTING. The scenario's assertions pass, but its critical cleanup fails: the deleted sandbox's pod is stillRunning60 s after delete. The pod carries adeletionTimestamp(the api issued the delete), and the node reportsFailedKillPod … KillPodSandbox … DeadlineExceeded, i.e. the node runtime fails to tear the VM down. The rerun on this branch failed the same way. I then deployed base15a2209to the same cluster from a separate worktree (binary hash40ca0c0b07ea…, none of the new module paths) and the scenario failed with the identical signature.No CHANGE-INDUCED failures.
Open questions
adapter/substrate_adapter.rs(3,044 lines) holds the wholeSubstrateAdapterimpl. Getting under ~2,000 lines needs thin delegating trait methods whose bodies live in the concern modules, e.g.create_execution(≈650 lines),claim_pooled_execution(≈560) andprepare_pooled_execution(≈300). That changes function bodies, so it's left for a follow-up that allows it.pod_ops/ensure_pod.rs(1,328 lines) is dominated by the single 1,181-lineensure_pod_exists. Splitting it needs body changes.local.encrypted_resuspend_preserves_cowfails its cleanup onmaintoo: a node-sideKillPodSandboxdeadline leaves the pod Terminating. It's unrelated to this PR but worth a separate look.pub(super)items use the explicitpub(in crate::controllers::substrate::kubernetes), as the task asks.pub(in super::super)would be shorter and cause fewer rustfmt reflows, if reviewers prefer it.🤖 Generated with Claude Code