laminar_db/process_function/
benchmark.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use arrow::array::RecordBatch;
7use laminar_core::checkpoint::{CheckpointAssignmentFence, CheckpointParticipant};
8use laminar_core::cluster::control::LeaseDeadline;
9use laminar_core::shuffle::{ShuffleReceiver, ShuffleSender};
10use laminar_core::state::{NodeId, VnodeRegistry};
11use uuid::Uuid;
12
13use super::{NativeProcessFunction, ProcessFunctionDescriptor, ProcessFunctionOperator};
14use crate::error::DbError;
15use crate::operator::sql_query::ClusterShuffleConfig;
16use crate::operator_graph::{GraphOperator, InputFrontier};
17
18const VNODE_COUNT: u32 = 1_024;
19
20#[derive(Clone, Copy)]
22pub enum NativeProcessBenchmarkMode {
23 Local,
25 SingleOwner,
27 TwoOwners,
29}
30
31pub struct NativeProcessBenchmark {
33 operator: ProcessFunctionOperator,
34 peer: Option<BenchmarkPeer>,
35}
36
37struct BenchmarkPeer {
38 operator: ProcessFunctionOperator,
39 receiver: Arc<ShuffleReceiver>,
40}
41
42impl NativeProcessBenchmark {
43 pub async fn new(
48 descriptor: ProcessFunctionDescriptor,
49 handler: Arc<dyn NativeProcessFunction>,
50 mode: NativeProcessBenchmarkMode,
51 ) -> Result<Self, DbError> {
52 if matches!(mode, NativeProcessBenchmarkMode::Local) {
53 return Ok(Self {
54 operator: ProcessFunctionOperator::new(descriptor, handler, VNODE_COUNT)?,
55 peer: None,
56 });
57 }
58 let owners = match mode {
59 NativeProcessBenchmarkMode::SingleOwner => vec![7; VNODE_COUNT as usize],
60 NativeProcessBenchmarkMode::TwoOwners => (0..VNODE_COUNT)
61 .map(|vnode| 7 + u64::from(vnode % 2))
62 .collect(),
63 NativeProcessBenchmarkMode::Local => unreachable!("local fixture returned above"),
64 };
65 let assignment = CheckpointAssignmentFence::from_owner_map(
66 7,
67 &owners,
68 owners
69 .iter()
70 .copied()
71 .collect::<std::collections::BTreeSet<_>>()
72 .into_iter()
73 .map(|node_id| CheckpointParticipant {
74 node_id,
75 boot_incarnation: Uuid::from_u128(u128::from(node_id)),
76 })
77 .collect(),
78 )
79 .map_err(DbError::Config)?;
80 let (operator, scope) = cluster_operator(
81 descriptor.clone(),
82 Arc::clone(&handler),
83 7,
84 &owners,
85 &assignment,
86 )
87 .await?;
88 let peer = if matches!(mode, NativeProcessBenchmarkMode::TwoOwners) {
89 let (operator, remote) =
90 cluster_operator(descriptor, handler, 8, &owners, &assignment).await?;
91 scope.sender.register_peer(8, remote.receiver.local_addr());
92 remote.sender.register_peer(7, scope.receiver.local_addr());
93 Some(BenchmarkPeer {
94 operator,
95 receiver: remote.receiver,
96 })
97 } else {
98 None
99 };
100 Ok(Self { operator, peer })
101 }
102
103 pub async fn step(&mut self, batch: &RecordBatch) -> Result<Vec<RecordBatch>, DbError> {
109 if self.peer.is_some() {
110 return self.step_two_owners(batch).await;
111 }
112 self.operator
113 .process_with_frontiers(
114 &[vec![batch.clone()]],
115 &[InputFrontier {
116 watermark: None,
117 idle: false,
118 }],
119 )
120 .await
121 }
122
123 async fn step_two_owners(&mut self, batch: &RecordBatch) -> Result<Vec<RecordBatch>, DbError> {
124 let peer = self
125 .peer
126 .as_mut()
127 .ok_or_else(|| DbError::Config("benchmark peer is missing".into()))?;
128 let before = self
129 .operator
130 .accepted_activations()
131 .saturating_add(peer.operator.accepted_activations());
132 let frontier = [InputFrontier::default()];
133 let mut output = self
134 .operator
135 .process_with_frontiers(&[vec![batch.clone()]], &frontier)
136 .await?;
137 tokio::time::timeout(Duration::from_secs(5), async {
138 loop {
139 for (stage, batch) in peer.receiver.drain_checkpointed_staged() {
140 peer.operator.stage_checkpointed_shuffle(
141 &stage,
142 crate::operator::RetainedBatch::from_received(batch),
143 i64::MIN,
144 )?;
145 }
146 output.extend(self.operator.process_with_frontiers(&[], &frontier).await?);
147 output.extend(peer.operator.process_with_frontiers(&[], &frontier).await?);
148 let processed = self
149 .operator
150 .accepted_activations()
151 .saturating_add(peer.operator.accepted_activations())
152 .saturating_sub(before);
153 if processed == batch.num_rows() as u64
154 && !self.operator.checkpoint_drain_pending()
155 && !peer.operator.checkpoint_drain_pending()
156 {
157 break Ok(output);
158 }
159 tokio::task::yield_now().await;
160 }
161 })
162 .await
163 .map_err(|_| DbError::Config("two-owner benchmark drain timed out".into()))?
164 }
165}
166
167async fn cluster_operator(
168 descriptor: ProcessFunctionDescriptor,
169 handler: Arc<dyn NativeProcessFunction>,
170 node: u64,
171 owners: &[u64],
172 assignment: &CheckpointAssignmentFence,
173) -> Result<(ProcessFunctionOperator, ClusterShuffleConfig), DbError> {
174 let mut operator = ProcessFunctionOperator::new(descriptor, handler, VNODE_COUNT)?;
175 let boot = Uuid::from_u128(u128::from(node));
176 let registry = Arc::new(VnodeRegistry::single_owner(VNODE_COUNT, NodeId(node)));
177 registry.set_assignment_and_version(
178 Arc::from(owners.iter().copied().map(NodeId).collect::<Vec<_>>()),
179 7,
180 );
181 let sender = Arc::new(ShuffleSender::new(node, boot));
182 let receiver = Arc::new(
183 ShuffleReceiver::bind(node, std::net::SocketAddr::from(([127, 0, 0, 1], 0)), boot)
184 .await
185 .map_err(|error| DbError::Config(error.to_string()))?,
186 );
187 let deadline = Arc::new(LeaseDeadline::live_for(Duration::from_secs(300)));
188 sender
189 .bind_process_lease_deadline_pair(&receiver, Arc::clone(&deadline))
190 .map_err(|error| DbError::Config(error.to_string()))?;
191 sender
192 .install_assignment_fence(assignment, owners)
193 .map_err(|error| DbError::Config(error.to_string()))?;
194 receiver
195 .install_assignment_fence(assignment, owners)
196 .map_err(|error| DbError::Config(error.to_string()))?;
197 sender.set_recovery_gen(3);
198 receiver.set_recovery_gen(3);
199 operator.require_cluster_execution(
200 "activity",
201 tokio::runtime::Handle::current(),
202 NodeId(node),
203 )?;
204 operator.bind_startup_assignment(
205 assignment,
206 &owners
207 .iter()
208 .enumerate()
209 .filter_map(|(vnode, owner)| {
210 (*owner == node)
211 .then_some(u32::try_from(vnode).expect("fixed benchmark vnode domain fits u32"))
212 })
213 .collect::<Vec<_>>(),
214 )?;
215 let scope = ClusterShuffleConfig {
216 registry,
217 sender,
218 receiver,
219 self_id: NodeId(node),
220 topology: None,
221 };
222 operator.bind_process_execution_authority(&scope, deadline)?;
223 Ok((operator, scope))
224}