1use std::collections::BTreeMap;
4
5use crate::active::{ActiveReader, ActiveWriter};
6use crate::protocol::{ManifestEntry, PeerAccess};
7use crate::region::{PreparedRegion, RegionId, WriterEndpoint};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum BatchError {
12 InvalidLimits,
14 Empty,
16 TooManyRegions,
18 DuplicateRegionId(RegionId),
20 BatchBytesExceeded,
22 InvalidRegionLength,
24 UnknownRegion(RegionId),
26 WrongDirection(RegionId),
28 CommitMismatch,
30}
31
32impl core::fmt::Display for BatchError {
33 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34 write!(formatter, "region batch operation failed: {self:?}")
35 }
36}
37
38impl std::error::Error for BatchError {}
39
40pub struct TransferBatch {
42 regions: Vec<PreparedRegion>,
43 max_regions: usize,
44 max_region_bytes: u64,
45 max_batch_bytes: u64,
46 total_logical: u64,
47 total_mapped: u64,
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub struct ExpectedRegion {
53 pub(crate) id: RegionId,
54 pub(crate) writer: WriterEndpoint,
55 pub(crate) logical_len: usize,
56}
57
58pub struct ExpectedBatch {
61 pub(crate) regions: Vec<ExpectedRegion>,
62 pub(crate) total_logical: u64,
63}
64
65impl ExpectedRegion {
66 pub const fn new(id: RegionId, writer: WriterEndpoint, logical_len: usize) -> Self {
68 Self {
69 id,
70 writer,
71 logical_len,
72 }
73 }
74
75 pub const fn id(self) -> RegionId {
77 self.id
78 }
79
80 pub const fn writer(self) -> WriterEndpoint {
82 self.writer
83 }
84
85 pub const fn logical_len(self) -> usize {
87 self.logical_len
88 }
89}
90
91impl ExpectedBatch {
92 pub fn try_from_regions(mut regions: Vec<ExpectedRegion>) -> Result<Self, BatchError> {
94 if regions.is_empty() {
95 return Err(BatchError::Empty);
96 }
97 if regions.len() > 16 {
98 return Err(BatchError::TooManyRegions);
99 }
100 regions.sort_unstable_by_key(|region| region.id);
101 if let Some(duplicate) = regions
102 .windows(2)
103 .find(|pair| pair[0].id == pair[1].id)
104 .map(|pair| pair[0].id)
105 {
106 return Err(BatchError::DuplicateRegionId(duplicate));
107 }
108 let total_logical = regions.iter().try_fold(0_u64, |total, region| {
109 let logical = u64::try_from(region.logical_len)
110 .ok()
111 .filter(|logical| *logical != 0)
112 .ok_or(BatchError::InvalidRegionLength)?;
113 total
114 .checked_add(logical)
115 .ok_or(BatchError::BatchBytesExceeded)
116 })?;
117 Ok(Self {
118 regions,
119 total_logical,
120 })
121 }
122
123 pub fn len(&self) -> usize {
125 self.regions.len()
126 }
127
128 pub fn is_empty(&self) -> bool {
130 self.regions.is_empty()
131 }
132
133 pub(crate) fn try_from_specs(regions: Vec<ExpectedRegion>) -> Result<Self, BatchError> {
134 Self::try_from_regions(regions)
135 }
136}
137
138impl TransferBatch {
139 #[allow(dead_code)]
140 pub(crate) fn new(
141 max_regions: u16,
142 max_region_bytes: u64,
143 max_batch_bytes: u64,
144 ) -> Result<Self, BatchError> {
145 let max_regions = usize::from(max_regions);
146 if max_regions == 0 || max_regions > 16 {
147 return Err(BatchError::TooManyRegions);
148 }
149 if max_region_bytes == 0 || max_batch_bytes == 0 {
150 return Err(BatchError::InvalidLimits);
151 }
152 Ok(Self {
153 regions: Vec::with_capacity(max_regions),
154 max_regions,
155 max_region_bytes,
156 max_batch_bytes,
157 total_logical: 0,
158 total_mapped: 0,
159 })
160 }
161
162 pub fn add(&mut self, region: PreparedRegion) -> Result<(), BatchError> {
164 if self.regions.len() == self.max_regions {
165 return Err(BatchError::TooManyRegions);
166 }
167 let id = region.spec().id;
168 if self.regions.iter().any(|existing| existing.spec().id == id) {
169 return Err(BatchError::DuplicateRegionId(id));
170 }
171 let logical =
172 u64::try_from(region.logical_len()).map_err(|_| BatchError::BatchBytesExceeded)?;
173 let mapped =
174 u64::try_from(region.mapped_len()).map_err(|_| BatchError::BatchBytesExceeded)?;
175 if logical > self.max_region_bytes {
176 return Err(BatchError::InvalidRegionLength);
177 }
178 let total_logical = self
179 .total_logical
180 .checked_add(logical)
181 .ok_or(BatchError::BatchBytesExceeded)?;
182 let total_mapped = self
183 .total_mapped
184 .checked_add(mapped)
185 .ok_or(BatchError::BatchBytesExceeded)?;
186 if total_logical > self.max_batch_bytes || total_mapped > self.max_batch_bytes {
187 return Err(BatchError::BatchBytesExceeded);
188 }
189 self.total_logical = total_logical;
190 self.total_mapped = total_mapped;
191 self.regions.push(region);
192 Ok(())
193 }
194
195 pub fn len(&self) -> usize {
197 self.regions.len()
198 }
199
200 pub fn is_empty(&self) -> bool {
202 self.regions.is_empty()
203 }
204
205 pub(crate) fn reservation_lengths(&self) -> Vec<u64> {
206 let mut lengths = self
207 .regions
208 .iter()
209 .map(|region| {
210 (
211 region.spec().id,
212 u64::try_from(region.mapped_len()).expect("prepared length is native"),
213 )
214 })
215 .collect::<Vec<_>>();
216 lengths.sort_unstable_by_key(|(id, _)| *id);
217 lengths.into_iter().map(|(_, length)| length).collect()
218 }
219
220 pub(crate) fn manifest_entries(&self) -> Option<Vec<ManifestEntry>> {
221 self.regions
222 .iter()
223 .map(|region| {
224 let access = match region.spec().writer {
225 WriterEndpoint::Coordinator => PeerAccess::ReadOnly,
226 WriterEndpoint::Receiver => PeerAccess::SoleWriter,
227 };
228 Some(ManifestEntry::from_native(
229 region.request.native_spec(region.spec().id.get())?,
230 access,
231 ))
232 })
233 .collect()
234 }
235
236 #[allow(dead_code)]
237 pub(crate) fn into_pending(self) -> Result<PendingBatch, BatchError> {
238 if self.regions.is_empty() {
239 return Err(BatchError::Empty);
240 }
241 Ok(PendingBatch {
242 regions: self.regions,
243 total_logical: self.total_logical,
244 total_mapped: self.total_mapped,
245 })
246 }
247}
248
249#[allow(dead_code)]
251pub(crate) struct PendingBatch {
252 pub(crate) regions: Vec<PreparedRegion>,
253 pub(crate) total_logical: u64,
254 pub(crate) total_mapped: u64,
255}
256
257impl PendingBatch {
258 pub(crate) fn manifest_entries(&self) -> Option<Vec<ManifestEntry>> {
259 self.regions
260 .iter()
261 .map(|region| {
262 let access = match region.spec().writer {
263 WriterEndpoint::Coordinator => PeerAccess::ReadOnly,
264 WriterEndpoint::Receiver => PeerAccess::SoleWriter,
265 };
266 Some(ManifestEntry::from_native(
267 region.request.native_spec(region.spec().id.get())?,
268 access,
269 ))
270 })
271 .collect()
272 }
273}
274
275#[allow(dead_code)]
276pub(crate) enum CommittedRegion {
277 Reader(ActiveReader),
278 Writer(ActiveWriter),
279}
280
281#[derive(Clone, Copy, Debug, Eq, PartialEq)]
283pub(crate) enum LocalRegionAuthority {
284 Reader,
285 Writer,
286}
287
288pub struct ActiveRegionSet {
290 regions: BTreeMap<RegionId, CommittedRegion>,
291}
292
293impl ActiveRegionSet {
294 #[allow(dead_code)]
295 pub(crate) fn from_committed(
296 pending: PendingBatch,
297 regions: impl IntoIterator<Item = (RegionId, CommittedRegion)>,
298 ) -> Result<Self, BatchError> {
299 let expected = pending.regions.iter().map(|region| {
300 let authority = match region.spec().writer {
301 WriterEndpoint::Coordinator => LocalRegionAuthority::Writer,
302 WriterEndpoint::Receiver => LocalRegionAuthority::Reader,
303 };
304 (region.spec().id, authority)
305 });
306 let result = Self::from_local_committed(expected, regions);
307 drop(pending);
308 result
309 }
310
311 pub(crate) fn from_local_committed(
312 expected: impl IntoIterator<Item = (RegionId, LocalRegionAuthority)>,
313 regions: impl IntoIterator<Item = (RegionId, CommittedRegion)>,
314 ) -> Result<Self, BatchError> {
315 let mut expected = expected.into_iter().collect::<Vec<_>>();
316 if expected.len() > 16 {
317 return Err(BatchError::CommitMismatch);
318 }
319 expected.sort_unstable_by_key(|(id, _)| *id);
320 if let Some(duplicate) = expected
321 .windows(2)
322 .find(|pair| pair[0].0 == pair[1].0)
323 .map(|pair| pair[0].0)
324 {
325 return Err(BatchError::DuplicateRegionId(duplicate));
326 }
327 let mut keyed = BTreeMap::new();
328 for (id, region) in regions {
329 if keyed.insert(id, region).is_some() {
330 return Err(BatchError::DuplicateRegionId(id));
331 }
332 }
333 if keyed.len() != expected.len() || keyed.len() > 16 {
334 return Err(BatchError::CommitMismatch);
335 }
336 for (id, authority) in expected {
337 match (authority, keyed.get(&id)) {
338 (LocalRegionAuthority::Writer, Some(CommittedRegion::Writer(_)))
339 | (LocalRegionAuthority::Reader, Some(CommittedRegion::Reader(_))) => {}
340 _ => return Err(BatchError::CommitMismatch),
341 }
342 }
343 Ok(Self { regions: keyed })
344 }
345
346 pub fn len(&self) -> usize {
348 self.regions.len()
349 }
350
351 pub fn is_empty(&self) -> bool {
353 self.regions.is_empty()
354 }
355
356 pub fn take_writer(&mut self, id: RegionId) -> Result<ActiveWriter, BatchError> {
358 match self.regions.get(&id) {
359 None => return Err(BatchError::UnknownRegion(id)),
360 Some(CommittedRegion::Reader(_)) => return Err(BatchError::WrongDirection(id)),
361 Some(CommittedRegion::Writer(_)) => {}
362 }
363 match self.regions.remove(&id) {
364 Some(CommittedRegion::Writer(writer)) => Ok(writer),
365 Some(CommittedRegion::Reader(_)) => Err(BatchError::WrongDirection(id)),
366 None => Err(BatchError::UnknownRegion(id)),
367 }
368 }
369
370 pub fn take_reader(&mut self, id: RegionId) -> Result<ActiveReader, BatchError> {
372 match self.regions.get(&id) {
373 None => return Err(BatchError::UnknownRegion(id)),
374 Some(CommittedRegion::Writer(_)) => return Err(BatchError::WrongDirection(id)),
375 Some(CommittedRegion::Reader(_)) => {}
376 }
377 match self.regions.remove(&id) {
378 Some(CommittedRegion::Reader(reader)) => Ok(reader),
379 Some(CommittedRegion::Writer(_)) => Err(BatchError::WrongDirection(id)),
380 None => Err(BatchError::UnknownRegion(id)),
381 }
382 }
383}
384
385#[cfg(test)]
386#[path = "batch_test.rs"]
387mod tests;