Skip to content

node-ctl: split facets.rs into facet and local-state modules (pure move) - #563

Merged
ac-pplx merged 1 commit into
mainfrom
ac/split-node-ctl-facets
Oct 16, 2026
Merged

ac-pplx merged 1 commit into
mainfrom
ac/split-node-ctl-facets

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

crates/libs/node/ctl/server/src/facets.rs (2,765 code lines) holds node-ctl's facet registry and dispatch, and next to them every facet's implementation. The facets share nothing but the dispatch and a few local-state readers. This keeps the registry, dispatch and shared publish types in facets.rs and moves the facets, and the local-state reads they share, into child modules. facets.rs drops to 413 code lines; the largest new file is local_state.rs at 539.

This is a pure move, generated by scripts/split_rust_module.py (#562) from the spec below and proved by its verify. It is part of splitting the largest production modules into smaller ones with one job each.

What moved where

Module Holds Code lines
network_policy The network-policy facet: inbound refresh and apply, and outbound publish. 417
template_desired_state The template desired-state facet and the template base-snapshot policy refresh. 289
sandbox_desired_state The sandbox desired-state facet and the local desired-state apply it drives. 399
cluster_deleted_eviction Which local sandboxes the cluster has deleted may be evicted, and how fast: the per-sync budget, backoff and candidate selection. The eviction loop itself runs in the sandbox desired-state facet. 87
port_exposure Reconciliation of a sandbox's exposed ports, including the loopback bridges behind them. 398
inventory_publish Outbound publishes of sandbox and template inventory, node status, pending backups and auto-lifecycle delete completions. 256
local_state Reads of the node's local sandbox and template state that the facets reconcile and publish. 539
facets.rs (parent) the facet registry and dispatch, and the publish types and helpers several facets share 413

How to check it

The spec (item names, not lines):

file = "crates/libs/node/ctl/server/src/facets.rs"

[[module]]
name = "network_policy"
doc = "The network-policy facet: inbound refresh and apply, and outbound publish."
items = [
  "struct LocalSandboxNetworkPolicies", "impl LocalSandboxNetworkPolicies", "enum SandboxPolicyReconcileAction",
  "fn split_network_policy_items", "fn network_policy_state_requests", "fn refresh_network_policy", "fn apply_network_policy_state",
  "fn fetch_local_sandbox_network_policies", "fn fetch_local_sandbox_network_policies_individually",
  "fn should_fallback_to_individual_policy_fetch", "fn sandbox_policy_reconcile_action",
  "fn sandbox_policy_needs_apply", "fn apply_decision_from_proto", "fn apply_decision_counts_as_applied",
  "fn apply_template_policy", "fn apply_sandbox_policy", "fn publish_network_policy",
  "fn resolve_network_policy_scope", "fn resolve_network_policy_publish_scope",
]

[[module]]
name = "template_desired_state"
doc = "The template desired-state facet and the template base-snapshot policy refresh."
items = ["fn refresh_template_base_snapshot_policy", "fn refresh_template_desired_state"]

[[module]]
name = "sandbox_desired_state"
doc = "The sandbox desired-state facet and the local desired-state apply it drives."
items = [
  "fn sandbox_desired_reconcile_action", "fn refresh_sandbox_desired_state", "enum LocalDesiredStateRpc", "impl LocalDesiredStateRpc",
  "impl std::fmt::Display for LocalDesiredStateRpc", "enum LocalDesiredStateApplyError",
  "impl LocalDesiredStateApplyError", "fn apply_sandbox_snapshot_desired",
  "fn apply_sandbox_auto_lifecycle_desired", "fn handle_local_desired_state_apply",
]

[[module]]
name = "cluster_deleted_eviction"
doc = "Which local sandboxes the cluster has deleted may be evicted, and how fast: the per-sync budget, backoff and candidate selection. The eviction loop itself runs in the sandbox desired-state facet."
items = [
  "const CLUSTER_DELETED_*", "enum ClusterDeletedEvictDisposition", "fn cluster_deleted_evict_refusal_disposition",
  "static CLUSTER_DELETED_EVICT_BACKOFF_UNTIL", "fn select_cluster_deleted_evict_candidates",
  "fn local_row_is_cluster_deleted_reclaimable",
]

[[module]]
name = "port_exposure"
doc = "Reconciliation of a sandbox's exposed ports, including the loopback bridges behind them."
items = [
  "enum PortExposureRpc", "impl PortExposureRpc", "impl std::fmt::Display for PortExposureRpc",
  "fn sbx_mgr_desired_state_apply_is_terminal", "enum SandboxPortExposureApplyError",
  "impl SandboxPortExposureApplyError", "struct SandboxPortExposureApplyOutcome",
  "trait SandboxPortExposureApplier", "impl SandboxPortExposureApplier for *",
  "struct PortExposureReconcileMode", "fn reconcile_sandbox_port_exposures",
  "fn apply_sandbox_port_exposure_desired", "enum OpenLoopbackBridgeError", "struct OpenLoopbackBridgeOutcome",
  "fn open_loopback_bridge_for_exposure", "fn close_loopback_bridge_for_exposure",
  "fn port_protocol_label", "fn port_desired_state_label",
]

[[module]]
name = "inventory_publish"
doc = "Outbound publishes of sandbox and template inventory, node status, pending backups and auto-lifecycle delete completions."
items = [
  "fn publish_sandbox_inventory", "fn publish_template_inventory", "fn publish_node_status",
  "const PENDING_LOCAL_BACKUP_BUDGET", "fn collect_pending_local_backups", "fn bounded_pending_local_backups",
  "fn fetch_pending_local_backups", "fn collect_auto_lifecycle_delete_completions",
  "fn acknowledge_auto_lifecycle_delete_completions",
]

[[module]]
name = "local_state"
doc = "Reads of the node's local sandbox and template state that the facets reconcile and publish."
items = [
  "fn resolve_template_scope", "fn resolve_sandbox_scope", "fn list_local_template_names",
  "fn list_local_sandbox_ids", "fn collect_sandbox_states", "fn fetch_sandbox_states",
  "fn fetch_sandbox_state_batches", "fn index_sandbox_state_response", "fn fetch_sandbox_network_policies",
  "fn fetch_sandbox_snapshots", "fn parse_sandbox_id", "fn fetch_sandbox_disk_usage",
  "fn published_sandbox_state", "fn sandbox_spaced_version", "fn sandbox_last_used_at",
  "fn sandbox_last_access_at", "fn sandbox_last_command_at", "fn max_proto_timestamp",
  "fn absent_sandbox_state", "fn collect_template_states", "fn local_sandbox_allocation_ids",
  "fn fetch_local_sandbox_states", "fn network_slot_state_has_live_allocation",
]

The tool is scripts/split_rust_module.py from #562 (not on main yet).

  • Regenerate: on main at b9fc395e0, run split_rust_module.py apply spec.toml && cargo fmt --all. This reproduces the first commit.
  • Proof: split_rust_module.py verify origin/main → regenerated and compared 8 files from 1 split parents: identical. verify derives the spec from the diff, regenerates the split from the merge base, and requires HEAD's Rust files to equal the rustfmt'd result byte for byte.
  • Edits inside moved text: 101 private items, fields or methods → pub(super). Each keeps the reach the item had: one module deeper, pub(super) reaches exactly the old module.

Observable changes

  • Log targets: 41 tracing events without an explicit target: now log under the new module paths (pplx_space_node_ctl_server::facets::<module> instead of pplx_space_node_ctl_server::facets). Nothing in the repository selects on these targets exactly.

  • Diagnostics name new locations:

    • the 1 panic site left in the parent shift line numbers;
    • the 41 tracing callsites in moved code report the new file and line in their metadata, including OpenTelemetry's code.filepath, code.lineno and code.namespace;
    • std::any::type_name of a moved type names its new module;

    The reviews found nothing in the repository that depends on these.

Validation

Run on a Linux development machine at the PR head:

  • cargo check --workspace --all-targets --all-features --locked: pass, no warnings.
  • Every clippy invocation in run_clippy_checks (ci/scripts/run-rust-checks.sh) with -D warnings: pass.
  • cargo check -p pplx-space-node-ctl-server --no-default-features --all-targets: pass.
  • cargo nextest run -p pplx-space-node-ctl-server --all-features with Postgres and SPACE_CI_REQUIRE_TEST_DEPS=1: 93/93 passed, none skipped. No tests of this package are in the privileged group.
  • archcheck as CI runs it: no required findings. The only new advisory finding is the stale file_size entry below.

Interactions

No open pull request touches facets.rs. Nothing outside crates/ cites the file or a moved function.

Follow-ups, kept out of this PR because verify allows no other Rust edits: a doc comment in internal/api/server/src/node_local_backup_pending.rs still points collect_pending_local_backups at facets.rs; and imports in facets.rs that only the child modules use could move to them.

The archcheck file_size baseline entry for this file is now stale (an advisory finding). Split PRs leave that table alone, so they don't conflict with each other there; one follow-up prunes the stale entries after they land.

Rollback

Revert the commit(s). No data, configuration, wire or public API change.

🤖 Generated with Claude Code

@ac-pplx
ac-pplx force-pushed the ac/split-node-ctl-facets branch from f01d43b to 87185fb Compare October 16, 2026 06:45
facets.rs holds the facet registry and dispatch together with every
facet's implementation. Keep the registry, dispatch and shared publish
types in facets.rs and move each facet into its own child module:
network_policy, template_desired_state, sandbox_desired_state,
cluster_deleted_eviction, port_exposure, inventory_publish and the
local_state reads they share.

Generated by scripts/split_rust_module.py (#562) from the spec in the
pull request. Items move verbatim. The only edits keep each item's
reach: 101 private items, fields or methods become pub(super). The
parent re-exports the moved items, so no caller changes.
@ac-pplx
ac-pplx force-pushed the ac/split-node-ctl-facets branch from 87185fb to 1576058 Compare October 16, 2026 06:46
@ac-pplx ac-pplx changed the title node-ctl: split facets.rs into one module per facet (pure move) node-ctl: split facets.rs into facet and local-state modules (pure move) Oct 16, 2026
@ac-pplx
ac-pplx marked this pull request as ready for review October 16, 2026 14:49

@nickdujay-pplx nickdujay-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 157605880120760bec2c9411c0e0ae5e0bd91e3e; no blocking findings.

Verified all 129 original top-level items survive the split with their contents unchanged apart from the documented visibility adjustments and formatting. Checked module/re-export wiring and the module-sensitive macros; reconciliation decisions, retries, RPC payloads, and snapshot/inventory handling are unchanged.

Validation: independently reproduced the split across all 8 Rust files, then ran all 93 node-ctl tests with real PostgreSQL, all features, and SPACE_CI_REQUIRE_TEST_DEPS=1: 93 passed, 0 skipped. Current CI checks are green.

No schema, configuration, or protocol migration is introduced, so this does not add a deployment-order or rollback dependency. The documented tracing-target and diagnostic-location changes remain; I found no exact old-target selectors in this repository.

@the-gigi-pplx

Copy link
Copy Markdown
Collaborator

Review

No findings.

Verified against 157605880120760bec2c9411c0e0ae5e0bd91e3e.

What I ran:

  • scripts/split_rust_module.py verify origin/main, using the script from PR scripts: add split_rust_module.py for mechanical, verifiable Rust module splits #562's head: identical, exit 0.
  • apply with the spec in this PR's body, run on the merge base and rustfmt'd: it reproduces every .rs file at the head byte for byte. So the glob patterns in the published spec select the same items as the expanded spec verify --print-spec derives.
  • Each moved child file, grepped for self::/super:: in doc comments. The tool doesn't rewrite comments, so a link like that would point one module off after the move. There are none.
  • The non-split files, grepped for comments that name the old file next to a moved item.
  • Whether main has touched the split file since the merge base: it hasn't.

I also checked that the docs fixups the body lists as follow-ups are still the only stale pointers: node_local_backup_pending.rs:319 still names facets.rs for collect_pending_local_backups, which now lives in facets/inventory_publish.rs, as the body already says.

What I didn't check: whether the grouping is the right one. That's a design call, and I took the body's word for it. I also didn't rebuild or retest the crate; CI is green on this head.

@the-gigi-pplx the-gigi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. No findings; details in the review comment above.

@ac-pplx
ac-pplx added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit eb3f55b Oct 16, 2026
20 checks passed
@ac-pplx
ac-pplx deleted the ac/split-node-ctl-facets branch October 16, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants