use std::collections::HashSet;
use std::sync::Mutex;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct RegionId(pub u64);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct LocalLsn(pub u64);
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SessionToken {
pub pkrange_id: u32,
pub version: u64,
pub global_lsn: u64,
pub region_progress: Vec<(u64, u64)>,
}
impl SessionToken {
#[cfg(test)]
pub fn parse(s: &str) -> Option<Self> {
Self::parse_detailed(s).ok()
}
pub fn parse_detailed(s: &str) -> Result<Self, SessionTokenParseError> {
use SessionTokenParseError::*;
let (pkrange_str, rest) = s.split_once(':').ok_or(MissingPkRangeSeparator)?;
let pkrange_id = pkrange_str
.parse::<u32>()
.map_err(|_| InvalidPkRangeId(pkrange_str.to_string()))?;
let mut hash_parts = rest.split('#');
let version_str = hash_parts.next().ok_or(MissingVersion)?;
if version_str == "-1" {
let lsn_str = hash_parts.next().ok_or(MissingV1Lsn)?;
let lsn = lsn_str
.parse::<u64>()
.map_err(|_| InvalidV1Lsn(lsn_str.to_string()))?;
return Ok(SessionToken {
pkrange_id,
version: 0,
global_lsn: lsn,
region_progress: vec![],
});
}
let version = version_str
.parse::<u64>()
.map_err(|_| InvalidVersion(version_str.to_string()))?;
let global_lsn_str = hash_parts.next().ok_or(MissingGlobalLsn)?;
let global_lsn = global_lsn_str
.parse::<u64>()
.map_err(|_| InvalidGlobalLsn(global_lsn_str.to_string()))?;
let mut region_progress = Vec::new();
for segment in hash_parts {
if segment.is_empty() {
continue;
}
let (region_str, lsn_str) = segment
.split_once('=')
.ok_or_else(|| InvalidRegionSegment(segment.to_string()))?;
let region_id = region_str
.parse::<u64>()
.map_err(|_| InvalidRegionId(region_str.to_string()))?;
let lsn = lsn_str
.parse::<u64>()
.map_err(|_| InvalidRegionLsn(lsn_str.to_string()))?;
region_progress.push((region_id, lsn));
}
Ok(SessionToken {
pkrange_id,
version,
global_lsn,
region_progress,
})
}
pub fn format_v2(
pkrange_id: u32,
version: u64,
global_lsn: u64,
region: RegionId,
local_lsn: LocalLsn,
prior_progress: &[(u64, u64)],
) -> String {
let RegionId(region_id) = region;
let LocalLsn(local_lsn) = local_lsn;
if prior_progress.is_empty() {
return format!(
"{}:{}#{}#{}={}",
pkrange_id, version, global_lsn, region_id, local_lsn
);
}
let mut merged: Vec<(u64, u64)> = prior_progress.to_vec();
let mut found = false;
for (rid, lsn) in merged.iter_mut() {
if *rid == region_id {
*lsn = (*lsn).max(local_lsn);
found = true;
break;
}
}
if !found {
merged.push((region_id, local_lsn));
}
merged.sort_by_key(|(rid, _)| *rid);
let mut out = format!("{}:{}#{}", pkrange_id, version, global_lsn);
for (rid, lsn) in merged {
use std::fmt::Write;
let _ = write!(out, "#{}={}", rid, lsn);
}
out
}
pub fn format(pkrange_id: u32, lsn: u64) -> String {
format!("{}:-1#{}", pkrange_id, lsn)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SessionTokenParseError {
MissingPkRangeSeparator,
InvalidPkRangeId(String),
MissingVersion,
MissingV1Lsn,
InvalidV1Lsn(String),
InvalidVersion(String),
MissingGlobalLsn,
InvalidGlobalLsn(String),
InvalidRegionSegment(String),
InvalidRegionId(String),
InvalidRegionLsn(String),
EmptyComposite,
}
impl std::fmt::Display for SessionTokenParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use SessionTokenParseError::*;
match self {
MissingPkRangeSeparator => write!(f, "missing ':' separator after pkrange id"),
InvalidPkRangeId(s) => write!(f, "invalid pkrange id '{}'", s),
MissingVersion => write!(f, "missing version segment"),
MissingV1Lsn => write!(f, "missing V1 LSN after '-1'"),
InvalidV1Lsn(s) => write!(f, "invalid V1 LSN '{}'", s),
InvalidVersion(s) => write!(f, "invalid version '{}'", s),
MissingGlobalLsn => write!(f, "missing global LSN segment"),
InvalidGlobalLsn(s) => write!(f, "invalid global LSN '{}'", s),
InvalidRegionSegment(s) => {
write!(f, "region segment '{}' missing '=' separator", s)
}
InvalidRegionId(s) => write!(f, "invalid region id '{}'", s),
InvalidRegionLsn(s) => write!(f, "invalid region LSN '{}'", s),
EmptyComposite => write!(f, "empty composite session token entry"),
}
}
}
pub(crate) fn parse_composite_session_token(
s: &str,
) -> Result<Vec<SessionToken>, SessionTokenParseError> {
s.split(',')
.map(|part| {
let trimmed = part.trim();
if trimmed.is_empty() {
return Err(SessionTokenParseError::EmptyComposite);
}
SessionToken::parse_detailed(trimmed)
})
.collect()
}
pub(crate) struct SessionState {
forced_epks: Mutex<HashSet<String>>,
}
impl SessionState {
pub fn new() -> Self {
Self {
forced_epks: Mutex::new(HashSet::new()),
}
}
pub fn set_force_unavailable_for(&self, epk: &str) {
self.forced_epks.lock().unwrap().insert(epk.to_string());
}
pub fn check_and_clear_forced_for(&self, epk: &str) -> bool {
self.forced_epks.lock().unwrap().remove(epk)
}
pub fn snapshot_forced_epks(&self) -> Vec<String> {
self.forced_epks.lock().unwrap().iter().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_v1_token() {
let token = SessionToken::parse("0:-1#5").unwrap();
assert_eq!(token.pkrange_id, 0);
assert_eq!(token.version, 0);
assert_eq!(token.global_lsn, 5);
assert!(token.region_progress.is_empty());
}
#[test]
fn parse_v2_token() {
let token = SessionToken::parse("0:1#100#0=100").unwrap();
assert_eq!(token.pkrange_id, 0);
assert_eq!(token.version, 1);
assert_eq!(token.global_lsn, 100);
assert_eq!(token.region_progress, vec![(0, 100)]);
}
#[test]
fn parse_v2_multi_region() {
let token = SessionToken::parse("2:3#50#0=50#1=45").unwrap();
assert_eq!(token.pkrange_id, 2);
assert_eq!(token.version, 3);
assert_eq!(token.global_lsn, 50);
assert_eq!(token.region_progress, vec![(0, 50), (1, 45)]);
}
#[test]
fn format_v2_token() {
let s = SessionToken::format_v2(0, 1, 100, RegionId(0), LocalLsn(100), &[]);
assert_eq!(s, "0:1#100#0=100");
}
#[test]
fn format_v2_token_distinguishes_local_from_global() {
let s = SessionToken::format_v2(2, 4, 100, RegionId(1), LocalLsn(73), &[]);
assert_eq!(s, "2:4#100#1=73");
}
#[test]
fn format_v2_preserves_prior_multi_region_progress() {
let incoming = SessionToken::parse("0:5#200#0=200#1=150#2=120").unwrap();
let s = SessionToken::format_v2(
0,
5,
200,
RegionId(1),
LocalLsn(180),
&incoming.region_progress,
);
assert_eq!(s, "0:5#200#0=200#1=180#2=120");
}
#[test]
fn format_v2_local_lsn_takes_max_against_prior() {
let prior = vec![(0, 100), (1, 80)];
let s = SessionToken::format_v2(0, 1, 100, RegionId(1), LocalLsn(40), &prior);
assert_eq!(s, "0:1#100#0=100#1=80");
}
#[test]
fn format_v2_inserts_new_region_when_absent_from_prior() {
let prior = vec![(0, 100)];
let s = SessionToken::format_v2(0, 1, 100, RegionId(2), LocalLsn(7), &prior);
assert_eq!(s, "0:1#100#0=100#2=7");
}
#[test]
fn format_v1_token() {
assert_eq!(SessionToken::format(2, 10), "2:-1#10");
}
#[test]
fn parse_composite_v2() {
let tokens = parse_composite_session_token("0:1#5#0=5,1:1#3#0=3").unwrap();
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].global_lsn, 5);
assert_eq!(tokens[1].global_lsn, 3);
}
#[test]
fn parse_composite_mixed() {
let tokens = parse_composite_session_token("0:-1#5,1:1#3#0=3").unwrap();
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].version, 0);
assert_eq!(tokens[1].version, 1);
}
#[test]
fn parse_composite_rejects_malformed_session_token() {
assert!(parse_composite_session_token("0:1#5#0=5,broken-token").is_err());
assert!(parse_composite_session_token("0:1#5#0=5,").is_err());
}
#[test]
fn forced_unavailability_one_shot() {
let state = SessionState::new();
let epk = "ABCD";
assert!(!state.check_and_clear_forced_for(epk));
state.set_force_unavailable_for(epk);
assert!(state.check_and_clear_forced_for(epk));
assert!(!state.check_and_clear_forced_for(epk));
}
#[test]
fn forced_unavailability_scoped_per_epk() {
let state = SessionState::new();
state.set_force_unavailable_for("AAAA");
assert!(!state.check_and_clear_forced_for("BBBB"));
assert!(state.check_and_clear_forced_for("AAAA"));
}
}