betex 0.35.0

Betfair / Prediction Market Exchange
Documentation
/// Default capacity for the engine command channel.
pub const DEFAULT_ENGINE_CHANNEL_CAPACITY: usize = 8192;

/// Default batch size for close/lapse/void order batches started by market-state changes.
pub const DEFAULT_CLOSE_BATCH_MAX_EVENTS: u16 = 25;

/// Default poll timeout for the engine loop in milliseconds.
pub const DEFAULT_ENGINE_POLL_TIMEOUT_MS: u64 = 100;

/// Default number of external commands to process before forcing an internal batch burst.
pub const DEFAULT_ENGINE_MAX_EXTERNAL_BEFORE_INTERNAL_BURST: usize = 32;

/// Default number of internal continuation commands to run in a single burst.
pub const DEFAULT_ENGINE_INTERNAL_BURST_BASE_COMMANDS: usize = 4;

/// Default maximum elapsed time for a single internal burst in milliseconds.
pub const DEFAULT_ENGINE_INTERNAL_BURST_BASE_ELAPSED_MS: u64 = 1;

#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ThreadPinning {
    /// Pin the engine command-processing + publishing thread to this core id.
    #[serde(default)]
    pub engine_core: Option<usize>,
    /// Pin the WAL poller thread to this core id.
    #[serde(default)]
    pub wal_poller_core: Option<usize>,
    /// Pin disruptor consumer threads in registration order.
    ///
    /// This applies to handlers registered via `EngineBuilder::{register_handler,...}` only.
    #[serde(default)]
    pub handler_cores: Vec<usize>,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct JournalConfig {
    /// Per-segment LMDB map size in bytes. Default: 256MB prod, 8MB test.
    #[serde(default = "JournalConfig::default_segment_map_size_bytes")]
    pub segment_map_size_bytes: usize,
    /// Auto-rotate after N events (soft limit, checked at batch boundaries). Default: 5000.
    #[serde(default = "JournalConfig::default_max_segment_events")]
    pub max_segment_events: u64,
    /// Run contiguity validation on startup. Default: true.
    #[serde(default = "JournalConfig::default_validate_on_startup")]
    pub validate_on_startup: bool,
    /// On startup, retire sealed segments where `end_seq <= confirmed_seq`.
    #[serde(default)]
    pub prune_before_seq_on_startup: Option<u64>,
    /// Remove queued `trash/` segments on startup. Default: true.
    #[serde(default = "JournalConfig::default_clear_trash_on_startup")]
    pub clear_trash_on_startup: bool,
}

impl JournalConfig {
    fn default_segment_map_size_bytes() -> usize {
        if cfg!(test) {
            8 * 1024 * 1024 // 8MB for tests
        } else {
            256 * 1024 * 1024 // 256MB for prod
        }
    }

    fn default_validate_on_startup() -> bool {
        true
    }

    fn default_max_segment_events() -> u64 {
        // Sizing basis (measured rkyv payload): `OrderCancelledBatched` with 4096 cancelled orders
        // encodes to ~32_904 bytes. At 5000 events/segment that's ~157 MiB raw payload,
        // leaving practical headroom in the default 256 MiB LMDB map for index/page overhead.
        5_000
    }

    fn default_clear_trash_on_startup() -> bool {
        true
    }
}

impl Default for JournalConfig {
    fn default() -> Self {
        Self {
            segment_map_size_bytes: Self::default_segment_map_size_bytes(),
            max_segment_events: Self::default_max_segment_events(),
            validate_on_startup: Self::default_validate_on_startup(),
            prune_before_seq_on_startup: None,
            clear_trash_on_startup: Self::default_clear_trash_on_startup(),
        }
    }
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Config {
    pub ring_size_pow2: usize,
    pub response_timeout_ms: u64,
    #[serde(default = "Config::default_engine_channel_capacity")]
    pub engine_channel_capacity: usize,
    /// Initial capacity for per-market order storage.
    ///
    /// This is used to pre-size the book's internal order arena to reduce reallocations
    /// during warm-up and in-play bursts.
    pub order_store_capacity: usize,
    /// If set, enforce that all BinaryYes markets use this denominator (ticks per $1.00 payout).
    ///
    /// Example: `Some(100)` enforces $0.01 per tick.
    #[serde(default)]
    pub enforce_binary_yes_max_price_ticks: Option<u16>,
    #[serde(default = "Config::default_close_batch_max_events")]
    pub close_batch_max_events: u16,
    #[serde(default = "Config::default_engine_poll_timeout_ms")]
    pub engine_poll_timeout_ms: u64,
    #[serde(default = "Config::default_engine_max_external_before_internal_burst")]
    pub engine_max_external_before_internal_burst: usize,
    #[serde(default = "Config::default_engine_internal_burst_base_commands")]
    pub engine_internal_burst_base_commands: usize,
    #[serde(default = "Config::default_engine_internal_burst_base_elapsed_ms")]
    pub engine_internal_burst_base_elapsed_ms: u64,
    #[serde(default)]
    pub pinning: Option<ThreadPinning>,
    #[serde(default)]
    pub journal: JournalConfig,
}

impl Config {
    fn default_engine_channel_capacity() -> usize {
        DEFAULT_ENGINE_CHANNEL_CAPACITY
    }

    fn default_close_batch_max_events() -> u16 {
        DEFAULT_CLOSE_BATCH_MAX_EVENTS
    }

    fn default_engine_poll_timeout_ms() -> u64 {
        DEFAULT_ENGINE_POLL_TIMEOUT_MS
    }

    fn default_engine_max_external_before_internal_burst() -> usize {
        DEFAULT_ENGINE_MAX_EXTERNAL_BEFORE_INTERNAL_BURST
    }

    fn default_engine_internal_burst_base_commands() -> usize {
        DEFAULT_ENGINE_INTERNAL_BURST_BASE_COMMANDS
    }

    fn default_engine_internal_burst_base_elapsed_ms() -> u64 {
        DEFAULT_ENGINE_INTERNAL_BURST_BASE_ELAPSED_MS
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            ring_size_pow2: 1024,
            response_timeout_ms: 1000,
            engine_channel_capacity: Self::default_engine_channel_capacity(),
            order_store_capacity: 20_000,
            enforce_binary_yes_max_price_ticks: None,
            close_batch_max_events: Self::default_close_batch_max_events(),
            engine_poll_timeout_ms: Self::default_engine_poll_timeout_ms(),
            engine_max_external_before_internal_burst:
                Self::default_engine_max_external_before_internal_burst(),
            engine_internal_burst_base_commands: Self::default_engine_internal_burst_base_commands(
            ),
            engine_internal_burst_base_elapsed_ms:
                Self::default_engine_internal_burst_base_elapsed_ms(),
            pinning: None,
            journal: JournalConfig::default(),
        }
    }
}