use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use url::Url;
use super::ru_model::RuChargingModel;
#[derive(Clone, Debug)]
pub struct VirtualAccountConfig {
regions: Vec<VirtualRegion>,
write_mode: WriteMode,
consistency: ConsistencyLevel,
replication: ReplicationConfig,
replication_overrides: HashMap<(String, String), ReplicationConfig>,
ru_model: RuChargingModel,
throttling_enabled: bool,
enable_per_partition_failover: Arc<AtomicBool>,
}
impl VirtualAccountConfig {
pub fn new(mut regions: Vec<VirtualRegion>) -> crate::error::Result<Self> {
if regions.is_empty() {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("at least one region is required")
.build());
}
for (idx, r) in regions.iter_mut().enumerate() {
if r.region_id == 0 {
r.region_id = idx as u64;
}
}
Ok(Self {
regions,
write_mode: WriteMode::Single,
consistency: ConsistencyLevel::Session,
replication: ReplicationConfig::default(),
replication_overrides: HashMap::new(),
ru_model: RuChargingModel::default(),
throttling_enabled: false,
enable_per_partition_failover: Arc::new(AtomicBool::new(false)),
})
}
pub fn with_write_mode(mut self, mode: WriteMode) -> Self {
self.write_mode = mode;
self
}
pub fn with_consistency(mut self, level: ConsistencyLevel) -> Self {
self.consistency = level;
self
}
pub fn with_replication_config(mut self, config: ReplicationConfig) -> Self {
self.replication = config;
self
}
pub fn with_replication_override(
mut self,
source: &str,
target: &str,
config: ReplicationConfig,
) -> crate::error::Result<Self> {
let known: Vec<&str> = self.regions.iter().map(|r| r.name.as_str()).collect();
if !known.contains(&source) {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message(format!(
"replication override source region '{}' is not configured (known: {:?})",
source, known
))
.build());
}
if !known.contains(&target) {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message(format!(
"replication override target region '{}' is not configured (known: {:?})",
target, known
))
.build());
}
if source == target {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("replication override source and target must be different regions")
.build());
}
self.replication_overrides
.insert((source.to_string(), target.to_string()), config);
Ok(self)
}
pub fn with_ru_model(mut self, model: RuChargingModel) -> Self {
self.ru_model = model;
self
}
pub fn with_throttling_enabled(mut self, enabled: bool) -> Self {
self.throttling_enabled = enabled;
self
}
pub fn throttling_enabled(&self) -> bool {
self.throttling_enabled
}
pub fn with_per_partition_failover(self, enabled: bool) -> Self {
self.enable_per_partition_failover
.store(enabled, Ordering::SeqCst);
self
}
pub fn per_partition_failover_enabled(&self) -> bool {
self.enable_per_partition_failover.load(Ordering::SeqCst)
}
pub fn set_per_partition_failover(&self, enabled: bool) {
self.enable_per_partition_failover
.store(enabled, Ordering::SeqCst);
}
pub fn regions(&self) -> &[VirtualRegion] {
&self.regions
}
pub fn write_mode(&self) -> WriteMode {
self.write_mode
}
pub fn consistency(&self) -> ConsistencyLevel {
self.consistency
}
pub fn replication(&self) -> &ReplicationConfig {
&self.replication
}
pub fn ru_model(&self) -> &RuChargingModel {
&self.ru_model
}
pub fn replication_for(&self, source: &str, target: &str) -> &ReplicationConfig {
self.replication_overrides
.get(&(source.to_string(), target.to_string()))
.unwrap_or(&self.replication)
}
pub fn write_region_name(&self) -> &str {
&self.regions[0].name
}
pub fn is_write_region(&self, region_name: &str) -> bool {
match self.write_mode {
WriteMode::Multi => true,
WriteMode::Single => self.regions[0].name == region_name,
}
}
pub fn region_for_url(&self, url: &Url) -> Option<&str> {
let host = url.host_str()?;
let scheme = url.scheme();
let port = url.port_or_known_default();
for r in &self.regions {
let Some(rhost) = r.gateway_url.host_str() else {
continue;
};
if !rhost.eq_ignore_ascii_case(host) {
continue;
}
if r.gateway_url.scheme() != scheme {
continue;
}
if r.gateway_url.port_or_known_default() != port {
continue;
}
return Some(&r.name);
}
None
}
pub fn region_id_for(&self, region_name: &str) -> u64 {
self.regions
.iter()
.find(|r| r.name == region_name)
.map(|r| r.region_id)
.unwrap_or(0)
}
}
#[derive(Clone, Debug)]
pub struct VirtualRegion {
name: String,
gateway_url: Url,
region_id: u64,
}
impl VirtualRegion {
pub fn new(name: &str, gateway_url: Url) -> Self {
Self {
name: name.to_string(),
gateway_url,
region_id: 0,
}
}
pub fn with_region_id(mut self, id: u64) -> Self {
self.region_id = id;
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn gateway_url(&self) -> &Url {
&self.gateway_url
}
pub fn region_id(&self) -> u64 {
self.region_id
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WriteMode {
Single,
Multi,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConsistencyLevel {
Strong,
BoundedStaleness,
Session,
ConsistentPrefix,
Eventual,
}
impl ConsistencyLevel {
pub fn as_str(&self) -> &str {
match self {
ConsistencyLevel::Strong => "Strong",
ConsistencyLevel::BoundedStaleness => "BoundedStaleness",
ConsistencyLevel::Session => "Session",
ConsistencyLevel::ConsistentPrefix => "ConsistentPrefix",
ConsistencyLevel::Eventual => "Eventual",
}
}
pub fn is_session(&self) -> bool {
matches!(self, ConsistencyLevel::Session)
}
}
pub const DEFAULT_MAX_BUFFERED_REPLICATIONS: usize = 10_000;
pub type ReplicationDelayFn = std::sync::Arc<dyn Fn() -> Duration + Send + Sync>;
#[derive(Clone)]
pub struct ReplicationConfig {
min_delay: Duration,
max_delay: Duration,
max_buffered_replications: usize,
delay_fn: Option<ReplicationDelayFn>,
jitter_seed: Option<std::sync::Arc<std::sync::Mutex<u64>>>,
}
impl std::fmt::Debug for ReplicationConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReplicationConfig")
.field("min_delay", &self.min_delay)
.field("max_delay", &self.max_delay)
.field("max_buffered_replications", &self.max_buffered_replications)
.field("delay_fn", &self.delay_fn.as_ref().map(|_| "<custom>"))
.field(
"jitter_seed",
&self.jitter_seed.as_ref().map(|_| "<seeded>"),
)
.finish()
}
}
impl ReplicationConfig {
pub fn immediate() -> Self {
Self {
min_delay: Duration::ZERO,
max_delay: Duration::ZERO,
max_buffered_replications: DEFAULT_MAX_BUFFERED_REPLICATIONS,
delay_fn: None,
jitter_seed: None,
}
}
pub fn fixed(delay: Duration) -> Self {
Self {
min_delay: delay,
max_delay: delay,
max_buffered_replications: DEFAULT_MAX_BUFFERED_REPLICATIONS,
delay_fn: None,
jitter_seed: None,
}
}
pub fn range(min: Duration, max: Duration) -> crate::error::Result<Self> {
if min > max {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("min delay must be <= max delay")
.build());
}
Ok(Self {
min_delay: min,
max_delay: max,
max_buffered_replications: DEFAULT_MAX_BUFFERED_REPLICATIONS,
delay_fn: None,
jitter_seed: None,
})
}
pub fn with_max_buffered_replications(mut self, max: usize) -> Self {
self.max_buffered_replications = max.max(1);
self
}
pub fn with_replication_delay_fn(mut self, f: ReplicationDelayFn) -> Self {
self.delay_fn = Some(f);
self
}
pub fn with_jitter_seed(mut self, seed: u64) -> Self {
let s = if seed == 0 { 0xDEAD_BEEF_u64 } else { seed };
self.jitter_seed = Some(std::sync::Arc::new(std::sync::Mutex::new(s)));
self
}
pub fn is_immediate(&self) -> bool {
self.delay_fn.is_none() && self.max_delay == Duration::ZERO
}
pub fn sample_delay(&self) -> Duration {
if let Some(f) = &self.delay_fn {
return f();
}
if self.min_delay == self.max_delay {
return self.min_delay;
}
let range = self.max_delay - self.min_delay;
let frac = if let Some(state) = &self.jitter_seed {
seeded_xorshift_fraction(state)
} else {
rand_fraction()
};
self.min_delay + range.mul_f64(frac)
}
pub fn min_delay(&self) -> Duration {
self.min_delay
}
pub fn max_delay(&self) -> Duration {
self.max_delay
}
pub fn max_buffered_replications(&self) -> usize {
self.max_buffered_replications
}
}
impl Default for ReplicationConfig {
fn default() -> Self {
Self {
min_delay: Duration::from_millis(20),
max_delay: Duration::from_millis(50),
max_buffered_replications: DEFAULT_MAX_BUFFERED_REPLICATIONS,
delay_fn: None,
jitter_seed: None,
}
}
}
fn seeded_xorshift_fraction(state: &std::sync::Arc<std::sync::Mutex<u64>>) -> f64 {
let mut guard = state.lock().unwrap();
let mut x = *guard;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*guard = x;
((x >> 11) as f64) / ((1u64 << 53) as f64)
}
fn rand_fraction() -> f64 {
use std::cell::Cell;
use std::time::SystemTime;
thread_local! {
static STATE: Cell<u64> = Cell::new(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
);
}
STATE.with(|s| {
let mut x = s.get();
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
s.set(x);
((x >> 11) as f64) / ((1u64 << 53) as f64)
})
}
#[derive(Clone, Debug)]
pub struct ContainerConfig {
partition_count: u32,
provisioned_throughput_ru: Option<u32>,
}
pub const MAX_PARTITION_COUNT: u32 = 100_000;
impl ContainerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_partition_count(mut self, count: u32) -> Self {
self.partition_count = count;
self
}
pub fn with_throughput(mut self, ru_per_second: u32) -> Self {
self.provisioned_throughput_ru = Some(ru_per_second);
self
}
pub fn build(self) -> crate::error::Result<Self> {
if self.partition_count == 0 {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("partition count must be > 0")
.build());
}
if self.partition_count > MAX_PARTITION_COUNT {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message(format!("partition count must be <= {MAX_PARTITION_COUNT}"))
.build());
}
if let Some(ru) = self.provisioned_throughput_ru {
if ru < 400 {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("provisioned throughput must be >= 400 RU/s")
.build());
}
}
Ok(self)
}
pub fn partition_count(&self) -> u32 {
self.partition_count
}
pub fn provisioned_throughput_ru(&self) -> Option<u32> {
self.provisioned_throughput_ru
}
}
impl Default for ContainerConfig {
fn default() -> Self {
Self {
partition_count: 4,
provisioned_throughput_ru: None,
}
}
}