Skip to content

Split TreeContext into TreeConfig and OperationState#424

Open
steiler wants to merge 2 commits into
mainfrom
improveTreeContext
Open

Split TreeContext into TreeConfig and OperationState#424
steiler wants to merge 2 commits into
mainfrom
improveTreeContext

Conversation

@steiler
Copy link
Copy Markdown
Collaborator

@steiler steiler commented May 4, 2026

TreeContext currently holds four fields with fundamentally different lifecycles:

  • Setup fields (SchemaClient, PoolFactory): fixed at tree creation, immutable, shared across all entries and operations
  • Operation fields (ExplicitDeletes, NonRevertiveInfos): reset and accumulated during each operation (import pass, transaction, validation), copied when branching

To callers, all four fields look identical. There is no semantic distinction between "this never changes" and "this accumulates per-operation." This creates three problems:

  1. Unclear semantics: Code reading e.GetTreeContext().NonRevertiveInfo() cannot immediately tell whether this reads global config or operation-specific state.
  2. Silent mutations: Processors and operations mutate ExplicitDeletes and NonRevertiveInfos as side effects of traversal (e.g., ImportConfigProcessor.Run calls e.GetTreeContext().ExplicitDeletes().Add(...)). A reader must trace deep into multiple call stacks to understand what state is being mutated.
  3. Accidental sharing: If a future refactor accidentally fails to deep-copy mutable state during tree branching (DeepCopy), the bug is silent — two branches silently share mutations.

Solution

Split TreeContext into two separate concerns:

  1. TreeConfig interface: immutable, created once per tree root, shared by all entries. Contains SchemaClient and PoolFactory.
  2. OperationState interface: mutable, created once per tree root but deep-copied when the tree is branched. Contains ExplicitDeletes and NonRevertiveInfos.

The TreeContext interface is simplified to a lifecycle marker (DeepCopy plus the two getters), and all flat data-access methods are removed. A big-bang migration updates all call sites at once. DeepCopy reuses the same TreeConfig instance and deep-copies OperationState.

Benefits:

  • Locality: Reading GetTreeConfig() tells you "this is fixed;" reading GetOperationState() tells you "this accumulates."
  • Testability: OperationState can be tested in isolation from schema and pool setup.
  • Safety: The distinction is enforced at the interface level, reducing the risk of accidental mutation sharing.
  • Future leverage: Sets the stage for Phase 2, where OperationState can be threaded explicitly through processors and transaction flows.

steiler and others added 2 commits June 3, 2026 13:37
`TreeContext` currently holds four fields with fundamentally different lifecycles:
- **Setup fields** (SchemaClient, PoolFactory): fixed at tree creation, immutable, shared across all entries and operations
- **Operation fields** (ExplicitDeletes, NonRevertiveInfos): reset and accumulated during each operation (import pass, transaction, validation), copied when branching

To callers, all four fields look identical. There is no semantic distinction between "this never changes" and "this accumulates per-operation." This creates three problems:

1. **Unclear semantics**: Code reading `e.GetTreeContext().NonRevertiveInfo()` cannot immediately tell whether this reads global config or operation-specific state.
2. **Invisible mutation surface**: Processors and operations mutate `ExplicitDeletes` and `NonRevertiveInfos` as side effects of traversal (e.g., `ImportConfigProcessor.Run` calls `e.GetTreeContext().ExplicitDeletes().Add(...)`). The call-site gives no signal that mutable per-operation state is being written. Phase 1 makes that surface visible via the `OperationState()` accessor; Phase 2 (future PR) will thread `TreeOperationState` explicitly through processor and transaction signatures to eliminate the reach-through entirely.
3. **Accidental sharing**: If a future refactor accidentally fails to deep-copy mutable state during tree branching (DeepCopy), the bug is silent — two branches silently share mutations.

## Solution

Split `TreeContext` into two separate concerns:

1. **TreeConfig interface**: immutable, declared in `pkg/tree/api`. Contains SchemaClient and PoolFactory. `TreeContext` self-implements this interface — `TreeConfig()` returns itself, so no separate struct is needed and the interface values are naturally shared across DeepCopy calls.
2. **TreeOperationState interface**: mutable, declared and implemented in `pkg/tree/api`. Created once per tree root, deep-copied when the tree is branched. Contains ExplicitDeletes and NonRevertiveInfos.

The `TreeContext` interface is simplified to a lifecycle container (DeepCopy plus the two accessors), and all flat data-access methods are removed. A big-bang migration updates all call sites at once. DeepCopy shares immutable config fields by value (interface reference semantics) and deep-copies TreeOperationState.

Benefits:
- **Locality**: Reading `tc.TreeConfig()` tells you "this is fixed;" reading `tc.OperationState()` tells you "this accumulates."
- **Testability**: TreeOperationState can be tested in isolation from schema and pool setup.
- **Safety**: The distinction is enforced at the interface level, reducing the risk of accidental mutation sharing.
- **Future leverage**: Sets the stage for Phase 2, where TreeOperationState can be threaded explicitly through processors and transaction flows, eliminating the reach-through mutations entirely.

Co-authored-by: Cursor <cursoragent@cursor.com>
…pper

Follow-up cleanup on the TreeContext split:

- Delete the private `treeConfig` wrapper struct. `TreeContext` now
  self-implements `api.TreeConfig` — `TreeConfig()` returns `t`, so the
  immutable fields are shared across DeepCopy calls via interface-value
  reference semantics with no extra indirection layer.

- Rename `OperationState` → `TreeOperationState` (and constructor/method)
  to align the `Tree*` prefix with `TreeConfig` for consistent naming
  across both tree-lifecycle types.

- Drop the Java-style `Get` prefix: `GetTreeConfig()` → `TreeConfig()`,
  `GetOperationState()` → `OperationState()`, matching Go getter convention
  used throughout the rest of the codebase.

- Rename `DeepCopyState()` → `DeepCopy()` on `TreeOperationState` now that
  the receiver type makes the name unambiguous.

- Update Behavior 7 test from pointer-identity check on `TreeConfig()` to
  field-identity check on `SchemaClient()` / `PoolFactory()`, which is the
  actual invariant now that `TreeConfig()` returns self.

Co-authored-by: Cursor <cursoragent@cursor.com>
@steiler steiler force-pushed the improveTreeContext branch from 43cb6e1 to a99578d Compare June 3, 2026 11:38
@steiler steiler marked this pull request as ready for review June 3, 2026 11:38
@steiler steiler requested a review from a team as a code owner June 3, 2026 11:38
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

❌ Patch coverage is 81.63265% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/tree/api/operation_state.go 83.33% 2 Missing ⚠️
pkg/tree/ops/treeexport.go 0.00% 2 Missing ⚠️
pkg/tree/root_entry.go 33.33% 2 Missing ⚠️
pkg/datastore/deviations.go 0.00% 1 Missing ⚠️
pkg/datastore/target/gnmi/stream.go 0.00% 1 Missing ⚠️
pkg/tree/api/leaf_entry.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant