1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
use std::{
path::{Path, PathBuf},
time::Duration,
};
use crc32fast::Hasher;
use snafu::Snafu;
use super::{
io::{Filesystem, ProductionFilesystem},
ledger::LEDGER_LEN,
record::RECORD_HEADER_LEN,
};
// We don't want data files to be bigger than 128MB, but we might end up overshooting slightly.
pub const DEFAULT_MAX_DATA_FILE_SIZE: usize = 128 * 1024 * 1024;
// We allow records to be as large(*) as a data file.
pub const DEFAULT_MAX_RECORD_SIZE: usize = DEFAULT_MAX_DATA_FILE_SIZE;
// The maximum record size has to be bigger than the record header itself, since we count the record header towards
// sizing/space usage, etc... but we also use the overaligned version here to make sure we're similarly accounting for
// what `rkyv` will do when we serialize a record.
pub const MINIMUM_MAX_RECORD_SIZE: usize = align16(RECORD_HEADER_LEN + 1);
// We want to ensure a reasonable time before we `fsync`/flush to disk, and 500ms should provide that for non-critical
// workloads.
//
// Practically, it's far more definitive than `disk_v1` which does not definitely `fsync` at all, at least with how we
// have it configured.
pub const DEFAULT_FLUSH_INTERVAL: Duration = Duration::from_millis(500);
// Using 256KB as it aligns nicely with the I/O size exposed by major cloud providers. This may not
// be the underlying block size used by the OS, but it still aligns well with what will happen on
// the "backend" for cloud providers, which is simply a useful default for when we want to look at
// buffer throughput and estimate how many IOPS will be consumed, etc.
pub const DEFAULT_WRITE_BUFFER_SIZE: usize = 256 * 1024;
// We specifically limit ourselves to 0-31 for file IDs in test, because it lets us more quickly
// create/consume the file IDs so we can test edge cases like file ID rollover and "writer is
// waiting to open file that reader is still on".
#[cfg(not(test))]
pub const MAX_FILE_ID: u16 = u16::MAX;
#[cfg(test)]
pub const MAX_FILE_ID: u16 = 6;
// The alignment used by the record serializer.
const SERIALIZER_ALIGNMENT: usize = 16;
const MAX_ALIGNABLE_AMOUNT: usize = usize::MAX - SERIALIZER_ALIGNMENT;
pub(crate) fn create_crc32c_hasher() -> Hasher {
crc32fast::Hasher::new()
}
/// Aligns the given amount to 16.
///
/// This is required due to the overalignment used in record serialization, such that we can correctly determine minimum
/// on-disk sizes for various elements, and account for those in size limits, etc.
pub(crate) const fn align16(amount: usize) -> usize {
// The amount must be less than `MAX_ALIGNABLE_AMOUNT` otherwise we'll overflow trying to align it, ending up with a
// nonsensical value.
assert!(
amount <= MAX_ALIGNABLE_AMOUNT,
"`amount` must be less than `MAX_ALIGNABLE_AMOUNT`"
);
((amount + SERIALIZER_ALIGNMENT - 1) / SERIALIZER_ALIGNMENT) * SERIALIZER_ALIGNMENT
}
/// Gets the maximum possible data file size given the type-level numerical limits and buffer invariants.
fn get_maximum_data_file_size() -> u64 {
let ledger_len: u64 = LEDGER_LEN
.try_into()
.expect("Ledger length cannot be greater than `u64`.");
(u64::MAX - ledger_len) / 2
}
/// Gets the minimum buffer size for the given maximum data file size.
///
/// This ensures that we are allowed to store enough bytes on-disk, as the buffer design requires being able to always
/// write to a minimum number of data files, etc. This allow ensures that we're accounting for non-data file disk usage
/// so that we do not overrun the specified maximum buffer size when considering the sum total of files placed on disk.
fn get_minimum_buffer_size(max_data_file_size: u64) -> Option<u64> {
// We're doing this fallible conversion back-and-forth because we have to interoperate with `u64` and `usize`, and
// we need to ensure we're not getting values that can't be represented correctly in both types, as well as ensuring
// we're not implicitly overflowing and generating nonsensical numbers.
let ledger_len = LEDGER_LEN
.try_into()
.expect("Ledger length cannot be greater than `u64`.");
// We always need to be able to allocate two data files, so the buffer size has to be at least as big as 2x data
// files at their maximum allowed size, plus an allowance for the size of the ledger state itself.
max_data_file_size
.checked_mul(2)
.and_then(|doubled| doubled.checked_add(ledger_len))
}
#[derive(Debug, Snafu)]
pub enum BuildError {
#[snafu(display("parameter '{}' was invalid: {}", param_name, reason))]
InvalidParameter {
param_name: &'static str,
reason: String,
},
}
/// Buffer configuration.
#[derive(Clone, Debug)]
pub struct DiskBufferConfig<FS> {
/// Directory where this buffer will write its files.
///
/// Must be unique from all other buffers, whether within the same process or other Vector
/// processes on the machine.
pub(crate) data_dir: PathBuf,
/// Maximum size, in bytes, that the buffer can consume.
///
/// The actual maximum on-disk buffer size is this amount rounded up to the next multiple of
/// `max_data_file_size`, but internally, the next multiple of `max_data_file_size` when
/// rounding this amount _down_ is what gets used as the maximum buffer size.
///
/// This ensures that we never use more then the documented "rounded to the next multiple"
/// amount, as we must account for one full data file's worth of extra data.
pub(crate) max_buffer_size: u64,
/// Maximum size, in bytes, to target for each individual data file.
///
/// This value is not strictly obey because we cannot know ahead of encoding/serializing if the
/// free space a data file has is enough to hold the write. In other words, we never attempt to
/// write to a data file if it is as larger or larger than this value, but may write a record
/// that causes a data file to exceed this value by as much as `max_record_size`.
pub(crate) max_data_file_size: u64,
/// Maximum size, in bytes, of an encoded record.
///
/// Any record which, when encoded and serialized, is larger than this amount will not be written
/// to the buffer.
pub(crate) max_record_size: usize,
/// Size, in bytes, of the writer's internal buffer.
///
/// This buffer is used to coalesce writes to the underlying data file where possible, which in
/// turn reduces the number of syscalls needed to issue writes to the underlying data file.
pub(crate) write_buffer_size: usize,
/// Flush interval for ledger and data files.
///
/// While data is asynchronously flushed by the OS, and the reader/writer can proceed with a
/// "hard" flush (aka `fsync`/`fsyncdata`), the flush interval effectively controls the
/// acceptable window of time for data loss.
///
/// In the event that data had not yet been durably written to disk, and Vector crashed, the
/// amount of data written since the last flush would be lost.
pub(crate) flush_interval: Duration,
/// Filesystem implementation for opening data files.
///
/// We allow parameterizing the filesystem implementation for ease of testing. The "filesystem"
/// implementation essentially defines how we open and delete data files, as well as the type of
/// the data file objects we get when opening a data file.
pub(crate) filesystem: FS,
}
/// Builder for [`DiskBufferConfig`].
#[derive(Clone, Debug)]
pub struct DiskBufferConfigBuilder<FS = ProductionFilesystem>
where
FS: Filesystem,
{
pub(crate) data_dir: PathBuf,
pub(crate) max_buffer_size: Option<u64>,
pub(crate) max_data_file_size: Option<u64>,
pub(crate) max_record_size: Option<usize>,
pub(crate) write_buffer_size: Option<usize>,
pub(crate) flush_interval: Option<Duration>,
pub(crate) filesystem: FS,
}
impl DiskBufferConfigBuilder {
pub fn from_path<P>(data_dir: P) -> DiskBufferConfigBuilder
where
P: AsRef<Path>,
{
DiskBufferConfigBuilder {
data_dir: data_dir.as_ref().to_path_buf(),
max_buffer_size: None,
max_data_file_size: None,
max_record_size: None,
write_buffer_size: None,
flush_interval: None,
filesystem: ProductionFilesystem,
}
}
}
impl<FS> DiskBufferConfigBuilder<FS>
where
FS: Filesystem,
{
/// Sets the maximum size, in bytes, that the buffer can consume.
///
/// The actual maximum on-disk buffer size is this amount rounded up to the next multiple of
/// `max_data_file_size`, but internally, the next multiple of `max_data_file_size` when
/// rounding this amount _down_ is what gets used as the maximum buffer size.
///
/// This ensures that we never use more then the documented "rounded to the next multiple"
/// amount, as we must account for one full data file's worth of extra data.
///
/// Defaults to `usize::MAX`, or effectively no limit. Due to the internal design of the
/// buffer, the effective maximum limit is around `max_data_file_size` * 2^16.
#[allow(dead_code)]
pub fn max_buffer_size(mut self, amount: u64) -> Self {
self.max_buffer_size = Some(amount);
self
}
/// Sets the maximum size, in bytes, to target for each individual data file.
///
/// This value is not strictly obey because we cannot know ahead of encoding/serializing if the
/// free space a data file has is enough to hold the write. In other words, we never attempt to
/// write to a data file if it is as larger or larger than this value, but may write a record
/// that causes a data file to exceed this value by as much as `max_record_size`.
///
/// Defaults to 128MB.
#[allow(dead_code)]
pub fn max_data_file_size(mut self, amount: u64) -> Self {
self.max_data_file_size = Some(amount);
self
}
/// Sets the maximum size, in bytes, of an encoded record.
///
/// Any record which, when encoded and serialized, is larger than this amount will not be written
/// to the buffer.
///
/// Defaults to 128MB.
#[allow(dead_code)]
pub fn max_record_size(mut self, amount: usize) -> Self {
self.max_record_size = Some(amount);
self
}
/// Size, in bytes, of the writer's internal buffer.
///
/// This buffer is used to coalesce writes to the underlying data file where possible, which in
/// turn reduces the number of syscalls needed to issue writes to the underlying data file.
///
/// Defaults to 256KB.
#[allow(dead_code)]
pub fn write_buffer_size(mut self, amount: usize) -> Self {
self.write_buffer_size = Some(amount);
self
}
/// Sets the flush interval for ledger and data files.
///
/// While data is asynchronously flushed by the OS, and the reader/writer can proceed with a
/// "hard" flush (aka `fsync`/`fsyncdata`), the flush interval effectively controls the
/// acceptable window of time for data loss.
///
/// In the event that data had not yet been durably written to disk, and Vector crashed, the
/// amount of data written since the last flush would be lost.
///
/// Defaults to 500ms.
#[allow(dead_code)]
pub fn flush_interval(mut self, interval: Duration) -> Self {
self.flush_interval = Some(interval);
self
}
/// Filesystem implementation for opening data files.
///
/// We allow parameterizing the filesystem implementation for ease of testing. The "filesystem"
/// implementation essentially defines how we open and delete data files, as well as the type of
/// the data file objects we get when opening a data file.
///
/// Defaults to a Tokio-backed implementation.
#[allow(dead_code)]
pub fn filesystem<FS2>(self, filesystem: FS2) -> DiskBufferConfigBuilder<FS2>
where
FS2: Filesystem,
{
DiskBufferConfigBuilder {
data_dir: self.data_dir,
max_buffer_size: self.max_buffer_size,
max_data_file_size: self.max_data_file_size,
max_record_size: self.max_record_size,
write_buffer_size: self.write_buffer_size,
flush_interval: self.flush_interval,
filesystem,
}
}
/// Consumes this builder and constructs a `DiskBufferConfig`.
pub fn build(self) -> Result<DiskBufferConfig<FS>, BuildError> {
let max_buffer_size = self.max_buffer_size.unwrap_or(u64::MAX);
let max_data_file_size = self.max_data_file_size.unwrap_or_else(|| {
u64::try_from(DEFAULT_MAX_DATA_FILE_SIZE)
.expect("Default maximum data file size should never be greater than 2^64 bytes.")
});
let max_record_size = self.max_record_size.unwrap_or(DEFAULT_MAX_RECORD_SIZE);
let write_buffer_size = self.write_buffer_size.unwrap_or(DEFAULT_WRITE_BUFFER_SIZE);
let flush_interval = self.flush_interval.unwrap_or(DEFAULT_FLUSH_INTERVAL);
let filesystem = self.filesystem;
// Validate the input parameters.
if max_data_file_size == 0 {
return Err(BuildError::InvalidParameter {
param_name: "max_data_file_size",
reason: "cannot be zero".to_string(),
});
}
let data_file_size_mechanical_limit = get_maximum_data_file_size();
if max_data_file_size > data_file_size_mechanical_limit {
return Err(BuildError::InvalidParameter {
param_name: "max_data_file_size",
reason: format!("cannot be greater than {data_file_size_mechanical_limit} bytes"),
});
}
let Some(minimum_buffer_size) = get_minimum_buffer_size(max_data_file_size) else {
unreachable!("maximum data file size should be correctly limited at this point")
};
if max_buffer_size < minimum_buffer_size {
return Err(BuildError::InvalidParameter {
param_name: "max_buffer_size",
reason: format!("must be greater than or equal to {minimum_buffer_size} bytes"),
});
}
if max_record_size == 0 {
return Err(BuildError::InvalidParameter {
param_name: "max_record_size",
reason: "cannot be zero".to_string(),
});
}
if max_record_size <= MINIMUM_MAX_RECORD_SIZE {
return Err(BuildError::InvalidParameter {
param_name: "max_record_size",
reason: format!("must be greater than or equal to {MINIMUM_MAX_RECORD_SIZE} bytes",),
});
}
let Ok(max_record_size_converted) = u64::try_from(max_record_size) else {
return Err(BuildError::InvalidParameter {
param_name: "max_record_size",
reason: "must be less than 2^64 bytes".to_string(),
});
};
if max_record_size_converted > max_data_file_size {
return Err(BuildError::InvalidParameter {
param_name: "max_record_size",
reason: "must be less than or equal to `max_data_file_size`".to_string(),
});
}
if write_buffer_size == 0 {
return Err(BuildError::InvalidParameter {
param_name: "write_buffer_size",
reason: "cannot be zero".to_string(),
});
}
// Users configure the `max_size` of their disk buffers, which translates to the `max_buffer_size` field here,
// and represents the maximum desired size of a disk buffer in terms of on-disk usage. In order to meet this
// request, we do a few things internally and also enforce a lower bound on `max_buffer_size` to ensure we can
// commit to respecting the communicated maximum buffer size.
//
// Internally, we track the current buffer size as a function of the sum of the size of all unacknowledged
// records. This means, simply, that if 100 records are written that consume 1KB a piece, our current buffer
// size should be around 100KB, and as those records are read and acknowledged, the current buffer size would
// drop by 1KB for each of them until eventually it went back down to zero.
//
// One of the design invariants around data files is that they are written to until they reach the maximum data
// file size, such that they are guaranteed to never be greater in size than `max_data_file_size`. This is
// coupled with the fact that a data file cannot be deleted from disk until all records written to it have been
// read _and_ acknowledged.
//
// Together, this means that we need to set a lower bound of 2*`max_data_file_size` for `max_buffer_size`.
//
// First, given the "data file keeps getting written to until we reach its max size" invariant, we know that in
// order to commit to the on-disk buffer size not exceeding `max_buffer_size`, the value must be at least as
// much as a single full data file, aka `max_data_file_size`.
//
// Secondly, we also want to ensure that the writer can make progress as the reader makes progress. If the
// maximum buffer size was equal to the maximum data file size, the writer would be stalled as soon as the data
// file reached the maximum size, until the reader was able to fully read and acknowledge all records, and thus
// delete the data file from disk. If we instead require that the maximum buffer size exceeds
// `max_data_file_size`, this allows us to open the next data file and start writing to it up until the maximum
// buffer size.
//
// Since we could essentially read and acknowledge all but the last remaining record in a data file, this would
// imply we gave the writer the ability to write that much more data, which means we would need at least double
// the maximum data file size in order to support the writer being able to make progress in the aforementioned
// situation.
//
// Finally, we come to this calculation. Since the logic dictates that we essentially require at least one extra
// data file past the minimum of one, we need to use an _internal_ maximum buffer size of `max_buffer_size` -
// `max_data_file_size`, so that as the reader makes progress, the writer never is led to believe it can create
// another data file such that the number of active data files, multiplied by `max_data_file_size`, would exceed
// `max_buffer_size`.
let max_buffer_size = max_buffer_size - max_data_file_size;
Ok(DiskBufferConfig {
data_dir: self.data_dir,
max_buffer_size,
max_data_file_size,
max_record_size,
write_buffer_size,
flush_interval,
filesystem,
})
}
}
#[cfg(test)]
mod tests {
use proptest::{prop_assert, proptest, test_runner::Config};
use crate::variants::disk_v2::common::MAX_ALIGNABLE_AMOUNT;
use super::{
align16, BuildError, DiskBufferConfigBuilder, MINIMUM_MAX_RECORD_SIZE, SERIALIZER_ALIGNMENT,
};
#[test]
#[should_panic(expected = "`amount` must be less than `MAX_ALIGNABLE_AMOUNT`")]
fn test_align16_too_large() {
// We forcefully panic if the input to `align16` is too large to align without overflow, primarily because
// that's a huge amount even on 32-bit systems and in non-test code, we only use `align16` in a const context,
// so it will panic during compilation, not at runtime.
align16(MAX_ALIGNABLE_AMOUNT + 1);
}
proptest! {
#![proptest_config(Config::with_cases(1000))]
#[test]
fn test_align16(input in 0..MAX_ALIGNABLE_AMOUNT) {
// You may think to yourself: "this test seems excessive and not necessary", but, au contraire! Our
// algorithm depends on integer division rounding towards zero, which is an invariant provided to Rust by
// way of LLVM itself. In order to avoid weird surprises down the line if that invariant changes, including
// a future where we, or others, potentially compile Vector with an alternative compiler that does not
// round towards zero... we're being extra careful and hedging our bet by having such a property test.
// Make sure we're actually aligned.
let aligned = align16(input);
prop_assert!(aligned % SERIALIZER_ALIGNMENT == 0);
// Make sure we're not overaligned, too.
let delta = if aligned >= input {
aligned - input
} else {
panic!("`aligned` must never be less than `input` in this test; inputs are crafted to obey `MAX_ALIGNABLE_AMOUNT`");
};
prop_assert!(delta <= SERIALIZER_ALIGNMENT, "`align16` returned overaligned input: input={} aligned={} delta={}", input, aligned, delta);
}
}
#[test]
fn basic_rejections() {
// Maximum data file size cannot be zero.
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_data_file_size(0)
.build();
match result {
Err(BuildError::InvalidParameter { param_name, .. }) => assert_eq!(
param_name, "max_data_file_size",
"invalid parameter should have been `max_data_file_size`"
),
_ => panic!("expected invalid parameter error"),
}
// Maximum data file size cannot be greater than u64::MAX / 2, since we multiply it by 2 when calculating the
// lower bound for the maximum buffer size.
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_data_file_size((u64::MAX / 2) + 1)
.build();
match result {
Err(BuildError::InvalidParameter { param_name, .. }) => assert_eq!(
param_name, "max_data_file_size",
"invalid parameter should have been `max_data_file_size`"
),
_ => panic!("expected invalid parameter error"),
}
// Maximum buffer size cannot be zero.
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_buffer_size(0)
.build();
match result {
Err(BuildError::InvalidParameter { param_name, .. }) => assert_eq!(
param_name, "max_buffer_size",
"invalid parameter should have been `max_buffer_size`"
),
_ => panic!("expected invalid parameter error"),
}
// Maximum buffer size cannot be less than 2x the maximum data file size.
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_data_file_size(10000)
.max_record_size(100)
.max_buffer_size(19999)
.build();
match result {
Err(BuildError::InvalidParameter { param_name, .. }) => assert_eq!(
param_name, "max_buffer_size",
"invalid parameter should have been `max_buffer_size`"
),
_ => panic!("expected invalid parameter error"),
}
// Maximum record size cannot be zero.
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_record_size(0)
.build();
match result {
Err(BuildError::InvalidParameter { param_name, .. }) => assert_eq!(
param_name, "max_record_size",
"invalid parameter should have been `max_record_size`"
),
_ => panic!("expected invalid parameter error"),
}
// Maximum record size cannot be less than the minimum record header length.
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_record_size(MINIMUM_MAX_RECORD_SIZE - 1)
.build();
match result {
Err(BuildError::InvalidParameter { param_name, .. }) => assert_eq!(
param_name, "max_record_size",
"invalid parameter should have been `max_record_size`"
),
_ => panic!("expected invalid parameter error"),
}
// Maximum record size cannot be greater than maximum data file size.
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_data_file_size(123_456)
.max_record_size(123_457)
.build();
match result {
Err(BuildError::InvalidParameter { param_name, .. }) => assert_eq!(
param_name, "max_record_size",
"invalid parameter should have been `max_record_size`"
),
_ => panic!("expected invalid parameter error"),
}
}
proptest! {
#![proptest_config(Config::with_cases(10000))]
#[test]
fn ensure_max_buffer_size_lower_bound(max_buffer_size in 1..u64::MAX, max_record_data_file_size in 1..u64::MAX) {
let max_data_file_size = max_record_data_file_size;
let max_record_size = usize::try_from(max_record_data_file_size)
.expect("Maximum record size, and data file size, must be less than 2^64 bytes.");
let result = DiskBufferConfigBuilder::from_path("/tmp/dummy/path")
.max_buffer_size(max_buffer_size)
.max_data_file_size(max_data_file_size)
.max_record_size(max_record_size)
.build();
// We don't necessarily care about the error cases here, but what we do care about is making sure that, when
// the generated configuration is theoretically valid, the calculated maximum buffer size actually meets our expectation of
// being at least `max_data_file_size` and `max_data_file_size` less than the input maximum buffer size.
if let Ok(config) = result {
prop_assert!(config.max_buffer_size >= max_data_file_size, "calculated max buffer size must always be greater than or equal to `max_data_file_size`");
prop_assert!(config.max_buffer_size + max_data_file_size == max_buffer_size, "calculated max buffer size must always be less `max_data_file_size` than input max buffer size");
}
}
}
}