Skip to main content

prolly/prolly/
parallel.rs

1//! Parallel rebalancing operations for Prolly trees
2//!
3//! This module provides parallel processing capabilities for tree operations,
4//! enabling efficient use of multi-core systems for large trees.
5//!
6//! # Overview
7//!
8//! The ParallelRebalancer trait and its default implementation enable:
9//! - Concurrent processing of independent subtrees during rebalancing
10//! - Parallel batch mutation processing for leaf groups
11//! - Threshold-based fallback to sequential processing for small trees
12//!
13//! # Configuration
14//!
15//! Use [`ParallelConfig`] to control parallel behavior:
16//! - `max_threads`: Maximum number of threads (0 = use rayon default)
17//! - `parallelism_threshold`: Minimum items before parallelization kicks in
18//!
19//! # Example
20//!
21//! ```rust
22//! use prolly::{Prolly, MemStore, Config, Mutation, ParallelRebalancer, DefaultParallelRebalancer, ParallelConfig};
23//! use std::sync::Arc;
24//!
25//! let store = Arc::new(MemStore::new());
26//! let config = Config::default();
27//! let prolly = Prolly::new(Arc::clone(&store), config);
28//! let tree = prolly.create();
29//!
30//! // Create mutations
31//! let mutations: Vec<Mutation> = (0..100)
32//!     .map(|i| Mutation::Upsert {
33//!         key: format!("key{:04}", i).into_bytes(),
34//!         val: format!("val{}", i).into_bytes(),
35//!     })
36//!     .collect();
37//!
38//! // Apply mutations with parallel processing
39//! let rebalancer = DefaultParallelRebalancer::new();
40//! let parallel_config = ParallelConfig::default();
41//! let new_tree = rebalancer.parallel_batch(&store, &prolly, &tree, mutations, &parallel_config).unwrap();
42//! ```
43//!
44//! # Error Handling
45//!
46//! Error propagation is handled through rayon's `collect()` mechanism:
47//! - When any parallel operation fails, the first error is propagated
48//! - Remaining parallel work is cancelled (rayon's short-circuit behavior)
49//! - All errors are wrapped in `Error::Store` for consistency
50
51use rayon::prelude::*;
52
53use super::cid::Cid;
54use super::error::{Error, Mutation};
55use super::node::Node;
56use super::store::Store;
57use super::tree::Tree;
58
59use super::batch::{BatchApplyResult, BatchWriter, BatchWriterConfig};
60use super::rebalance;
61use super::Prolly;
62
63/// Configuration for parallel rebalancing operations.
64///
65/// Controls how and when parallel processing is used during tree operations.
66///
67/// # Example
68/// ```rust
69/// use prolly::ParallelConfig;
70///
71/// // Use defaults (rayon's thread count, threshold of 100)
72/// let config = ParallelConfig::default();
73///
74/// // Custom configuration
75/// let config = ParallelConfig {
76///     max_threads: 4,
77///     parallelism_threshold: 50,
78/// };
79/// ```
80#[derive(Clone, Debug)]
81pub struct ParallelConfig {
82    /// Maximum number of threads to use.
83    ///
84    /// Set to 0 to use rayon's default (usually the number of CPU cores).
85    pub max_threads: usize,
86
87    /// Minimum number of items before parallelization kicks in.
88    ///
89    /// If the number of items to process is below this threshold,
90    /// sequential processing is used instead for efficiency.
91    pub parallelism_threshold: usize,
92}
93
94impl Default for ParallelConfig {
95    fn default() -> Self {
96        Self {
97            max_threads: 0, // Use rayon default (usually num_cpus)
98            parallelism_threshold: 100,
99        }
100    }
101}
102
103impl ParallelConfig {
104    /// Create a new ParallelConfig with custom settings.
105    pub fn new(max_threads: usize, parallelism_threshold: usize) -> Self {
106        Self {
107            max_threads,
108            parallelism_threshold,
109        }
110    }
111
112    /// Create a config that always uses sequential processing.
113    pub fn sequential() -> Self {
114        Self {
115            max_threads: 1,
116            parallelism_threshold: usize::MAX,
117        }
118    }
119}
120
121fn batch_writer_config(config: &ParallelConfig, mutation_count: usize) -> BatchWriterConfig {
122    let default_config = BatchWriterConfig::new();
123    let prefetch_parallelism = if mutation_count < config.parallelism_threshold {
124        1
125    } else if config.max_threads == 0 {
126        default_config.prefetch_parallelism
127    } else {
128        config.max_threads
129    };
130
131    default_config.with_prefetch_parallelism(prefetch_parallelism.max(1))
132}
133
134pub(crate) fn parallel_batch_with_stats<S: Store>(
135    prolly: &Prolly<S>,
136    tree: &Tree,
137    mutations: Vec<Mutation>,
138    config: &ParallelConfig,
139) -> Result<BatchApplyResult, Error> {
140    let writer_config = batch_writer_config(config, mutations.len());
141    BatchWriter::with_config(writer_config).apply_batch_with_stats(prolly, tree, mutations)
142}
143
144/// Trait for parallel rebalancing operations.
145///
146/// Implementations of this trait provide parallel processing capabilities
147/// for tree rebalancing and batch mutation operations.
148///
149/// # Example
150/// ```rust
151/// use prolly::{Prolly, MemStore, Config, Mutation, ParallelRebalancer, DefaultParallelRebalancer, ParallelConfig};
152///
153/// let store = MemStore::new();
154/// let prolly = Prolly::new(store, Config::default());
155/// let tree = prolly.create();
156///
157/// let rebalancer = DefaultParallelRebalancer;
158/// let config = ParallelConfig::default();
159///
160/// let mutations = vec![
161///     Mutation::Upsert { key: b"a".to_vec(), val: b"1".to_vec() },
162///     Mutation::Upsert { key: b"b".to_vec(), val: b"2".to_vec() },
163/// ];
164///
165/// // Note: For direct trait usage, store and prolly must use the same store type
166/// // For convenience, use prolly.parallel_batch() instead
167/// ```
168pub trait ParallelRebalancer<S: Store> {
169    /// Rebalance multiple nodes in parallel.
170    ///
171    /// Processes independent subtrees concurrently using a thread pool.
172    /// Falls back to sequential processing when below the parallelism threshold.
173    ///
174    /// # Arguments
175    /// * `store` - The storage backend
176    /// * `prolly` - The Prolly tree manager
177    /// * `nodes` - Nodes to rebalance with their ancestor paths
178    /// * `config` - Parallel configuration
179    ///
180    /// # Returns
181    /// * `Ok(Vec<Cid>)` - Vector of new root CIDs for each rebalanced subtree
182    /// * `Err(Error)` - On storage or processing errors
183    fn parallel_rebalance(
184        &self,
185        store: &S,
186        prolly: &Prolly<S>,
187        nodes: Vec<(Node, Vec<(Node, usize)>)>,
188        config: &ParallelConfig,
189    ) -> Result<Vec<Cid>, Error>;
190
191    /// Apply batch mutations with parallel leaf processing.
192    ///
193    /// Groups mutations by target leaf and processes independent leaf groups
194    /// in parallel when beneficial. Uses batch_get and batch_put for efficient I/O.
195    ///
196    /// # Arguments
197    /// * `store` - The storage backend
198    /// * `prolly` - The Prolly tree manager
199    /// * `tree` - The tree to modify
200    /// * `mutations` - Vector of mutations to apply
201    /// * `config` - Parallel configuration
202    ///
203    /// # Returns
204    /// * `Ok(Tree)` - New tree with all mutations applied
205    /// * `Err(Error)` - On storage or processing errors
206    fn parallel_batch(
207        &self,
208        _store: &S,
209        prolly: &Prolly<S>,
210        tree: &Tree,
211        mutations: Vec<Mutation>,
212        config: &ParallelConfig,
213    ) -> Result<Tree, Error>;
214}
215
216/// Default implementation of ParallelRebalancer using rayon.
217///
218/// This implementation uses rayon's parallel iterators for concurrent processing
219/// and provides threshold-based fallback to sequential processing for small workloads.
220#[derive(Clone, Debug, Default)]
221pub struct DefaultParallelRebalancer;
222
223impl DefaultParallelRebalancer {
224    /// Create a new DefaultParallelRebalancer.
225    pub fn new() -> Self {
226        Self
227    }
228
229    /// Sequential rebalance for small workloads.
230    fn sequential_rebalance<S: Store>(
231        &self,
232        prolly: &Prolly<S>,
233        nodes: Vec<(Node, Vec<(Node, usize)>)>,
234    ) -> Result<Vec<Cid>, Error> {
235        nodes
236            .into_iter()
237            .map(|(node, ancestors)| rebalance::rebalance(prolly, node, &ancestors))
238            .collect()
239    }
240}
241
242impl<S: Store> ParallelRebalancer<S> for DefaultParallelRebalancer {
243    fn parallel_rebalance(
244        &self,
245        _store: &S,
246        prolly: &Prolly<S>,
247        nodes: Vec<(Node, Vec<(Node, usize)>)>,
248        config: &ParallelConfig,
249    ) -> Result<Vec<Cid>, Error> {
250        // Fall back to sequential if below threshold
251        if nodes.len() < config.parallelism_threshold {
252            return self.sequential_rebalance(prolly, nodes);
253        }
254
255        // Configure thread pool if max_threads is specified
256        if config.max_threads > 0 {
257            let pool = rayon::ThreadPoolBuilder::new()
258                .num_threads(config.max_threads)
259                .build()
260                .map_err(|e| {
261                    Error::Store(Box::new(std::io::Error::other(format!(
262                        "Failed to create thread pool: {}",
263                        e
264                    ))))
265                })?;
266
267            pool.install(|| {
268                nodes
269                    .into_par_iter()
270                    .map(|(node, ancestors)| rebalance::rebalance(prolly, node, &ancestors))
271                    .collect()
272            })
273        } else {
274            // Use rayon's default thread pool
275            nodes
276                .into_par_iter()
277                .map(|(node, ancestors)| rebalance::rebalance(prolly, node, &ancestors))
278                .collect()
279        }
280    }
281
282    fn parallel_batch(
283        &self,
284        _store: &S,
285        prolly: &Prolly<S>,
286        tree: &Tree,
287        mutations: Vec<Mutation>,
288        config: &ParallelConfig,
289    ) -> Result<Tree, Error> {
290        Ok(parallel_batch_with_stats(prolly, tree, mutations, config)?.tree)
291    }
292}