Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ target/
#.idea/
.DS_Store

# Untracked internal references (papers, private docs, etc.).
# Contents must never be referenced directly in code or comments.
# Extract and restate any needed information explicitly in the codebase.
internal/

Cargo.lock

.idea/
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ This is a Rust implementation of Skip Graphs - a distributed data structure for

## References

- `skip-graphs-paper.pdf` (Aspnes & Shah) — canonical algorithms: search (Algorithm 1, p.6), insert/join (Algorithm 2, p.8), delete (Algorithm 3, p.8).
Source papers and private documents live in `internal/` (gitignored, never committed).

- `internal/skip-graphs-paper.pdf` — Aspnes & Shah, "Skip Graphs". Canonical algorithms: search (Algorithm 1, p.6), insert/join (Algorithm 2, p.8), delete (Algorithm 3, p.8).

### Rules for internal references

1. **Never cite internal documents in code.** No path references, no "see PDF p.X", no file names from `internal/` in source files, comments, or doc-strings.
2. **Restate, don't link.** If an algorithm, invariant, or design decision from an internal document is needed in the codebase, copy the relevant content explicitly — as a comment, a doc-string, or a design doc under `docs/` — and use that in-codebase copy as the reference going forward.
3. **Agents must follow the same rules.** Any agent working in this repository must not produce code or comments that reference `internal/` paths or document titles. Extract and restate; never leak.

## Architecture

Expand Down
Binary file removed skip-graphs-paper.pdf
Binary file not shown.
61 changes: 34 additions & 27 deletions src/core/context/context_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ mod tests {

// Wait until cancellation is processed
let ctx_clone = ctx.clone();
wait_until(
move || ctx_clone.is_cancelled(),
Duration::from_millis(100)
).await.expect("context should be cancelled within 100ms");
wait_until(move || ctx_clone.is_cancelled(), Duration::from_millis(100))
.await
.expect("context should be cancelled within 100ms");
}

/// this test ensures that cancelling a parent context cancels its children
Expand All @@ -33,18 +32,18 @@ mod tests {
let child_clone = child.clone();
wait_until(
move || child_clone.is_cancelled(),
Duration::from_millis(100)
).await.expect("child context should be cancelled within 100ms");
Duration::from_millis(100),
)
.await
.expect("child context should be cancelled within 100ms");
}

/// this test ensures that running an operation completes successfully if its context is not canceled
#[tokio::test]
async fn test_successful_operation() {
let ctx = IrrevocableContext::new(&span_fixture(), "test_context");

let result = ctx.run(async {
Ok::<i32, anyhow::Error>(42)
}).await;
let result = ctx.run(async { Ok::<i32, anyhow::Error>(42) }).await;

assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
Expand All @@ -61,25 +60,27 @@ mod tests {

// Wait for cancellation to be processed
let ctx_clone = ctx.clone();
wait_until(
move || ctx_clone.is_cancelled(),
Duration::from_millis(100)
).await.expect("context should be cancelled within 100ms");

let result = ctx.run(async {
// This should not execute due to cancellation
sleep(Duration::from_millis(10)).await;
Ok::<i32, anyhow::Error>(42)

}).await;
wait_until(move || ctx_clone.is_cancelled(), Duration::from_millis(100))
.await
.expect("context should be cancelled within 100ms");

let result = ctx
.run(async {
// This should not execute due to cancellation
sleep(Duration::from_millis(10)).await;
Ok::<i32, anyhow::Error>(42)
})
.await;

// The operation should return an error since the context was canceled before it could complete
assert!(result.is_err());
// The error should indicate cancellation
assert!(result.unwrap_err().to_string().contains("context cancelled"));
assert!(result
.unwrap_err()
.to_string()
.contains("context cancelled"));
}


/// this test ensures that nested child contexts are canceled when the root context is canceled
#[tokio::test]
async fn test_nested_children() {
Expand All @@ -99,9 +100,15 @@ mod tests {
let child2_clone = child2.clone();
let grandchild_clone = grandchild.clone();
wait_until(
move || child1_clone.is_cancelled() && child2_clone.is_cancelled() && grandchild_clone.is_cancelled(),
Duration::from_millis(100)
).await.expect("all child contexts should be cancelled within 100ms");
move || {
child1_clone.is_cancelled()
&& child2_clone.is_cancelled()
&& grandchild_clone.is_cancelled()
},
Duration::from_millis(100),
)
.await
.expect("all child contexts should be cancelled within 100ms");
}

/// Test that we can create the error propagation hierarchy
Expand Down Expand Up @@ -137,9 +144,9 @@ mod tests {
if let Err(panic_payload) = result {
if let Some(panic_msg) = panic_payload.downcast_ref::<String>() {
assert_eq!(panic_msg, "irrecoverable error: test irrecoverable error");
} else{
} else {
panic!("unexpected panic payload type");
}
}
}
}
}
11 changes: 5 additions & 6 deletions src/core/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use tokio_util::sync::CancellationToken;
use tracing::Span;

/// A cancelable context that supports parent-child hierarchies and irrecoverable error propagation.
///
///
/// When an irrecoverable error is thrown, it propagates up to the root context and terminates the program.
/// Children automatically get cancelled when their parent is cancelled.
pub struct IrrevocableContext {
Expand All @@ -34,7 +34,7 @@ impl IrrevocableContext {
/// Create a new root context
pub fn new(parent_span: &Span, tag: &str) -> Self {
let span = tracing::span!(parent: parent_span, tracing::Level::TRACE, "irrevocable_context", tag = tag);

Self {
inner: Arc::new(ContextInner {
token: CancellationToken::new(),
Expand Down Expand Up @@ -82,7 +82,7 @@ impl IrrevocableContext {
F: std::future::Future<Output = Result<T>>,
{
let _enter = self.inner.span.enter();

tokio::select! {
result = future => result,
_ = self.cancelled() => {
Expand All @@ -96,13 +96,13 @@ impl IrrevocableContext {
/// there is no return from this function.
pub fn throw_irrecoverable(&self, err: anyhow::Error) -> ! {
let _enter = self.inner.span.enter();

// Propagate to parent if it exists
if let Some(parent) = &self.inner.parent {
tracing::error!("propagating irrecoverable error to parent context");
parent.throw_irrecoverable(err);
}

// Root context - panic with the error
panic!("irrecoverable error: {}", err);
}
Expand Down Expand Up @@ -145,4 +145,3 @@ impl Clone for IrrevocableContext {
}
}
}

7 changes: 6 additions & 1 deletion src/core/lookup/array_lookup_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ impl LookupTable for ArrayLookupTable {
}

// Log the update operation
tracing::trace!("lookup table entry updated: level {}, direction {}, identifier {}", level, direction, identity.id());
tracing::trace!(
"lookup table entry updated: level {}, direction {}, identifier {}",
level,
direction,
identity.id()
);
Ok(())
}

Expand Down
13 changes: 6 additions & 7 deletions src/core/lookup/array_lookup_table_test.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#[cfg(test)]
mod tests {
use crate::core::testutil::fixtures::*;
use std::collections::HashMap;
use crate::core::{model, ArrayLookupTable, LookupTable, LOOKUP_TABLE_LEVELS};
use crate::core::model::direction::Direction;
use crate::core::model::identity::Identity;
use crate::core::testutil::fixtures::*;
use crate::core::{model, ArrayLookupTable, LookupTable, LOOKUP_TABLE_LEVELS};
use std::collections::HashMap;

#[test]
/// A new lookup table should be empty.
Expand Down Expand Up @@ -127,8 +127,7 @@ mod tests {
let identities = random_identities(2 * levels);

for i in 0..levels {
lt.update_entry(identities[i], i, Direction::Left)
.unwrap();
lt.update_entry(identities[i], i, Direction::Left).unwrap();
lt.update_entry(identities[i + levels], i, Direction::Right)
.unwrap();
}
Expand Down Expand Up @@ -242,8 +241,8 @@ mod tests {
/// Note: failure or flaky behavior of this test may indicate a bug in the implementation.
#[test]
fn test_randomized_concurrent_operations_with_validation() {
use rand::Rng;
use parking_lot::Mutex;
use rand::Rng;
use std::sync::{Arc, Barrier};
use std::thread;

Expand Down Expand Up @@ -440,4 +439,4 @@ mod tests {
assert_eq!(lt2.get_entry(2, Direction::Left).unwrap(), Some(id3));
assert_eq!(lt3.get_entry(2, Direction::Left).unwrap(), Some(id3));
}
}
}
3 changes: 1 addition & 2 deletions src/core/lookup/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::core::model::direction::Direction;
use crate::core::model::identity::Identity;


pub mod array_lookup_table;
mod array_lookup_table_test;

Expand Down Expand Up @@ -40,7 +39,7 @@ pub trait LookupTable: Send + Sync {
fn right_neighbors(&self) -> anyhow::Result<Vec<(usize, Identity)>>;

/// Creates a shallow copy of this lookup table.
///
///
/// Implementations should ensure that cloned instances share the same underlying data
/// (e.g., using Arc for shared ownership). Changes made through one instance should be
/// visible in all cloned instances. This is the standard cloning behavior for all
Expand Down
2 changes: 1 addition & 1 deletion src/core/model/address.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fmt::Debug;
use fixedstr::{str128, str8};
use std::fmt::Debug;

/// Represents a networking address; composed of host + port
#[derive(Copy, Clone, PartialEq)]
Expand Down
2 changes: 1 addition & 1 deletion src/core/model/direction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ impl Display for Direction {
impl Debug for Direction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
}
20 changes: 10 additions & 10 deletions src/core/model/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ pub struct Identity {

impl Identity {
/// Create a new Identity
pub fn new(id: &Identifier, mem_vec: &MembershipVector, address: Address) -> Identity {
pub fn new(id: Identifier, mem_vec: MembershipVector, address: Address) -> Identity {
Identity {
id: *id,
mem_vec: *mem_vec,
id,
mem_vec,
address,
}
}

/// Get the identifier of the node
pub fn id(&self) -> &Identifier {
&self.id
pub fn id(&self) -> Identifier {
self.id
}

/// Get the membership vector of the node
pub fn mem_vec(&self) -> &MembershipVector {
&self.mem_vec
pub fn mem_vec(&self) -> MembershipVector {
self.mem_vec
}

/// Get the address of the node
Expand All @@ -45,9 +45,9 @@ mod tests {
let id = random_identifier();
let mem_vec = random_membership_vector();
let address = Address::new("localhost", "1234");
let identity = Identity::new(&id, &mem_vec, address);
assert_eq!(identity.id(), &id);
assert_eq!(identity.mem_vec(), &mem_vec);
let identity = Identity::new(id, mem_vec, address);
assert_eq!(identity.id(), id);
assert_eq!(identity.mem_vec(), mem_vec);
assert_eq!(identity.address(), address);
}
}
Loading
Loading