[fix][core] Single-lock max_level computation - #129
Conversation
…k-max-level # Conflicts: # src/node/repair_schedule.rs
There was a problem hiding this comment.
Pull request overview
This PR addresses a concurrency/consistency gap in BaseCore::max_level by introducing a single-lock lookup-table query (LookupTable::max_populated_level) and switching max_level to use it, avoiding separate reads of left/right neighbor state.
Changes:
- Add
LookupTable::max_populated_leveland implement it forArrayLookupTableusing a singleinner.read()snapshot. - Update
BaseCore::max_levelto usemax_populated_levelrather than combiningleft_neighbors/right_neighbors. - Adjust
left_neighbors/right_neighborsAPIs (and update affected tests) to removeResultwrapping.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/node/search_by_id_test.rs | Updates tests to reflect left_neighbors() now returning a Vec directly. |
| src/node/core.rs | Switches max_level() implementation to use max_populated_level(). |
| src/node/core_test.rs | Updates neighbor-based expectations and removes the prior max_level error-propagation test. |
| src/core/lookup/mod.rs | Changes LookupTable neighbor APIs and adds max_populated_level() to the trait. |
| src/core/lookup/array_lookup_table.rs | Implements max_populated_level() under a single read lock and adjusts neighbor methods to be infallible. |
| src/core/lookup/array_lookup_table_test.rs | Updates neighbor tests and adds coverage for max_populated_level() behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// Returns the list of left neighbors at the current node as a vector of tuples containing the level and identity. | ||
| fn left_neighbors(&self) -> anyhow::Result<Vec<(usize, Identity)>>; | ||
| fn left_neighbors(&self) -> Vec<(usize, Identity)>; | ||
|
|
||
| /// Returns the list of right neighbors at the current node as a vector of tuples containing the level and identity. | ||
| fn right_neighbors(&self) -> anyhow::Result<Vec<(usize, Identity)>>; | ||
| fn right_neighbors(&self) -> Vec<(usize, Identity)>; |
There was a problem hiding this comment.
Fair point, this is technically a breaking change for the pub trait. Given it, the crate isn't published (no license/repository metadata in Cargo.toml) and is still 0.1.0, I'm treating pre-1.0 API churn as acceptable rather than preserving a Result no implementation currently needs. Happy to revisit if this crate is headed for publication with external implementors in mind.
| /// | ||
| /// `None` if no level has a populated entry on either side (an empty table). `Some(level)` | ||
| /// for the highest level with a populated entry, otherwise. | ||
| fn max_populated_level(&self) -> Option<LookupTableLevel>; |
There was a problem hiding this comment.
Intentional deviation, not an oversight. Result was dead weight here (no level arg to bounds-check, RwLock::read() doesn't poison), so I dropped it rather than keep a wrapper that could never be Err. Called this out explicitly in the PR description now.
| fn max_level(&self) -> anyhow::Result<LookupTableLevel> { | ||
| let left = self | ||
| .lt | ||
| .left_neighbors() | ||
| .context("failed to read left neighbors from lookup table")?; | ||
| let right = self | ||
| .lt | ||
| .right_neighbors() | ||
| .context("failed to read right neighbors from lookup table")?; | ||
|
|
||
| Ok(left | ||
| .iter() | ||
| .chain(right.iter()) | ||
| .map(|(level, _)| *level) | ||
| .max() | ||
| .unwrap_or(0)) | ||
| Ok(self.lt.max_populated_level().unwrap_or(0)) | ||
| } |
There was a problem hiding this comment.
Core::max_level's # Errors section documents the trait's general contract (a future Core impl could genuinely fail reading its own storage), not a guarantee this specific implementation exercises it, that signature predates this PR and is out of scope here. BaseCore's own impl just has no failure path left, so the old error-propagation test was removed since there's nothing left for it to exercise. No doc change needed on the trait itself.
|
Claude (@claude) review |
|
Claude finished TheP2P (@thep2p)'s task —— View job Code Review: Single-lock
|
Closes #127.
Adds
LookupTable::max_populated_level, a single-lock query, and switchesBaseCore::max_levelto use it instead of composingleft_neighbors/right_neighborsunder two separate read locks.Two deliberate deviations from the issue's sketch, both narrowing rather than widening the API surface:
max_populated_levelreturnsOption<LookupTableLevel>, notanyhow::Result<Option<LookupTableLevel>>as sketched in the issue. It takes nolevelargument to bounds-check, andparking_lot::RwLock::read()doesn't poison, so it structurally can't fail, theResultwrapper would have been dead code.left_neighbors/right_neighborsgot the same treatment for the same reason. Sincemax_levelno longer calls them, they now have zero production callers left (test fixtures only).Core::max_levelkeeps its existinganyhow::Resultsignature, that's a broader trait contract predating this PR and out of scope here.BaseCore's implementation of it just no longer has a way to fail, so the priortest_max_level_error_propagationtest was removed, there's no failure path left to exercise.