Skip to content

Commit 5850c60

Browse files
authored
feat: migration 2.0 (#232)
1 parent 3be2ee2 commit 5850c60

26 files changed

Lines changed: 1114 additions & 155 deletions

File tree

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,14 @@ tokio-serde-postcard = { git = "https://github.com/xDarksome/tokio-serde-postcar
5555
postcard = { version = "1.0", default-features = false }
5656
itertools = "0.12"
5757
futures = "0.3"
58+
futures-concurrency = "7.6"
5859
backoff = { version = "0.4", features = ["tokio"] }
5960
tracing = "0.1"
6061
tokio-stream = "0.1"
6162
strum = "0.27"
6263
xxhash-rust = { version = "0.8", features = ["xxh3", "const_xxh3"] }
64+
thiserror = "1"
65+
anyhow = "1"
6366

6467
[workspace.lints.clippy]
6568
all = { level = "deny", priority = -1 }

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
250729.0
1+
250804.0

crates/cluster/src/keyspace.rs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ use {
66
derivative::Derivative,
77
derive_more::TryFrom,
88
serde::{Deserialize, Serialize},
9-
sharding::ShardId,
10-
std::collections::HashSet,
9+
std::{collections::HashSet, ops::RangeInclusive},
1110
xxhash_rust::xxh3::Xxh3Builder,
1211
};
1312

@@ -38,10 +37,18 @@ pub struct Keyspace<S = ()> {
3837
#[derive(Clone)]
3938
pub struct Shards(sharding::Keyspace<node_operator::Idx, { REPLICATION_FACTOR as usize }>);
4039

40+
/// ID of a [`Shard`].
41+
pub type ShardId = u16;
42+
43+
/// Returns the keyrange the provided shard is resposible for.
44+
pub fn keyrange(id: ShardId) -> RangeInclusive<u64> {
45+
sharding::ShardId(id).key_range()
46+
}
47+
4148
/// A single [`Shard`] within a [`Keyspace`].
4249
#[derive(Clone, Copy, Debug)]
43-
pub struct Shard {
44-
replica_set: [node_operator::Idx; REPLICATION_FACTOR as usize],
50+
pub struct Shard<T = node_operator::Idx> {
51+
pub(crate) replica_set: [T; REPLICATION_FACTOR as usize],
4552
}
4653

4754
/// Strategy of distributing [`Shard`]s to [`node_operator`]s.
@@ -95,9 +102,20 @@ impl Keyspace<Shards> {
95102
/// Returns the [`Shard`] that contains the specified key.
96103
pub fn shard(&self, key: u64) -> Shard {
97104
Shard {
98-
replica_set: *self.shards.0.shard_replicas(ShardId::from_key(key)),
105+
replica_set: *self
106+
.shards
107+
.0
108+
.shard_replicas(sharding::ShardId::from_key(key)),
99109
}
100110
}
111+
112+
/// Returns all [`Shard`]s of this [`Keyspace`].
113+
pub fn shards(&self) -> impl Iterator<Item = (ShardId, Shard)> + '_ {
114+
self.shards
115+
.0
116+
.shards()
117+
.map(|(id, &replica_set)| (id.0, Shard { replica_set }))
118+
}
101119
}
102120

103121
impl<S> Keyspace<S> {
@@ -144,10 +162,10 @@ impl<S> Keyspace<S> {
144162
}
145163
}
146164

147-
impl Shard {
165+
impl<T: Copy> Shard<T> {
148166
/// Returns [`ReplicaSet`] assigned to this [`Shard`].
149-
pub fn replica_set(&self) -> ReplicaSet {
150-
self.replica_set
167+
pub fn replica_set(&self) -> &ReplicaSet<T> {
168+
&self.replica_set
151169
}
152170
}
153171

crates/cluster/src/lib.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub trait Config: Send + Sync + 'static {
7474
/// data/logic into it.
7575
///
7676
/// If no additional logic is required - just specify [`Node`].
77-
type Node: Clone + Send + Sync + 'static;
77+
type Node: AsRef<PeerId> + Clone + Send + Sync + 'static;
7878

7979
/// Creates a new [`Config::Node`].
8080
fn new_node(&self, operator_id: node_operator::Id, node: Node) -> Self::Node;
@@ -96,7 +96,9 @@ struct Inner<C: Config> {
9696
config: C,
9797
smart_contract: C::SmartContract,
9898
view: ArcSwap<View<C>>,
99-
watch: watch::Receiver<()>,
99+
100+
watch_tx: watch::Sender<()>,
101+
watch_rx: watch::Receiver<()>,
100102
}
101103

102104
/// Version of a WCN [`Cluster`].
@@ -187,7 +189,8 @@ where
187189
config: cfg,
188190
smart_contract: contract,
189191
view: ArcSwap::new(Arc::new(view)),
190-
watch: rx,
192+
watch_tx: tx.clone(),
193+
watch_rx: rx,
191194
});
192195

193196
let guard = Task {
@@ -225,13 +228,55 @@ impl<C: Config> Cluster<C> {
225228
pub fn updates(&self) -> impl Stream<Item = ()> + Send + 'static {
226229
// TODO: periodically check with the SC to prevent drift
227230

228-
tokio_stream::wrappers::WatchStream::new(self.inner.watch.clone())
231+
tokio_stream::wrappers::WatchStream::new(self.inner.watch_rx.clone())
232+
}
233+
234+
/// Creates a new [`Watch`] of this [`Cluster`].
235+
pub fn watch(&self) -> Watch {
236+
let mut rx = self.inner.watch_rx.clone();
237+
rx.mark_changed();
238+
239+
Watch {
240+
inner: rx,
241+
_tx: self.inner.watch_tx.clone(),
242+
}
229243
}
230244

231245
/// Returns reference to the underlying [`SmartContract`].
232246
pub fn smart_contract(&self) -> &C::SmartContract {
233247
&self.inner.smart_contract
234248
}
249+
250+
/// Indicates whether this [`Cluster`] contains a [`Node`] with the provided
251+
/// ID.
252+
pub fn contains_node(&self, peer_id: &PeerId) -> bool
253+
where
254+
C::Node: AsRef<PeerId>,
255+
{
256+
self.using_view(|view| view.node_operators().contains_node(peer_id))
257+
}
258+
259+
/// Checks whether the provided [`keyspace`] version is compatible with
260+
/// current state of the [`Cluster`].
261+
pub fn validate_keyspace_version(&self, version: u64) -> bool {
262+
self.using_view(|view| view.validate_keyspace_version(version))
263+
}
264+
}
265+
266+
/// Handle to track [`Cluster`] updates.
267+
pub struct Watch {
268+
inner: watch::Receiver<()>,
269+
_tx: watch::Sender<()>,
270+
}
271+
272+
impl Watch {
273+
/// Resolves when the next [`Cluster`] update occurs.
274+
///
275+
/// First time after creation resolves immediately.
276+
pub async fn cluster_updated(&mut self) {
277+
// NOTE(unwrap): we are holding the `Sender`, so it won't ever error.
278+
self.inner.changed().await.unwrap()
279+
}
235280
}
236281

237282
impl<C: Config> Cluster<C>

crates/cluster/src/node.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Node within a WCN cluster.
22
33
use {
4+
derive_more::derive::AsRef,
45
libp2p::identity::PeerId,
56
serde::{Deserialize, Serialize},
67
std::net::SocketAddrV4,
@@ -11,12 +12,13 @@ use {
1112
/// The IP address is currently being encrypted using a format-preserving
1213
/// encryption algorithm.
1314
// TODO: encrypt
14-
#[derive(Debug, Clone, Copy)]
15+
#[derive(AsRef, Debug, Clone, Copy)]
1516
pub struct Node {
1617
/// [`PeerId`] of the [`Node`].
1718
///
1819
/// Used for authentication. Multiple nodes managed by the same
1920
/// node operator are allowed to have the same [`PeerId`].
21+
#[as_ref]
2022
pub peer_id: PeerId,
2123

2224
/// [`SocketAddrV4`] of the [`Node`].

crates/cluster/src/node_operator.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,31 @@ impl<N> NodeOperator<N> {
115115
&self.nodes[n % self.nodes.len()]
116116
}
117117

118-
/// List of [`Node`]s of the [`NodeOperator`].
118+
/// Returns [`Node`]s of this [`NodeOperator`].
119119
///
120120
/// [`NodeOperator`] is guaranteed to always have at least 2 nodes.
121121
pub fn nodes(&self) -> &[N] {
122122
&self.nodes
123123
}
124+
125+
/// Returns an [`Iterator`] of [`Node`]s of this [`NodeOperator`].
126+
///
127+
/// Iterates over all [`Node`]s starting from an arbitrary position.
128+
/// Intended for load balancing purposes.
129+
///
130+
/// [`NodeOperator`] is guaranteed to always have at least 2 nodes.
131+
pub fn nodes_lb_iter(&self) -> impl Iterator<Item = &N> {
132+
// TODO: this is suboptimal for > 2 nodes, because in case of a node failure the
133+
// next node in the list will receive all of the load of it's neighbor.
134+
//
135+
// Naive solution to this is to reallocate and shuffle. A better solution may be
136+
// to split the `Vec` on chunks of size <=N and then iterate over the chunks and
137+
// shuffle the chunks in-place.
138+
139+
let n = self.counter.fetch_add(1, atomic::Ordering::Relaxed);
140+
let (left, right) = self.nodes.split_at(n % self.nodes.len());
141+
right.iter().chain(left.iter())
142+
}
124143
}
125144

126145
/// [`NodeOperator`] with serialized [`Data`].

crates/cluster/src/node_operators.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use {
44
crate::{self as cluster, node_operator, Node, NodeOperator},
55
indexmap::IndexMap,
6+
libp2p::PeerId,
67
std::sync::{
78
atomic::{self, AtomicUsize},
89
Arc,
@@ -59,6 +60,20 @@ impl<N> NodeOperators<N> {
5960
})
6061
}
6162

63+
/// Indicates whether any of the [`NodeOperators`] contains a node with the
64+
/// provided ID.
65+
pub fn contains_node(&self, peer_id: &PeerId) -> bool
66+
where
67+
N: AsRef<PeerId>,
68+
{
69+
// TODO: Consider optimizing by building a lookup table.
70+
self.slots.iter().any(|opt| {
71+
opt.as_ref()
72+
.map(|op| op.nodes().iter().any(|node| node.as_ref() == peer_id))
73+
.unwrap_or_default()
74+
})
75+
}
76+
6277
/// Returns a [`NodeOperator`] responsible for the next request.
6378
///
6479
/// [`NodeOperator`]s are being iterated in round-robin fashion for

crates/cluster/src/smart_contract/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ pub trait Write {
9696
/// [`migration::Completed`] event MUST be emitted.
9797
/// Otherwise the data pull MUST be marked as completed for the
9898
/// [`node::Operator`] and [`migration::DataPullCompleted`] MUST be emitted.
99-
fn complete_migration(&self, id: migration::Id) -> impl Future<Output = WriteResult<()>>;
99+
fn complete_migration(&self, id: migration::Id)
100+
-> impl Future<Output = WriteResult<()>> + Send;
100101

101102
/// Aborts the ongoing data [`migration`] process restoring the WCN cluster
102103
/// to the original state it had before the migration had started.

crates/cluster/src/view.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ impl<C: Config<KeyspaceShards = keyspace::Shards>> View<C> {
4848
// therefore guaranteeing that every `NodeOperatorIdx` is valid.
4949
self.keyspace
5050
.shard(key)
51-
.replica_set()
51+
.replica_set
5252
.map(|idx| self.node_operators.get_by_idx(idx).unwrap())
5353
}
5454

@@ -61,10 +61,54 @@ impl<C: Config<KeyspaceShards = keyspace::Shards>> View<C> {
6161
self.migration()?
6262
.keyspace()
6363
.shard(key)
64-
.replica_set()
64+
.replica_set
6565
.map(|idx| self.node_operators.get_by_idx(idx).unwrap())
6666
.pipe(Some)
6767
}
68+
69+
/// Returns [`keyspace::Shard`]s of the primary [`Keyspace`].
70+
pub fn primary_keyspace_shards(
71+
&self,
72+
) -> impl Iterator<Item = (keyspace::ShardId, keyspace::Shard<&NodeOperator<C::Node>>)> {
73+
// NOTE(unwrap): we use `Keyspace::validate` every time it enters the system,
74+
// therefore guaranteeing that every `NodeOperatorIdx` is valid.
75+
self.keyspace.shards().map(|(id, shard)| {
76+
(id, keyspace::Shard {
77+
replica_set: shard
78+
.replica_set
79+
.map(|idx| self.node_operators.get_by_idx(idx).unwrap()),
80+
})
81+
})
82+
}
83+
84+
/// Returns [`keyspace::Shard`]s of the secondary [`Keyspace`].
85+
pub fn secondary_keyspace_shards(
86+
&self,
87+
) -> Option<impl Iterator<Item = (keyspace::ShardId, keyspace::Shard<&NodeOperator<C::Node>>)>>
88+
{
89+
// NOTE(unwrap): we use `Keyspace::validate` every time it enters the system,
90+
// therefore guaranteeing that every `NodeOperatorIdx` is valid.
91+
self.migration()?
92+
.keyspace()
93+
.shards()
94+
.map(|(id, shard)| {
95+
(id, keyspace::Shard {
96+
replica_set: shard
97+
.replica_set
98+
.map(|idx| self.node_operators.get_by_idx(idx).unwrap()),
99+
})
100+
})
101+
.pipe(Some)
102+
}
103+
104+
/// Indicates whether the specified [`node_operator`] is still in process of
105+
/// pulling data as part of a data [`migration`] process.
106+
pub fn is_pulling(&self, operator_id: &node_operator::Id) -> bool {
107+
self.node_operators()
108+
.get_idx(operator_id)
109+
.and_then(|operator_idx| self.migration().map(|mig| mig.is_pulling(operator_idx)))
110+
.unwrap_or_default()
111+
}
68112
}
69113

70114
impl<C: Config> View<C> {
@@ -98,6 +142,16 @@ impl<C: Config> View<C> {
98142
&self.node_operators
99143
}
100144

145+
/// Checks whether the provided [`keyspace`] version is compatible with
146+
/// current state of the cluster.
147+
pub fn validate_keyspace_version(&self, version: u64) -> bool {
148+
if let Some(migration) = self.migration() {
149+
migration.keyspace().version() == version
150+
} else {
151+
self.keyspace().version() == version
152+
}
153+
}
154+
101155
pub(super) fn require_no_migration(&self) -> Result<(), migration::InProgressError> {
102156
if let Some(migration) = self.migration() {
103157
return Err(migration::InProgressError(migration.id()));

0 commit comments

Comments
 (0)