Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
OpenDAS
dynamo
Commits
2fc65ad8
"vscode:/vscode.git/clone" did not exist on "3bf22bb407e1e0a87dfca060ff75e00688c888e4"
Unverified
Commit
2fc65ad8
authored
Jul 24, 2025
by
Yan Ru Pei
Committed by
GitHub
Jul 24, 2025
Browse files
feat: dump radix tree as router events (#2057)
parent
13d3cc13
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
362 additions
and
3 deletions
+362
-3
lib/llm/src/kv_router/approx.rs
lib/llm/src/kv_router/approx.rs
+24
-2
lib/llm/src/kv_router/indexer.rs
lib/llm/src/kv_router/indexer.rs
+338
-1
No files found.
lib/llm/src/kv_router/approx.rs
View file @
2fc65ad8
...
@@ -26,8 +26,8 @@ use tokio_util::sync::CancellationToken;
...
@@ -26,8 +26,8 @@ use tokio_util::sync::CancellationToken;
use
crate
::
tokens
::
TokenBlockSequence
;
use
crate
::
tokens
::
TokenBlockSequence
;
use
crate
::
kv_router
::
indexer
::{
use
crate
::
kv_router
::
indexer
::{
compute_block_hash_for_seq
,
KvIndexerInterface
,
KvRouterError
,
OverlapScores
,
RadixTree
,
compute_block_hash_for_seq
,
DumpRequest
,
KvIndexerInterface
,
KvRouterError
,
OverlapScores
,
WorkerId
,
RadixTree
,
WorkerId
,
};
};
use
crate
::
kv_router
::
protocols
::{
use
crate
::
kv_router
::
protocols
::{
ExternalSequenceBlockHash
,
KvCacheEvent
,
KvCacheEventData
,
KvCacheRemoveData
,
KvCacheStoreData
,
ExternalSequenceBlockHash
,
KvCacheEvent
,
KvCacheEventData
,
KvCacheRemoveData
,
KvCacheStoreData
,
...
@@ -172,6 +172,8 @@ pub struct ApproxKvIndexer {
...
@@ -172,6 +172,8 @@ pub struct ApproxKvIndexer {
route_tx
:
mpsc
::
Sender
<
RouterResult
>
,
route_tx
:
mpsc
::
Sender
<
RouterResult
>
,
/// A sender for remove worker requests.
/// A sender for remove worker requests.
remove_worker_tx
:
mpsc
::
Sender
<
WorkerId
>
,
remove_worker_tx
:
mpsc
::
Sender
<
WorkerId
>
,
/// A sender for dump requests.
dump_tx
:
mpsc
::
Sender
<
DumpRequest
>
,
/// A handle to the background task managing the KV store.
/// A handle to the background task managing the KV store.
task
:
OnceLock
<
std
::
thread
::
JoinHandle
<
()
>>
,
task
:
OnceLock
<
std
::
thread
::
JoinHandle
<
()
>>
,
/// The size of the KV block this indexer can handle.
/// The size of the KV block this indexer can handle.
...
@@ -183,6 +185,7 @@ impl ApproxKvIndexer {
...
@@ -183,6 +185,7 @@ impl ApproxKvIndexer {
let
(
match_tx
,
mut
match_rx
)
=
mpsc
::
channel
::
<
MatchRequest
>
(
2048
);
let
(
match_tx
,
mut
match_rx
)
=
mpsc
::
channel
::
<
MatchRequest
>
(
2048
);
let
(
route_tx
,
mut
route_rx
)
=
mpsc
::
channel
::
<
RouterResult
>
(
2048
);
let
(
route_tx
,
mut
route_rx
)
=
mpsc
::
channel
::
<
RouterResult
>
(
2048
);
let
(
remove_worker_tx
,
mut
remove_worker_rx
)
=
mpsc
::
channel
::
<
WorkerId
>
(
16
);
let
(
remove_worker_tx
,
mut
remove_worker_rx
)
=
mpsc
::
channel
::
<
WorkerId
>
(
16
);
let
(
dump_tx
,
mut
dump_rx
)
=
mpsc
::
channel
::
<
DumpRequest
>
(
16
);
let
cancel_clone
=
token
.clone
();
let
cancel_clone
=
token
.clone
();
let
task
=
std
::
thread
::
spawn
(
move
||
{
let
task
=
std
::
thread
::
spawn
(
move
||
{
// create a new tokio runtime which will only perform work on a single thread
// create a new tokio runtime which will only perform work on a single thread
...
@@ -240,6 +243,10 @@ impl ApproxKvIndexer {
...
@@ -240,6 +243,10 @@ impl ApproxKvIndexer {
Some
(
worker
)
=
remove_worker_rx
.recv
()
=>
{
Some
(
worker
)
=
remove_worker_rx
.recv
()
=>
{
trie
.remove_worker
(
worker
);
trie
.remove_worker
(
worker
);
}
}
Some
(
dump_req
)
=
dump_rx
.recv
()
=>
{
let
events
=
trie
.dump_tree_as_events
();
let
_
=
dump_req
.resp
.send
(
events
);
}
_
=
expiry_fut
=>
{
_
=
expiry_fut
=>
{
let
expired
=
timer_manager
.pop_expired
();
let
expired
=
timer_manager
.pop_expired
();
...
@@ -278,6 +285,7 @@ impl ApproxKvIndexer {
...
@@ -278,6 +285,7 @@ impl ApproxKvIndexer {
match_tx
,
match_tx
,
route_tx
,
route_tx
,
remove_worker_tx
,
remove_worker_tx
,
dump_tx
,
task
:
once
,
task
:
once
,
kv_block_size
,
kv_block_size
,
}
}
...
@@ -355,6 +363,20 @@ impl KvIndexerInterface for ApproxKvIndexer {
...
@@ -355,6 +363,20 @@ impl KvIndexerInterface for ApproxKvIndexer {
self
.remove_worker_tx
.send
(
worker
)
.await
.unwrap
();
self
.remove_worker_tx
.send
(
worker
)
.await
.unwrap
();
}
}
async
fn
dump_events
(
&
self
)
->
Result
<
Vec
<
RouterEvent
>
,
KvRouterError
>
{
let
(
resp_tx
,
resp_rx
)
=
oneshot
::
channel
();
let
dump_req
=
DumpRequest
{
resp
:
resp_tx
};
if
let
Err
(
e
)
=
self
.dump_tx
.send
(
dump_req
)
.await
{
tracing
::
error!
(
"Failed to send dump request: {:?}"
,
e
);
return
Err
(
KvRouterError
::
IndexerOffline
);
}
resp_rx
.await
.map_err
(|
_
|
KvRouterError
::
IndexerDroppedRequest
)
}
fn
shutdown
(
&
mut
self
)
{
fn
shutdown
(
&
mut
self
)
{
self
.cancel
.cancel
();
self
.cancel
.cancel
();
if
let
Some
(
task
)
=
self
.task
.take
()
{
if
let
Some
(
task
)
=
self
.task
.take
()
{
...
...
lib/llm/src/kv_router/indexer.rs
View file @
2fc65ad8
...
@@ -403,6 +403,81 @@ impl RadixTree {
...
@@ -403,6 +403,81 @@ impl RadixTree {
}
}
}
}
}
}
/// Dump the radix tree as a series of RouterEvents that can reconstruct the tree.
/// Uses BFS traversal to ensure that the tree reconstruction is unique,
/// though the exact event ordering will be lost.
pub
fn
dump_tree_as_events
(
&
self
)
->
Vec
<
RouterEvent
>
{
let
mut
events
=
Vec
::
new
();
let
mut
event_id
=
0u64
;
// BFS queue: (current_block, parent_external_hash, tokens_hash)
let
mut
queue
=
VecDeque
::
new
();
// Process root's children first
let
root_borrow
=
self
.root
.borrow
();
for
(
tokens_hash
,
child_block
)
in
&
root_borrow
.children
{
queue
.push_back
((
child_block
.clone
(),
None
,
*
tokens_hash
));
}
drop
(
root_borrow
);
while
let
Some
((
current_block
,
parent_external_hash
,
tokens_hash
))
=
queue
.pop_front
()
{
let
current_borrow
=
current_block
.borrow
();
// Closure to find external hash for a block in a worker's lookup
let
find_external_hash
=
|
worker_id
:
&
WorkerId
|
{
self
.lookup
.get
(
worker_id
)
.and_then
(|
worker_blocks
|
{
worker_blocks
.iter
()
.find
(|(
_
,
block
)|
Rc
::
ptr_eq
(
block
,
&
current_block
))
.map
(|(
hash
,
_
)|
*
hash
)
})
};
// For each worker that has this block
for
worker_id
in
&
current_borrow
.workers
{
// Find the external hash for this block from the worker's lookup
let
external_hash
=
find_external_hash
(
worker_id
);
if
let
Some
(
block_hash
)
=
external_hash
{
// Create a store event for this worker
let
event
=
RouterEvent
{
worker_id
:
*
worker_id
,
event
:
KvCacheEvent
{
event_id
,
data
:
KvCacheEventData
::
Stored
(
KvCacheStoreData
{
parent_hash
:
parent_external_hash
,
blocks
:
vec!
[
KvCacheStoredBlockData
{
block_hash
,
tokens_hash
,
}],
}),
},
};
events
.push
(
event
);
event_id
+=
1
;
}
}
// Add children to queue for BFS traversal
// We need to find any external hash for this block to use as parent
let
any_external_hash
=
if
!
current_borrow
.workers
.is_empty
()
{
current_borrow
.workers
.iter
()
.next
()
.and_then
(
find_external_hash
)
}
else
{
None
};
for
(
child_tokens_hash
,
child_block
)
in
&
current_borrow
.children
{
queue
.push_back
((
child_block
.clone
(),
any_external_hash
,
*
child_tokens_hash
));
}
}
events
}
}
}
/// Scores representing the overlap of workers.
/// Scores representing the overlap of workers.
...
@@ -466,6 +541,12 @@ pub struct MatchRequest {
...
@@ -466,6 +541,12 @@ pub struct MatchRequest {
resp
:
oneshot
::
Sender
<
OverlapScores
>
,
resp
:
oneshot
::
Sender
<
OverlapScores
>
,
}
}
/// A request to dump the tree as events
pub
struct
DumpRequest
{
/// Channel to send the dumped events
pub
resp
:
oneshot
::
Sender
<
Vec
<
RouterEvent
>>
,
}
#[async_trait]
#[async_trait]
pub
trait
KvIndexerInterface
{
pub
trait
KvIndexerInterface
{
/// Find matches for a given sequence of `LocalBlockHash`es.
/// Find matches for a given sequence of `LocalBlockHash`es.
...
@@ -512,6 +593,13 @@ pub trait KvIndexerInterface {
...
@@ -512,6 +593,13 @@ pub trait KvIndexerInterface {
/// Shutdown the KV Indexer.
/// Shutdown the KV Indexer.
fn
shutdown
(
&
mut
self
);
fn
shutdown
(
&
mut
self
);
/// Dump the entire tree as RouterEvents.
///
/// ### Returns
///
/// A vector of RouterEvents representing the current state of the tree.
async
fn
dump_events
(
&
self
)
->
Result
<
Vec
<
RouterEvent
>
,
KvRouterError
>
;
}
}
/// The KV Indexer, managing the KV store and handling events and match requests.
/// The KV Indexer, managing the KV store and handling events and match requests.
...
@@ -524,6 +612,8 @@ pub struct KvIndexer {
...
@@ -524,6 +612,8 @@ pub struct KvIndexer {
match_tx
:
mpsc
::
Sender
<
MatchRequest
>
,
match_tx
:
mpsc
::
Sender
<
MatchRequest
>
,
/// A sender for remove worker requests.
/// A sender for remove worker requests.
remove_worker_tx
:
mpsc
::
Sender
<
WorkerId
>
,
remove_worker_tx
:
mpsc
::
Sender
<
WorkerId
>
,
/// A sender for dump requests.
dump_tx
:
mpsc
::
Sender
<
DumpRequest
>
,
/// A handle to the background task managing the KV store.
/// A handle to the background task managing the KV store.
task
:
OnceLock
<
std
::
thread
::
JoinHandle
<
()
>>
,
task
:
OnceLock
<
std
::
thread
::
JoinHandle
<
()
>>
,
/// The size of the KV block this indexer can handle.
/// The size of the KV block this indexer can handle.
...
@@ -549,6 +639,7 @@ impl KvIndexer {
...
@@ -549,6 +639,7 @@ impl KvIndexer {
let
(
event_tx
,
event_rx
)
=
mpsc
::
channel
::
<
RouterEvent
>
(
2048
);
let
(
event_tx
,
event_rx
)
=
mpsc
::
channel
::
<
RouterEvent
>
(
2048
);
let
(
match_tx
,
match_rx
)
=
mpsc
::
channel
::
<
MatchRequest
>
(
128
);
let
(
match_tx
,
match_rx
)
=
mpsc
::
channel
::
<
MatchRequest
>
(
128
);
let
(
remove_worker_tx
,
remove_worker_rx
)
=
mpsc
::
channel
::
<
WorkerId
>
(
16
);
let
(
remove_worker_tx
,
remove_worker_rx
)
=
mpsc
::
channel
::
<
WorkerId
>
(
16
);
let
(
dump_tx
,
dump_rx
)
=
mpsc
::
channel
::
<
DumpRequest
>
(
16
);
let
cancel_clone
=
token
.clone
();
let
cancel_clone
=
token
.clone
();
let
task
=
std
::
thread
::
spawn
(
move
||
{
let
task
=
std
::
thread
::
spawn
(
move
||
{
// create a new tokio runtime which will only perform work on a single thread
// create a new tokio runtime which will only perform work on a single thread
...
@@ -566,6 +657,7 @@ impl KvIndexer {
...
@@ -566,6 +657,7 @@ impl KvIndexer {
let
mut
match_rx
=
match_rx
;
let
mut
match_rx
=
match_rx
;
let
mut
event_rx
=
event_rx
;
let
mut
event_rx
=
event_rx
;
let
mut
remove_worker_rx
=
remove_worker_rx
;
let
mut
remove_worker_rx
=
remove_worker_rx
;
let
mut
dump_rx
=
dump_rx
;
let
mut
trie
=
RadixTree
::
new_with_frequency
(
expiration_duration
);
let
mut
trie
=
RadixTree
::
new_with_frequency
(
expiration_duration
);
loop
{
loop
{
tokio
::
select!
{
tokio
::
select!
{
...
@@ -580,6 +672,11 @@ impl KvIndexer {
...
@@ -580,6 +672,11 @@ impl KvIndexer {
let
_
=
req
.resp
.send
(
matches
);
let
_
=
req
.resp
.send
(
matches
);
}
}
Some
(
dump_req
)
=
dump_rx
.recv
()
=>
{
let
events
=
trie
.dump_tree_as_events
();
let
_
=
dump_req
.resp
.send
(
events
);
}
_
=
cancel
.cancelled
()
=>
{
_
=
cancel
.cancelled
()
=>
{
tracing
::
debug!
(
"KvCacheIndexer progress loop shutting down"
);
tracing
::
debug!
(
"KvCacheIndexer progress loop shutting down"
);
return
;
return
;
...
@@ -606,6 +703,7 @@ impl KvIndexer {
...
@@ -606,6 +703,7 @@ impl KvIndexer {
event_tx
,
event_tx
,
match_tx
,
match_tx
,
remove_worker_tx
,
remove_worker_tx
,
dump_tx
,
task
:
once
,
task
:
once
,
kv_block_size
,
kv_block_size
,
}
}
...
@@ -683,6 +781,20 @@ impl KvIndexerInterface for KvIndexer {
...
@@ -683,6 +781,20 @@ impl KvIndexerInterface for KvIndexer {
task
.join
()
.expect
(
"Failed to join kv indexer task"
);
task
.join
()
.expect
(
"Failed to join kv indexer task"
);
}
}
}
}
async
fn
dump_events
(
&
self
)
->
Result
<
Vec
<
RouterEvent
>
,
KvRouterError
>
{
let
(
resp_tx
,
resp_rx
)
=
oneshot
::
channel
();
let
dump_req
=
DumpRequest
{
resp
:
resp_tx
};
if
let
Err
(
e
)
=
self
.dump_tx
.send
(
dump_req
)
.await
{
tracing
::
error!
(
"Failed to send dump request: {:?}"
,
e
);
return
Err
(
KvRouterError
::
IndexerOffline
);
}
resp_rx
.await
.map_err
(|
_
|
KvRouterError
::
IndexerDroppedRequest
)
}
}
}
#[derive(Debug,
Clone)]
#[derive(Debug,
Clone)]
...
@@ -692,7 +804,20 @@ pub struct ShardedMatchRequest {
...
@@ -692,7 +804,20 @@ pub struct ShardedMatchRequest {
resp
:
mpsc
::
Sender
<
OverlapScores
>
,
resp
:
mpsc
::
Sender
<
OverlapScores
>
,
}
}
/// The KV Indexer, managing the KV store and handling events and match requests.
/// A sharded KV Indexer that partitions the RadixTree across multiple independent shards.
///
/// ## Sharding Strategy
/// - Each worker is **permanently assigned** to a single shard on first event
/// - All KV blocks from a worker exist only in that worker's assigned shard
/// - New workers are assigned to the shard with the fewest workers (load balancing)
///
/// ## Operation
/// - **Events**: Routed directly to the worker's assigned shard
/// - **Match requests**: Broadcast to all shards (scatter-gather pattern)
/// - **Threading**: Each shard runs in its own thread with a single-threaded runtime
///
/// This design ensures no cross-shard synchronization for writes while enabling
/// parallel processing and better scalability.
pub
struct
KvIndexerSharded
{
pub
struct
KvIndexerSharded
{
/// A `CancellationToken` for managing shutdown.
/// A `CancellationToken` for managing shutdown.
cancel
:
CancellationToken
,
cancel
:
CancellationToken
,
...
@@ -704,6 +829,7 @@ pub struct KvIndexerSharded {
...
@@ -704,6 +829,7 @@ pub struct KvIndexerSharded {
event_tx
:
Vec
<
mpsc
::
Sender
<
RouterEvent
>>
,
event_tx
:
Vec
<
mpsc
::
Sender
<
RouterEvent
>>
,
request_broadcast_tx
:
broadcast
::
Sender
<
ShardedMatchRequest
>
,
request_broadcast_tx
:
broadcast
::
Sender
<
ShardedMatchRequest
>
,
remove_worker_tx
:
Vec
<
mpsc
::
Sender
<
WorkerId
>>
,
remove_worker_tx
:
Vec
<
mpsc
::
Sender
<
WorkerId
>>
,
dump_tx
:
Vec
<
mpsc
::
Sender
<
DumpRequest
>>
,
tasks
:
Vec
<
JoinHandle
<
()
>>
,
tasks
:
Vec
<
JoinHandle
<
()
>>
,
}
}
...
@@ -730,6 +856,7 @@ impl KvIndexerSharded {
...
@@ -730,6 +856,7 @@ impl KvIndexerSharded {
let
mut
event_tx
=
Vec
::
new
();
let
mut
event_tx
=
Vec
::
new
();
let
mut
remove_worker_tx
=
Vec
::
new
();
let
mut
remove_worker_tx
=
Vec
::
new
();
let
mut
dump_tx
=
Vec
::
new
();
// Add dump channels
let
mut
tasks
=
Vec
::
new
();
let
mut
tasks
=
Vec
::
new
();
let
(
request_broadcast_tx
,
_
)
=
broadcast
::
channel
::
<
ShardedMatchRequest
>
(
1048576
);
let
(
request_broadcast_tx
,
_
)
=
broadcast
::
channel
::
<
ShardedMatchRequest
>
(
1048576
);
...
@@ -738,11 +865,13 @@ impl KvIndexerSharded {
...
@@ -738,11 +865,13 @@ impl KvIndexerSharded {
let
(
shard_event_tx
,
mut
shard_event_rx
)
=
mpsc
::
channel
::
<
RouterEvent
>
(
2048
);
let
(
shard_event_tx
,
mut
shard_event_rx
)
=
mpsc
::
channel
::
<
RouterEvent
>
(
2048
);
let
(
shard_remove_worker_tx
,
mut
shard_remove_worker_rx
)
=
let
(
shard_remove_worker_tx
,
mut
shard_remove_worker_rx
)
=
mpsc
::
channel
::
<
WorkerId
>
(
16
);
mpsc
::
channel
::
<
WorkerId
>
(
16
);
let
(
shard_dump_tx
,
mut
shard_dump_rx
)
=
mpsc
::
channel
::
<
DumpRequest
>
(
16
);
// Add dump channel
let
mut
shard_broadcast_rx
=
request_broadcast_tx
.subscribe
();
let
mut
shard_broadcast_rx
=
request_broadcast_tx
.subscribe
();
let
cancel
=
token
.clone
();
let
cancel
=
token
.clone
();
event_tx
.push
(
shard_event_tx
);
event_tx
.push
(
shard_event_tx
);
remove_worker_tx
.push
(
shard_remove_worker_tx
);
remove_worker_tx
.push
(
shard_remove_worker_tx
);
dump_tx
.push
(
shard_dump_tx
);
// Store dump sender
let
runtime
=
tokio
::
runtime
::
Builder
::
new_multi_thread
()
let
runtime
=
tokio
::
runtime
::
Builder
::
new_multi_thread
()
.worker_threads
(
1
)
.worker_threads
(
1
)
...
@@ -771,6 +900,11 @@ impl KvIndexerSharded {
...
@@ -771,6 +900,11 @@ impl KvIndexerSharded {
}
}
}
}
Some
(
dump_req
)
=
shard_dump_rx
.recv
()
=>
{
let
events
=
trie
.dump_tree_as_events
();
let
_
=
dump_req
.resp
.send
(
events
);
}
_
=
cancel
.cancelled
()
=>
{
_
=
cancel
.cancelled
()
=>
{
tracing
::
trace!
(
"KvCacheIndexer progress loop shutting down"
);
tracing
::
trace!
(
"KvCacheIndexer progress loop shutting down"
);
return
;
return
;
...
@@ -798,6 +932,7 @@ impl KvIndexerSharded {
...
@@ -798,6 +932,7 @@ impl KvIndexerSharded {
event_tx
,
event_tx
,
request_broadcast_tx
,
request_broadcast_tx
,
remove_worker_tx
,
remove_worker_tx
,
dump_tx
,
// Add dump_tx field
tasks
,
tasks
,
}
}
}
}
...
@@ -905,6 +1040,35 @@ impl KvIndexerInterface for KvIndexerSharded {
...
@@ -905,6 +1040,35 @@ impl KvIndexerInterface for KvIndexerSharded {
self
.tasks
.pop
()
.unwrap
()
.join
()
.unwrap
();
self
.tasks
.pop
()
.unwrap
()
.join
()
.unwrap
();
}
}
}
}
async
fn
dump_events
(
&
self
)
->
Result
<
Vec
<
RouterEvent
>
,
KvRouterError
>
{
let
mut
all_events
=
Vec
::
new
();
// Create channels for each shard
let
mut
receivers
=
Vec
::
new
();
for
shard_dump_tx
in
&
self
.dump_tx
{
let
(
resp_tx
,
resp_rx
)
=
oneshot
::
channel
();
let
dump_req
=
DumpRequest
{
resp
:
resp_tx
};
if
let
Err
(
e
)
=
shard_dump_tx
.send
(
dump_req
)
.await
{
tracing
::
error!
(
"Failed to send dump request to shard: {:?}"
,
e
);
return
Err
(
KvRouterError
::
IndexerOffline
);
}
receivers
.push
(
resp_rx
);
}
// Collect results from all shards
for
resp_rx
in
receivers
{
match
resp_rx
.await
{
Ok
(
events
)
=>
all_events
.extend
(
events
),
Err
(
_
)
=>
return
Err
(
KvRouterError
::
IndexerDroppedRequest
),
}
}
Ok
(
all_events
)
}
}
}
#[cfg(test)]
#[cfg(test)]
...
@@ -1559,4 +1723,177 @@ mod tests {
...
@@ -1559,4 +1723,177 @@ mod tests {
let
overlap_scores
:
OverlapScores
=
Default
::
default
();
let
overlap_scores
:
OverlapScores
=
Default
::
default
();
assert
!
(
overlap_scores
.scores
.is_empty
());
assert
!
(
overlap_scores
.scores
.is_empty
());
}
}
#[tokio::test]
async
fn
test_dump_tree_as_events_round_trip
()
{
setup
();
// Configuration
let
kv_block_size
=
32
;
let
num_shards
=
2
;
// Build a non-trivial indexer with events
let
token1
=
CancellationToken
::
new
();
let
mut
original_indexer
=
KvIndexerSharded
::
new
(
token1
.clone
(),
num_shards
,
kv_block_size
);
let
worker_0
=
0
;
let
worker_1
=
1
;
let
worker_2
=
2
;
// Apply events to the original indexer
original_indexer
.apply_event
(
create_store_event
(
worker_0
,
0
,
vec!
[
1
,
2
,
3
],
None
))
.await
;
original_indexer
.apply_event
(
create_store_event
(
worker_1
,
1
,
vec!
[
1
,
2
,
3
],
None
))
.await
;
original_indexer
.apply_event
(
create_store_event
(
worker_1
,
2
,
vec!
[
4
,
5
],
Some
(
ExternalSequenceBlockHash
(
100
)),
))
.await
;
original_indexer
.apply_event
(
create_store_event
(
worker_2
,
3
,
vec!
[
6
,
7
],
None
))
.await
;
original_indexer
.apply_event
(
create_store_event
(
worker_0
,
4
,
vec!
[
4
],
Some
(
ExternalSequenceBlockHash
(
100
)),
))
.await
;
// Allow some time for events to be processed
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
50
))
.await
;
// Dump the original indexer
let
dump1
=
original_indexer
.dump_events
()
.await
.unwrap
();
println!
(
"Dumped {} events"
,
dump1
.len
());
// Create a new indexer and apply all dumped events
let
token2
=
CancellationToken
::
new
();
let
mut
reconstructed_indexer
=
KvIndexerSharded
::
new
(
token2
.clone
(),
num_shards
,
kv_block_size
);
for
event
in
&
dump1
{
reconstructed_indexer
.apply_event
(
event
.clone
())
.await
;
}
// Allow some time for events to be processed
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
50
))
.await
;
// Dump the reconstructed indexer
let
dump2
=
reconstructed_indexer
.dump_events
()
.await
.unwrap
();
// Sort both dumps for comparison (order might differ due to HashMap iteration and sharding)
let
mut
sorted_dump1
=
dump1
.clone
();
let
mut
sorted_dump2
=
dump2
.clone
();
// Sort by (worker_id, tokens_hash, parent_hash)
let
sort_key
=
|
event
:
&
RouterEvent
|
{
if
let
KvCacheEventData
::
Stored
(
ref
data
)
=
event
.event.data
{
(
event
.worker_id
,
data
.blocks
.first
()
.map
(|
b
|
b
.tokens_hash
.0
)
.unwrap_or
(
0
),
data
.parent_hash
.map
(|
h
|
h
.0
)
.unwrap_or
(
0
),
)
}
else
{
(
event
.worker_id
,
0
,
0
)
}
};
sorted_dump1
.sort_by_key
(
sort_key
);
sorted_dump2
.sort_by_key
(
sort_key
);
// Verify the dumps have the same length
assert_eq!
(
sorted_dump1
.len
(),
sorted_dump2
.len
(),
"Dumps have different lengths: {} vs {}"
,
sorted_dump1
.len
(),
sorted_dump2
.len
()
);
// Verify each event matches
for
(
i
,
(
event1
,
event2
))
in
sorted_dump1
.iter
()
.zip
(
sorted_dump2
.iter
())
.enumerate
()
{
assert_eq!
(
event1
.worker_id
,
event2
.worker_id
,
"Event {} worker_id mismatch"
,
i
);
if
let
(
KvCacheEventData
::
Stored
(
data1
),
KvCacheEventData
::
Stored
(
data2
))
=
(
&
event1
.event.data
,
&
event2
.event.data
)
{
assert_eq!
(
data1
.parent_hash
,
data2
.parent_hash
,
"Event {} parent_hash mismatch"
,
i
);
assert_eq!
(
data1
.blocks
.len
(),
data2
.blocks
.len
(),
"Event {} blocks length mismatch"
,
i
);
for
(
j
,
(
block1
,
block2
))
in
data1
.blocks
.iter
()
.zip
(
data2
.blocks
.iter
())
.enumerate
()
{
assert_eq!
(
block1
.tokens_hash
,
block2
.tokens_hash
,
"Event {} block {} tokens_hash mismatch"
,
i
,
j
);
assert_eq!
(
block1
.block_hash
,
block2
.block_hash
,
"Event {} block {} block_hash mismatch"
,
i
,
j
);
}
}
else
{
panic!
(
"Expected Stored events in both dumps"
);
}
}
// Also verify that both indexers produce the same match results
for
test_seq
in
[
vec!
[
LocalBlockHash
(
1
),
LocalBlockHash
(
2
),
LocalBlockHash
(
3
)],
vec!
[
LocalBlockHash
(
1
),
LocalBlockHash
(
4
),
LocalBlockHash
(
5
)],
vec!
[
LocalBlockHash
(
6
),
LocalBlockHash
(
7
)],
vec!
[
LocalBlockHash
(
1
)],
]
{
let
scores1
=
original_indexer
.find_matches
(
test_seq
.clone
())
.await
.unwrap
();
let
scores2
=
reconstructed_indexer
.find_matches
(
test_seq
.clone
())
.await
.unwrap
();
// Sort the scores to compare
let
mut
scores1_sorted
:
Vec
<
_
>
=
scores1
.scores
.iter
()
.collect
();
let
mut
scores2_sorted
:
Vec
<
_
>
=
scores2
.scores
.iter
()
.collect
();
scores1_sorted
.sort_by_key
(|(
k
,
_
)|
*
k
);
scores2_sorted
.sort_by_key
(|(
k
,
_
)|
*
k
);
assert_eq!
(
scores1_sorted
,
scores2_sorted
,
"Match scores differ for sequence {:?}"
,
test_seq
);
}
// Clean up
original_indexer
.shutdown
();
reconstructed_indexer
.shutdown
();
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment