Skip to content

Replacing a vector of paths with PathMap in zipper excl. tracking#4

Merged
luketpeterson merged 10 commits into
zipper_headfrom
zipper_head_ext
Nov 16, 2024
Merged

Replacing a vector of paths with PathMap in zipper excl. tracking#4
luketpeterson merged 10 commits into
zipper_headfrom
zipper_head_ext

Conversation

@marcin-rzeznicki

Copy link
Copy Markdown
Collaborator

Plus a couple of related changes:

  • replacing panic with error,
  • more precise types.

Changes

ZipperTracker is now parametrized by TrackingMode

TrackingMode serves as a type-level indicator whether we're tracking writes or reads. In Rust you cannot do much with this - but at the very least we don't need to panic at runtime e.g. in Clone. Also various constructors throughout the code are aware of what mode they expect, which may be helpful in further development as there is no way to mix read-tracking zippers with write-tracking zippers.

BytesTrieMap is used instad of Vec to track locked paths (SharedTrackerPaths)

This helps the complexity as checking no longer needs traversing all of the n paths and checking their prefixes.
The replacement uses BytesTrieMap as a prefix tree annotated with IsTracking values that designate the subtree as locked for writing, or reading (unsigned non-zero counter)

Replacing panic with error

The Conflict type is implemented, serving as a structured error info, and methods that previously panicked (and were poisoning the locks) now return Result<(), Conflict> allowing the caller to graciously handle read/write conflicts

What does not work?

Tests - I have not finished adapting them - they don't eve compile currently. Will fix in a subsequent PR

Things to scrutinize

Usage of zippers may be imprecise. Please check methods that rely on certain zipper/trie behavior e.g. Conflict::check_subtree_for_conflict, SharedTrackPaths::try_add_writer, SharedTrackPaths::try_add_reader

I am sorry for

Reformatting a lot of code which makes the PR much bigger. This is my lack of tooling knowledge showing up (I don't know how to turn it off 😄 ). Perhaps we could have some kind of project-wide formatting config to avoid this stylistic inconsistency?

@luketpeterson

Copy link
Copy Markdown
Collaborator

Ugh... I manually merged this with all the latest changes, but it seems I don't have the permissions to push back to this branch.

Comment thread src/zipper_head.rs
Comment on lines 24 to 44
/// A more efficient version of [read_zipper_at_path](Self::read_zipper_at_path), where the returned
/// zipper is constrained by the `'path` lifetime

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

We need to justify the failure mode of this function, i.e. "why could it be the case that I can't read at a path" in its docs.

Comment thread src/zipper_head.rs Outdated
let zipper_tracker = ZipperTracker::new_read_tracker(self.tracker_paths.clone(), path);
ReadZipperUntracked::new_with_node_and_cloned_path(root.borrow(), path.as_ref(), Some(path.len()), Some(zipper_tracker))
let zipper_tracker =
ZipperTracker::<TrackingRead>::new_no_check(self.tracker_paths.clone(), path);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

read_zipper_at_path_unchecked is a very hot method, is it possible to avoid the clone here? How deep is this clone? Debug mode is close to uselessly slow as is, so we have to avoid any additional "order of magnitude" reduction in speed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think it clones Arc - which results in just increasing ref count. https://doc.rust-lang.org/src/alloc/sync.rs.html#2090

Comment thread src/zipper_head.rs Outdated
Comment on lines +111 to +115
/// Creates a new [WriteZipper] with the specified path from the `ZipperHead`
pub fn write_zipper_at_exclusive_path<'k, K: AsRef<[u8]>>(&self, path: K) -> WriteZipperTracked<'a, 'k, V> {
pub fn write_zipper_at_exclusive_path<'k, K: AsRef<[u8]>>(
&self,
path: K,
) -> Result<WriteZipperTracked<'a, 'k, V>, Conflict> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

With Trackers being more internalized now, perhaps we should rename this method to "write_zipper_at_path", and the result indicates whether or not the path was exclusive or not.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@luketpeterson ⏫ I'm leaving it for you to decide

Comment thread src/zipper_head.rs
Comment on lines 309 to +564
//Make a ZipperHead for the whole map, and two child zippers
let map_head = map.zipper_head();
let mut a_zipper = map_head.write_zipper_at_exclusive_path(b"a");
let mut b_zipper = map_head.write_zipper_at_exclusive_path(b"b");
let mut a_zipper = map_head.write_zipper_at_exclusive_path(b"a").unwrap();
let mut b_zipper = map_head.write_zipper_at_exclusive_path(b"b").unwrap();

//Make a separate ZipperHead on each WriteZipper
let a_head = a_zipper.zipper_head();
let b_head = b_zipper.zipper_head();

//Make some WriteZippers on each head
let mut a0_zipper = a_head.write_zipper_at_exclusive_path(b"0");
let mut a1_zipper = a_head.write_zipper_at_exclusive_path(b"1");
let mut b0_zipper = b_head.write_zipper_at_exclusive_path(b"0");
let mut b1_zipper = b_head.write_zipper_at_exclusive_path(b"1");
let mut a0_zipper = a_head.write_zipper_at_exclusive_path(b"0").unwrap();
let mut a1_zipper = a_head.write_zipper_at_exclusive_path(b"1").unwrap();
let mut b0_zipper = b_head.write_zipper_at_exclusive_path(b"0").unwrap();
let mut b1_zipper = b_head.write_zipper_at_exclusive_path(b"1").unwrap();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Seems like we should reduce the synthetic overhead here if possible. Is it possible for users to use the ? operator? As mentioned, the "exclusive" seems to be captured by the unwrap here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Makes perfect sense. Agreed

Comment thread src/zipper_tracking.rs
#[derive(Debug)]
pub struct Conflict {
with: IsTracking,
at: Vec<u8>,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Seems like every context Conflicts occur in, you'd have access to the reference of the path? If people are expecting a Conflict, copying the path is a silly price to pay.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, perhaps, but the path does not have to be simply a clone of the original path, as conflicts can occur, depending on the usage pattern, below or above the passed path.

Comment thread src/zipper_tracking.rs Outdated
Comment thread src/zipper_tracking.rs Outdated
Comment thread src/zipper_tracking.rs
self.modify_at(path, try_add_reader_internal)
}

fn add_reader_unchecked(&self, path: &[u8]) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

What's the use of this function, or in what notion is this unchecked? This object being used on a relevant path seems mutually exclusive to at_path_unchecked being used.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's only used for cloning tracked zippers right now. The difference is that it simply increments the counter, without checking for exclusivity. In this case it is actually safe, because during cloning you obviously do not thread on a writer.

Comment thread src/zipper_tracking.rs Outdated
Comment thread src/zipper_tracking.rs Outdated
/// A shared registry of every outstanding zipper
#[derive(Clone, Default)]
pub(crate) struct SharedTrackerPaths(Arc<RwLock<ZipperPaths>>);
pub(crate) struct SharedTrackerPaths(Arc<RwLock<BytesTrieMap<IsTracking>>>);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Why is this Arc and RwLock, only one thread may have access to the tracking?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Can drop happen on a different thread?

@luketpeterson luketpeterson 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.

Looks fine except for the build-breaking change in release mode. Which should simply be reverted.

Comment thread src/zipper_head.rs Outdated
@luketpeterson

Copy link
Copy Markdown
Collaborator

I resolved the merge in my 8ceb571 commit.

I also attached the diff. zipper_head_ext_merge_diff.txt

@marcin-rzeznicki

Copy link
Copy Markdown
Collaborator Author

I resolved the merge in my 8ceb571 commit.

I also attached the diff. zipper_head_ext_merge_diff.txt

Thank you @luketpeterson . Cherry picked as 06477da

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