use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::cluster::{ClusterEpoch, PartitionId};
use crate::grid::active_active::HybridLogicalClock;
use crate::grid::elasticity::RegionId;
use crate::grid::hardening::ValueVersion;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SessionId(String);
impl SessionId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for SessionId {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for SessionId {
fn from(value: String) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct PartitionKey {
pub partition: PartitionId,
pub region: RegionId,
}
impl PartitionKey {
pub fn new(partition: PartitionId, region: impl Into<RegionId>) -> Self {
Self {
partition,
region: region.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct VersionStamp {
pub version: ValueVersion,
pub epoch: ClusterEpoch,
pub hlc: HybridLogicalClock,
}
impl VersionStamp {
pub const fn new(version: ValueVersion, epoch: ClusterEpoch, hlc: HybridLogicalClock) -> Self {
Self {
version,
epoch,
hlc,
}
}
pub fn covers(self, required: Self) -> bool {
self >= required
}
pub fn version_distance_from(self, candidate: Self) -> u64 {
self.version.saturating_sub(candidate.version)
}
}
impl Default for VersionStamp {
fn default() -> Self {
Self {
version: 0,
epoch: ClusterEpoch::default(),
hlc: HybridLogicalClock::new(0, 0),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionWatermark {
cap: usize,
seen: BTreeMap<PartitionKey, VersionStamp>,
coarsened_total: u64,
}
impl SessionWatermark {
pub fn new(cap: usize) -> Self {
Self {
cap: cap.max(1),
seen: BTreeMap::new(),
coarsened_total: 0,
}
}
pub fn cap(&self) -> usize {
self.cap
}
pub fn len(&self) -> usize {
self.seen.len()
}
pub fn is_empty(&self) -> bool {
self.seen.is_empty()
}
pub fn coarsened_total(&self) -> u64 {
self.coarsened_total
}
pub fn observe(&mut self, key: PartitionKey, stamp: VersionStamp) {
self.record(key, stamp);
}
pub fn record_write(&mut self, key: PartitionKey, stamp: VersionStamp) {
self.record(key, stamp);
}
pub fn covers(&self, key: &PartitionKey, required: VersionStamp) -> bool {
self.highest_seen(key)
.map(|seen| seen.covers(required))
.unwrap_or(false)
}
pub fn highest_seen(&self, key: &PartitionKey) -> Option<VersionStamp> {
self.seen.get(key).copied()
}
pub fn entries(&self) -> &BTreeMap<PartitionKey, VersionStamp> {
&self.seen
}
fn record(&mut self, key: PartitionKey, stamp: VersionStamp) {
if let Some(existing) = self.seen.get_mut(&key) {
if stamp > *existing {
*existing = stamp;
}
return;
}
if self.seen.len() >= self.cap {
self.coarsen_one();
}
self.seen.insert(key, stamp);
}
fn coarsen_one(&mut self) {
if let Some(key) = self
.seen
.iter()
.min_by_key(|(_, stamp)| **stamp)
.map(|(key, _)| key.clone())
{
self.seen.remove(&key);
self.coarsened_total = self.coarsened_total.saturating_add(1);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SessionRequest {
Sessionless,
Session(SessionToken),
}
impl SessionRequest {
pub fn is_sessionless(&self) -> bool {
matches!(self, Self::Sessionless)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionToken {
pub session_id: SessionId,
pub watermark: SessionWatermark,
pub nonce: u64,
pub issued_at_millis: u64,
mac: u64,
}
impl SessionToken {
pub fn issue(
session_id: impl Into<SessionId>,
watermark: SessionWatermark,
nonce: u64,
issued_at_millis: u64,
secret: &[u8],
) -> Self {
let session_id = session_id.into();
let mac = token_mac(&session_id, &watermark, nonce, issued_at_millis, secret);
Self {
session_id,
watermark,
nonce,
issued_at_millis,
mac,
}
}
pub fn mac(&self) -> u64 {
self.mac
}
pub fn verify(
&self,
expected_session: &SessionId,
secret: &[u8],
min_nonce: u64,
) -> Result<(), SessionTokenError> {
if &self.session_id != expected_session {
return Err(SessionTokenError::WrongSession);
}
if self.nonce < min_nonce {
return Err(SessionTokenError::Replayed);
}
let expected = token_mac(
&self.session_id,
&self.watermark,
self.nonce,
self.issued_at_millis,
secret,
);
if expected != self.mac {
return Err(SessionTokenError::Forged);
}
Ok(())
}
pub fn forged(mut self) -> Self {
self.mac ^= 0xa5a5_a5a5_a5a5_a5a5;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionTokenError {
Forged,
WrongSession,
Replayed,
Expired,
}
impl fmt::Display for SessionTokenError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Forged => "session token MAC verification failed",
Self::WrongSession => "session token belongs to a different session",
Self::Replayed => "session token replay rejected",
Self::Expired => "session token expired",
})
}
}
impl std::error::Error for SessionTokenError {}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionContextMetrics {
pub session_watermark_entries: u64,
pub session_watermark_coarsened_total: u64,
pub session_token_rejected_total: u64,
}
impl From<&SessionWatermark> for SessionContextMetrics {
fn from(watermark: &SessionWatermark) -> Self {
Self {
session_watermark_entries: watermark.len() as u64,
session_watermark_coarsened_total: watermark.coarsened_total(),
session_token_rejected_total: 0,
}
}
}
fn token_mac(
session_id: &SessionId,
watermark: &SessionWatermark,
nonce: u64,
issued_at_millis: u64,
secret: &[u8],
) -> u64 {
let mut hash = Fnv64::new();
hash.bytes(secret);
hash.bytes(session_id.as_str().as_bytes());
hash.u64(nonce);
hash.u64(issued_at_millis);
hash.u64(watermark.cap() as u64);
hash.u64(watermark.coarsened_total());
for (key, stamp) in watermark.entries() {
hash.u32(key.partition.value());
hash.bytes(key.region.as_str().as_bytes());
hash.u64(stamp.version);
hash.u64(stamp.epoch.value());
hash.u64(stamp.hlc.wall());
hash.u64(u64::from(stamp.hlc.logical()));
}
hash.finish()
}
#[derive(Debug, Clone, Copy)]
struct Fnv64(u64);
impl Fnv64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
fn new() -> Self {
Self(Self::OFFSET)
}
fn u32(&mut self, value: u32) {
self.bytes(&value.to_le_bytes());
}
fn u64(&mut self, value: u64) {
self.bytes(&value.to_le_bytes());
}
fn bytes(&mut self, bytes: &[u8]) {
for byte in bytes {
self.0 ^= u64::from(*byte);
self.0 = self.0.wrapping_mul(Self::PRIME);
}
}
fn finish(self) -> u64 {
self.0
}
}