use std::{collections::HashMap, fmt, sync::Arc};
use anyhow::{Result, bail};
const MAX_VIEW_ID_BYTES: usize = 128;
pub const MAX_ATTACH_WATERMARKS_PER_CLIENT: usize = 256;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ViewAccess {
ReadOnly,
ReadWrite,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ControllerState {
pub view_id: String,
pub client_id: String,
pub control_epoch: u64,
pub attachment_epoch: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ControlSnapshot {
pub controller: Option<ControllerState>,
pub control_revision: u64,
pub cols: u16,
pub rows: u16,
pub layout_epoch: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ControlClaim {
Granted(ControlChanged),
Rejected(ControlSnapshot),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ResumeEvidence {
pub previous_session_epoch: u64,
pub previous_attachment_epoch: u64,
pub previous_terminal_revision: u64,
}
#[derive(Clone, Copy, Debug)]
pub struct TakeOverRequest<'a> {
pub view_id: &'a str,
pub client_id: &'a str,
pub access: ViewAccess,
pub attach_generation: u64,
pub fence_conn_id: u64,
pub session_epoch: u64,
pub resume: Option<ResumeEvidence>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TakeOver {
pub attachment_epoch: u64,
pub resumed: bool,
pub controller_cleared: bool,
pub controller: Option<ControllerState>,
pub control_revision: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttachRejectionCode {
StaleResume,
ViewInvalid,
ViewLimit,
SessionEpochMismatch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AttachRejection {
pub code: AttachRejectionCode,
pub detail: String,
}
impl AttachRejection {
fn new(code: AttachRejectionCode, detail: impl Into<String>) -> Self {
Self {
code,
detail: detail.into(),
}
}
}
impl fmt::Display for AttachRejection {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.detail)
}
}
impl std::error::Error for AttachRejection {}
#[derive(Clone)]
pub struct StateStreamCancel(Arc<dyn Fn() + Send + Sync>);
impl StateStreamCancel {
pub fn new(cancel: impl Fn() + Send + Sync + 'static) -> Self {
Self(Arc::new(cancel))
}
fn fire(&self) {
(self.0)();
}
}
impl fmt::Debug for StateStreamCancel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("StateStreamCancel")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct AttachWatermark {
generation: u64,
fence_conn_id: u64,
}
#[derive(Clone, Debug)]
struct AttachedView {
client_id: String,
access: ViewAccess,
attachment_epoch: u64,
last_input_sequence: u64,
last_resize_sequence: u64,
state_stream: Option<StateStreamCancel>,
}
impl AttachedView {
fn cancel_state_stream(&self) {
if let Some(cancel) = &self.state_stream {
cancel.fire();
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ControlChanged {
pub controller: ControllerState,
pub cols: u16,
pub rows: u16,
pub layout_epoch: u64,
pub size_changed: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PreparedResize {
resize_sequence: u64,
cols: u16,
rows: u16,
layout_epoch: u64,
size_changed: bool,
}
impl PreparedResize {
pub fn layout_epoch(self) -> u64 {
self.layout_epoch
}
pub fn size_changed(self) -> bool {
self.size_changed
}
}
#[derive(Clone, Debug)]
pub struct ViewAuthority {
views: HashMap<String, AttachedView>,
controller: Option<ControllerState>,
next_control_epoch: u64,
next_attachment_epoch: u64,
control_revision: u64,
attach_watermarks: HashMap<String, HashMap<String, AttachWatermark>>,
cols: u16,
rows: u16,
layout_epoch: u64,
}
impl ViewAuthority {
pub fn new(cols: u16, rows: u16) -> Self {
Self {
views: HashMap::new(),
controller: None,
next_control_epoch: 0,
next_attachment_epoch: 0,
control_revision: 1,
attach_watermarks: HashMap::new(),
cols,
rows,
layout_epoch: 1,
}
}
pub fn attach(&mut self, view_id: &str, client_id: &str, access: ViewAccess) -> Result<u64> {
if view_id.is_empty() || view_id.len() > MAX_VIEW_ID_BYTES {
bail!("invalid view id");
}
if let Some(existing) = self.views.get(view_id) {
if existing.client_id == client_id {
return Ok(existing.attachment_epoch);
}
bail!("view id is already attached by another client");
}
self.next_attachment_epoch = self.next_attachment_epoch.saturating_add(1);
let attachment_epoch = self.next_attachment_epoch;
self.views.insert(
view_id.to_owned(),
AttachedView {
client_id: client_id.to_owned(),
access,
attachment_epoch,
last_input_sequence: 0,
last_resize_sequence: 0,
state_stream: None,
},
);
Ok(attachment_epoch)
}
pub fn take_over(
&mut self,
request: TakeOverRequest<'_>,
) -> std::result::Result<TakeOver, AttachRejection> {
let TakeOverRequest {
view_id,
client_id,
access,
attach_generation,
fence_conn_id,
session_epoch,
resume,
} = request;
if view_id.is_empty() || view_id.len() > MAX_VIEW_ID_BYTES {
return Err(AttachRejection::new(
AttachRejectionCode::ViewInvalid,
"invalid view id",
));
}
if let Some(resume) = resume
&& resume.previous_session_epoch != session_epoch
{
return Err(AttachRejection::new(
AttachRejectionCode::SessionEpochMismatch,
format!(
"resume expects session epoch {} but the session is at {session_epoch}",
resume.previous_session_epoch
),
));
}
let watermark = self
.attach_watermarks
.get(client_id)
.and_then(|views| views.get(view_id))
.copied();
match watermark {
Some(watermark) if attach_generation <= watermark.generation => {
return Err(AttachRejection::new(
AttachRejectionCode::StaleResume,
format!(
"attach generation {attach_generation} is not newer than {}",
watermark.generation
),
));
}
Some(_) => {}
None => {
let outstanding = self
.attach_watermarks
.get(client_id)
.map_or(0, HashMap::len);
if outstanding >= MAX_ATTACH_WATERMARKS_PER_CLIENT {
return Err(AttachRejection::new(
AttachRejectionCode::ViewLimit,
format!(
"client holds {outstanding} outstanding attach watermarks on this session"
),
));
}
}
}
if let Some(existing) = self.views.get(view_id)
&& existing.client_id != client_id
{
return Err(AttachRejection::new(
AttachRejectionCode::ViewInvalid,
"view id is already attached by another client",
));
}
let fence_conn_id = watermark.map_or(fence_conn_id, |watermark| {
watermark.fence_conn_id.max(fence_conn_id)
});
self.attach_watermarks
.entry(client_id.to_owned())
.or_default()
.insert(
view_id.to_owned(),
AttachWatermark {
generation: attach_generation,
fence_conn_id,
},
);
let previous = self.views.remove(view_id);
if let Some(previous) = &previous {
previous.cancel_state_stream();
}
self.next_attachment_epoch = self.next_attachment_epoch.saturating_add(1);
let attachment_epoch = self.next_attachment_epoch;
self.views.insert(
view_id.to_owned(),
AttachedView {
client_id: client_id.to_owned(),
access,
attachment_epoch,
last_input_sequence: 0,
last_resize_sequence: 0,
state_stream: None,
},
);
let controller_cleared = self.clear_controller_for_view(view_id);
Ok(TakeOver {
attachment_epoch,
resumed: previous.is_some(),
controller_cleared,
controller: self.controller.clone(),
control_revision: self.control_revision,
})
}
pub fn gc_attach_watermarks(
&mut self,
client_id: &str,
terminated_through_conn_id: u64,
) -> usize {
let attached: Vec<&str> = self
.views
.iter()
.filter(|(_, view)| view.client_id == client_id)
.map(|(view_id, _)| view_id.as_str())
.collect();
let Some(watermarks) = self.attach_watermarks.get_mut(client_id) else {
return 0;
};
let before = watermarks.len();
watermarks.retain(|view_id, watermark| {
attached.iter().any(|attached| *attached == view_id)
|| watermark.fence_conn_id > terminated_through_conn_id
});
let freed = before - watermarks.len();
if watermarks.is_empty() {
self.attach_watermarks.remove(client_id);
}
freed
}
pub fn attach_watermark_count(&self, client_id: &str) -> usize {
self.attach_watermarks
.get(client_id)
.map_or(0, HashMap::len)
}
pub fn register_state_stream(
&mut self,
view_id: &str,
attachment_epoch: u64,
cancel: StateStreamCancel,
) -> Result<()> {
let view = self
.views
.get_mut(view_id)
.ok_or_else(|| anyhow::anyhow!("view is not attached"))?;
if view.attachment_epoch != attachment_epoch {
bail!("stale attachment epoch");
}
view.state_stream = Some(cancel);
Ok(())
}
pub fn detach(&mut self, view_id: &str, client_id: &str) -> bool {
let owned = self
.views
.get(view_id)
.is_some_and(|view| view.client_id == client_id);
if !owned {
return false;
}
if let Some(view) = self.views.remove(view_id) {
view.cancel_state_stream();
}
self.clear_controller_for_view(view_id);
true
}
pub fn detach_view_if_epoch(
&mut self,
view_id: &str,
client_id: &str,
attachment_epoch: u64,
) -> bool {
let current = self.views.get(view_id).is_some_and(|view| {
view.client_id == client_id && view.attachment_epoch == attachment_epoch
});
if !current {
return false;
}
if let Some(view) = self.views.remove(view_id) {
view.cancel_state_stream();
}
self.clear_controller_for_view(view_id);
true
}
pub fn claim_control(
&mut self,
view_id: &str,
client_id: &str,
cols: u16,
rows: u16,
) -> Result<ControlChanged> {
let view = self.require_view(view_id, client_id)?;
if view.access != ViewAccess::ReadWrite {
bail!("read-only view cannot control terminal size");
}
let attachment_epoch = view.attachment_epoch;
Ok(self.commit_claim(view_id, client_id, attachment_epoch, cols, rows))
}
pub fn claim_control_checked(
&mut self,
view_id: &str,
client_id: &str,
attachment_epoch: u64,
cols: u16,
rows: u16,
expected_control_revision: Option<u64>,
) -> Result<ControlClaim> {
let view = self.require_view(view_id, client_id)?;
if view.access != ViewAccess::ReadWrite {
bail!("read-only view cannot control terminal size");
}
if view.attachment_epoch != attachment_epoch {
bail!("stale attachment epoch");
}
if expected_control_revision.is_some_and(|expected| expected != self.control_revision) {
return Ok(ControlClaim::Rejected(self.control_snapshot()));
}
Ok(ControlClaim::Granted(self.commit_claim(
view_id,
client_id,
attachment_epoch,
cols,
rows,
)))
}
fn commit_claim(
&mut self,
view_id: &str,
client_id: &str,
attachment_epoch: u64,
cols: u16,
rows: u16,
) -> ControlChanged {
self.next_control_epoch = self.next_control_epoch.saturating_add(1);
let controller = ControllerState {
view_id: view_id.to_owned(),
client_id: client_id.to_owned(),
control_epoch: self.next_control_epoch,
attachment_epoch,
};
self.controller = Some(controller.clone());
self.control_revision = self.control_revision.saturating_add(1);
let size_changed = (self.cols, self.rows) != (cols, rows);
if size_changed {
self.cols = cols;
self.rows = rows;
self.layout_epoch = self.layout_epoch.saturating_add(1);
}
ControlChanged {
controller,
cols: self.cols,
rows: self.rows,
layout_epoch: self.layout_epoch,
size_changed,
}
}
fn clear_controller_for_view(&mut self, view_id: &str) -> bool {
if !self
.controller
.as_ref()
.is_some_and(|controller| controller.view_id == view_id)
{
return false;
}
self.controller = None;
self.control_revision = self.control_revision.saturating_add(1);
true
}
pub fn authorize_resize(
&mut self,
view_id: &str,
client_id: &str,
control_epoch: u64,
resize_sequence: u64,
cols: u16,
rows: u16,
) -> Result<bool> {
let Some(prepared) = self.prepare_resize(
view_id,
client_id,
control_epoch,
resize_sequence,
cols,
rows,
)?
else {
return Ok(false);
};
self.commit_resize(view_id, prepared);
Ok(prepared.size_changed)
}
#[allow(clippy::too_many_arguments)]
pub fn authorize_resize_checked(
&mut self,
view_id: &str,
client_id: &str,
attachment_epoch: u64,
control_epoch: u64,
resize_sequence: u64,
cols: u16,
rows: u16,
) -> Result<bool> {
let Some(prepared) = self.prepare_resize_checked(
view_id,
client_id,
attachment_epoch,
control_epoch,
resize_sequence,
cols,
rows,
)?
else {
return Ok(false);
};
self.commit_resize(view_id, prepared);
Ok(prepared.size_changed)
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_resize_checked(
&self,
view_id: &str,
client_id: &str,
attachment_epoch: u64,
control_epoch: u64,
resize_sequence: u64,
cols: u16,
rows: u16,
) -> Result<Option<PreparedResize>> {
let view = self.require_view(view_id, client_id)?;
if view.attachment_epoch != attachment_epoch {
bail!("stale attachment epoch");
}
self.prepare_resize(
view_id,
client_id,
control_epoch,
resize_sequence,
cols,
rows,
)
}
pub fn prepare_resize(
&self,
view_id: &str,
client_id: &str,
control_epoch: u64,
resize_sequence: u64,
cols: u16,
rows: u16,
) -> Result<Option<PreparedResize>> {
let authorized = self.controller.as_ref().is_some_and(|controller| {
controller.view_id == view_id
&& controller.client_id == client_id
&& controller.control_epoch == control_epoch
});
if !authorized {
bail!("stale or unauthorized resize controller");
}
let view = self.require_view(view_id, client_id)?;
if resize_sequence <= view.last_resize_sequence {
return Ok(None);
}
let size_changed = (self.cols, self.rows) != (cols, rows);
Ok(Some(PreparedResize {
resize_sequence,
cols,
rows,
layout_epoch: if size_changed {
self.layout_epoch.saturating_add(1)
} else {
self.layout_epoch
},
size_changed,
}))
}
pub fn commit_resize(&mut self, view_id: &str, prepared: PreparedResize) {
let view = self
.views
.get_mut(view_id)
.expect("prepared resize view must remain attached while authority is locked");
view.last_resize_sequence = prepared.resize_sequence;
if prepared.size_changed {
self.cols = prepared.cols;
self.rows = prepared.rows;
self.layout_epoch = prepared.layout_epoch;
}
}
pub fn authorize_input(
&mut self,
view_id: &str,
client_id: &str,
attachment_epoch: u64,
input_sequence: u64,
) -> Result<bool> {
let view = self.require_view_mut(view_id, client_id)?;
if view.access != ViewAccess::ReadWrite {
bail!("view is read-only");
}
if view.attachment_epoch != attachment_epoch {
bail!("stale attachment epoch");
}
if input_sequence <= view.last_input_sequence {
return Ok(false);
}
view.last_input_sequence = input_sequence;
Ok(true)
}
pub fn controller(&self) -> Option<&ControllerState> {
self.controller.as_ref()
}
pub fn control_revision(&self) -> u64 {
self.control_revision
}
pub fn control_snapshot(&self) -> ControlSnapshot {
ControlSnapshot {
controller: self.controller.clone(),
control_revision: self.control_revision,
cols: self.cols,
rows: self.rows,
layout_epoch: self.layout_epoch,
}
}
pub fn attachment_epoch(&self, view_id: &str) -> Option<u64> {
self.views.get(view_id).map(|view| view.attachment_epoch)
}
pub fn layout_epoch(&self) -> u64 {
self.layout_epoch
}
pub fn size(&self) -> (u16, u16) {
(self.cols, self.rows)
}
pub fn has_views(&self) -> bool {
!self.views.is_empty()
}
fn require_view(&self, view_id: &str, client_id: &str) -> Result<&AttachedView> {
let view = self
.views
.get(view_id)
.ok_or_else(|| anyhow::anyhow!("view is not attached"))?;
if view.client_id != client_id {
bail!("view belongs to another client");
}
Ok(view)
}
fn require_view_mut(&mut self, view_id: &str, client_id: &str) -> Result<&mut AttachedView> {
let view = self
.views
.get_mut(view_id)
.ok_or_else(|| anyhow::anyhow!("view is not attached"))?;
if view.client_id != client_id {
bail!("view belongs to another client");
}
Ok(view)
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, Ordering};
use super::*;
fn attempt<'a>(
view_id: &'a str,
client_id: &'a str,
attach_generation: u64,
) -> TakeOverRequest<'a> {
TakeOverRequest {
view_id,
client_id,
access: ViewAccess::ReadWrite,
attach_generation,
fence_conn_id: 1,
session_epoch: 1,
resume: None,
}
}
#[test]
fn every_attach_attempt_is_ordered_even_without_resume_evidence() {
let mut authority = ViewAuthority::new(80, 24);
let first = authority.take_over(attempt("a", "client", 1)).unwrap();
assert!(!first.resumed);
let stale = authority.take_over(attempt("a", "client", 1)).unwrap_err();
assert_eq!(stale.code, AttachRejectionCode::StaleResume);
let second = authority.take_over(attempt("a", "client", 2)).unwrap();
assert!(second.resumed);
assert!(second.attachment_epoch > first.attachment_epoch);
assert!(
authority
.authorize_input("a", "client", first.attachment_epoch, 1)
.is_err()
);
let mut legacy = ViewAuthority::new(80, 24);
let once = legacy.attach("a", "client", ViewAccess::ReadWrite).unwrap();
let twice = legacy.attach("a", "client", ViewAccess::ReadWrite).unwrap();
assert_eq!(once, twice);
}
#[test]
fn resume_validates_the_session_epoch_before_touching_any_state() {
let mut authority = ViewAuthority::new(80, 24);
let rejected = authority
.take_over(TakeOverRequest {
attach_generation: 9,
resume: Some(ResumeEvidence {
previous_session_epoch: 4,
previous_attachment_epoch: 2,
previous_terminal_revision: 77,
}),
session_epoch: 5,
..attempt("a", "client", 9)
})
.unwrap_err();
assert_eq!(rejected.code, AttachRejectionCode::SessionEpochMismatch);
assert_eq!(authority.attach_watermark_count("client"), 0);
let accepted = authority.take_over(attempt("a", "client", 1)).unwrap();
assert_eq!(accepted.attachment_epoch, 1);
}
#[test]
fn watermark_gc_waits_for_every_connection_that_could_deliver_an_older_attach() {
let mut authority = ViewAuthority::new(80, 24);
let attached = authority
.take_over(TakeOverRequest {
fence_conn_id: 7,
..attempt("a", "client", 5)
})
.unwrap();
assert!(authority.detach_view_if_epoch("a", "client", attached.attachment_epoch));
assert_eq!(authority.gc_attach_watermarks("client", 2), 0);
assert_eq!(authority.attach_watermark_count("client"), 1);
let delayed = authority.take_over(attempt("a", "client", 1)).unwrap_err();
assert_eq!(delayed.code, AttachRejectionCode::StaleResume);
assert_eq!(authority.gc_attach_watermarks("client", 7), 1);
assert_eq!(authority.attach_watermark_count("client"), 0);
assert!(authority.take_over(attempt("a", "client", 1)).is_ok());
}
#[test]
fn attested_termination_collects_every_watermark_it_covers() {
let mut authority = ViewAuthority::new(80, 24);
for index in 0..8 {
let view_id = format!("view-{index}");
let attached = authority
.take_over(TakeOverRequest {
fence_conn_id: index + 1,
..attempt(&view_id, "client", 1)
})
.unwrap();
assert!(authority.detach_view_if_epoch(&view_id, "client", attached.attachment_epoch));
}
assert_eq!(authority.attach_watermark_count("client"), 8);
assert_eq!(authority.gc_attach_watermarks("client", 4), 4);
assert_eq!(authority.attach_watermark_count("client"), 4);
assert_eq!(authority.gc_attach_watermarks("client", 8), 4);
assert_eq!(
authority.attach_watermark_count("client"),
0,
"attesting every fencing connection terminated must leave nothing behind"
);
}
#[test]
fn watermarks_of_attached_views_survive_gc() {
let mut authority = ViewAuthority::new(80, 24);
authority.take_over(attempt("a", "client", 1)).unwrap();
assert_eq!(authority.gc_attach_watermarks("client", u64::MAX), 0);
assert_eq!(authority.attach_watermark_count("client"), 1);
}
#[test]
fn outstanding_watermarks_are_capped_per_client() {
let mut authority = ViewAuthority::new(80, 24);
for index in 0..MAX_ATTACH_WATERMARKS_PER_CLIENT {
let view_id = format!("view-{index}");
let attached = authority.take_over(attempt(&view_id, "client", 1)).unwrap();
authority.detach_view_if_epoch(&view_id, "client", attached.attachment_epoch);
}
assert_eq!(
authority.attach_watermark_count("client"),
MAX_ATTACH_WATERMARKS_PER_CLIENT
);
let over = authority.take_over(attempt("one-too-many", "client", 1));
assert_eq!(
over.unwrap_err().code,
AttachRejectionCode::ViewLimit,
"a new watermark key past the cap must fail safe"
);
assert!(authority.take_over(attempt("view-0", "client", 2)).is_ok());
assert!(
authority
.take_over(attempt("elsewhere", "other", 1))
.is_ok()
);
}
#[test]
fn control_revision_advances_on_claims_and_on_clears() {
let mut authority = ViewAuthority::new(80, 24);
assert_eq!(authority.control_revision(), 1);
authority
.attach("a", "client", ViewAccess::ReadWrite)
.unwrap();
authority.claim_control("a", "client", 100, 30).unwrap();
assert_eq!(authority.control_revision(), 2);
assert!(authority.detach("a", "client"));
assert!(authority.controller().is_none());
assert_eq!(authority.control_revision(), 3);
}
#[test]
fn a_claim_expecting_a_superseded_revision_loses_to_an_intervening_clear() {
let mut authority = ViewAuthority::new(80, 24);
let a = authority
.attach("a", "client-a", ViewAccess::ReadWrite)
.unwrap();
let b = authority
.attach("b", "client-b", ViewAccess::ReadWrite)
.unwrap();
authority.claim_control("a", "client-a", 100, 30).unwrap();
let observed = authority.control_revision();
assert!(authority.detach_view_if_epoch("a", "client-a", a));
let rejected = authority
.claim_control_checked("b", "client-b", b, 120, 40, Some(observed))
.unwrap();
let ControlClaim::Rejected(state) = rejected else {
panic!("a claim against a superseded revision must not be granted");
};
assert!(state.controller.is_none());
assert_eq!(state.control_revision, authority.control_revision());
assert_eq!(
authority.size(),
(100, 30),
"a rejected claim resizes nothing"
);
let granted = authority
.claim_control_checked("b", "client-b", b, 120, 40, Some(state.control_revision))
.unwrap();
assert!(matches!(granted, ControlClaim::Granted(_)));
assert_eq!(authority.size(), (120, 40));
authority
.attach("c", "client-c", ViewAccess::ReadWrite)
.unwrap();
let legacy = authority
.claim_control_checked("c", "client-c", 3, 80, 24, None)
.unwrap();
assert!(matches!(legacy, ControlClaim::Granted(_)));
}
#[test]
fn takeover_clears_control_held_by_the_previous_incarnation() {
let mut authority = ViewAuthority::new(80, 24);
let first = authority.take_over(attempt("a", "client", 1)).unwrap();
authority.claim_control("a", "client", 100, 30).unwrap();
assert_eq!(
authority.controller().unwrap().attachment_epoch,
first.attachment_epoch
);
let second = authority.take_over(attempt("a", "client", 2)).unwrap();
assert!(second.controller_cleared);
assert!(authority.controller().is_none());
assert_eq!(second.control_revision, authority.control_revision());
assert_eq!(authority.size(), (100, 30), "a takeover keeps the size");
}
#[test]
fn takeover_reports_the_controller_another_view_still_holds() {
let mut authority = ViewAuthority::new(80, 24);
let holder = authority.take_over(attempt("b", "client", 1)).unwrap();
authority.claim_control("b", "client", 100, 30).unwrap();
let taken = authority.take_over(attempt("a", "client", 1)).unwrap();
assert!(!taken.controller_cleared);
let controller = taken.controller.expect("b still holds control");
assert_eq!(controller.view_id, "b");
assert_eq!(controller.attachment_epoch, holder.attachment_epoch);
assert_eq!(taken.control_revision, authority.control_revision());
let retaken = authority.take_over(attempt("b", "client", 2)).unwrap();
assert!(retaken.controller_cleared);
assert!(retaken.controller.is_none());
assert_eq!(retaken.control_revision, authority.control_revision());
}
#[test]
fn checked_control_and_resize_reject_a_superseded_incarnation() {
let mut authority = ViewAuthority::new(80, 24);
let first = authority.take_over(attempt("a", "client", 1)).unwrap();
authority.claim_control("a", "client", 100, 30).unwrap();
let second = authority.take_over(attempt("a", "client", 2)).unwrap();
assert!(
authority
.claim_control_checked("a", "client", first.attachment_epoch, 90, 20, None)
.is_err()
);
let reclaimed = authority
.claim_control_checked("a", "client", second.attachment_epoch, 100, 30, None)
.unwrap();
let ControlClaim::Granted(reclaimed) = reclaimed else {
panic!("the current incarnation must be able to claim control");
};
assert!(
authority
.authorize_resize_checked(
"a",
"client",
first.attachment_epoch,
reclaimed.controller.control_epoch,
1,
90,
20,
)
.is_err(),
"a superseded incarnation must not resize even with the live control epoch"
);
assert_eq!(authority.size(), (100, 30));
assert!(
authority
.authorize_resize_checked(
"a",
"client",
second.attachment_epoch,
reclaimed.controller.control_epoch,
1,
101,
31,
)
.unwrap()
);
}
#[test]
fn state_stream_registration_is_epoch_checked_and_cancelled_by_takeover() {
let mut authority = ViewAuthority::new(80, 24);
let first = authority.take_over(attempt("a", "client", 1)).unwrap();
let cancelled = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&cancelled);
authority
.register_state_stream(
"a",
first.attachment_epoch,
StateStreamCancel::new(move || flag.store(true, Ordering::SeqCst)),
)
.unwrap();
let second = authority.take_over(attempt("a", "client", 2)).unwrap();
assert!(
cancelled.load(Ordering::SeqCst),
"takeover must fire the previous incarnation's cancel handle"
);
assert!(
authority
.register_state_stream("a", first.attachment_epoch, StateStreamCancel::new(|| ()),)
.is_err()
);
assert!(
authority
.register_state_stream("a", second.attachment_epoch, StateStreamCancel::new(|| ()),)
.is_ok()
);
}
#[test]
fn epoch_conditional_detach_spares_the_incarnation_that_replaced_it() {
let mut authority = ViewAuthority::new(80, 24);
let first = authority.take_over(attempt("a", "client", 1)).unwrap();
let second = authority.take_over(attempt("a", "client", 2)).unwrap();
assert!(!authority.detach_view_if_epoch("a", "client", first.attachment_epoch));
assert_eq!(
authority.attachment_epoch("a"),
Some(second.attachment_epoch)
);
assert!(authority.detach_view_if_epoch("a", "client", second.attachment_epoch));
assert!(authority.attachment_epoch("a").is_none());
}
#[test]
fn a_view_held_by_another_client_is_not_takeable() {
let mut authority = ViewAuthority::new(80, 24);
authority.take_over(attempt("a", "client", 1)).unwrap();
let rejected = authority.take_over(attempt("a", "other", 1)).unwrap_err();
assert_eq!(rejected.code, AttachRejectionCode::ViewInvalid);
assert_eq!(
authority.attach_watermark_count("other"),
0,
"a rejected attempt must not raise a watermark"
);
}
#[test]
fn last_authority_processed_claim_wins_and_stale_resize_fails() {
let mut authority = ViewAuthority::new(80, 24);
authority
.attach("a", "client-a", ViewAccess::ReadWrite)
.unwrap();
authority
.attach("b", "client-b", ViewAccess::ReadWrite)
.unwrap();
let a = authority.claim_control("a", "client-a", 100, 30).unwrap();
let b = authority.claim_control("b", "client-b", 120, 40).unwrap();
assert!(b.controller.control_epoch > a.controller.control_epoch);
assert!(
authority
.authorize_resize("a", "client-a", a.controller.control_epoch, 1, 90, 20)
.is_err()
);
assert_eq!(authority.size(), (120, 40));
let reclaimed = authority.claim_control("a", "client-a", 100, 30).unwrap();
assert!(
authority
.authorize_resize(
"a",
"client-a",
reclaimed.controller.control_epoch,
1,
101,
31,
)
.unwrap()
);
}
#[test]
fn input_is_deduplicated_per_attachment() {
let mut authority = ViewAuthority::new(80, 24);
let epoch = authority
.attach("a", "client", ViewAccess::ReadWrite)
.unwrap();
assert!(authority.authorize_input("a", "client", epoch, 1).unwrap());
assert!(!authority.authorize_input("a", "client", epoch, 1).unwrap());
assert!(
authority
.authorize_input("a", "client", epoch + 1, 2)
.is_err()
);
}
#[test]
fn detaching_controller_keeps_size_and_clears_controller() {
let mut authority = ViewAuthority::new(80, 24);
authority
.attach("a", "client", ViewAccess::ReadWrite)
.unwrap();
authority.claim_control("a", "client", 100, 30).unwrap();
assert!(authority.detach("a", "client"));
assert!(authority.controller().is_none());
assert_eq!(authority.size(), (100, 30));
}
}