use std::collections::BTreeMap;
use std::sync::Arc;
use alkcall::channels::operations::{
ChannelPlan, Establishment, EstablishmentError, OpenEstablisher, OpenHandler,
};
use alkcall::core::types::BiStream;
use alkcall::registry::spec::{
AccessControl, ChannelOpenSpec, OperationSpec, OperationType, Visibility,
};
use parking_lot::Mutex;
use serde_json::{json, Value};
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc;
use super::tokens::OpsContext;
use super::wire::{
declared_error, digest_details_schema, digest_ingress, digest_wire_schema, namespace_schema,
BLOBS_CHANNEL_ALPN, BLOB_MISSING, FETCH_OP_ID,
};
use crate::facade::Blob;
use crate::keys::Key;
use crate::store::Store;
use crate::Error;
pub type FanoutChunk = Arc<Vec<u8>>;
pub type FanoutReceiver = mpsc::Receiver<FanoutChunk>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlowSubscriberPolicy {
Stall,
Drop,
}
struct LiveBroadcast {
policy: SlowSubscriberPolicy,
arms: Vec<mpsc::Sender<FanoutChunk>>,
}
struct FanoutLive {
broadcasts: Mutex<BTreeMap<Key, LiveBroadcast>>,
max_subscribers: usize,
channel_bound: usize,
}
pub struct FanoutRegistry {
live: Arc<FanoutLive>,
}
pub struct FanoutPublisher {
live: Arc<FanoutLive>,
digest: Key,
}
impl FanoutPublisher {
pub fn digest(&self) -> &Key {
&self.digest
}
pub async fn broadcast(&self, chunk: FanoutChunk) {
let (policy, arms) = {
let broadcasts = self.live.broadcasts.lock();
let Some(broadcast) = broadcasts.get(&self.digest) else {
return;
};
(broadcast.policy, broadcast.arms.clone())
};
let mut removed: Vec<mpsc::Sender<FanoutChunk>> = Vec::new();
match policy {
SlowSubscriberPolicy::Stall => {
for arm in &arms {
if arm.send(Arc::clone(&chunk)).await.is_err() {
removed.push(arm.clone());
}
}
}
SlowSubscriberPolicy::Drop => {
for arm in &arms {
if arm.try_send(Arc::clone(&chunk)).is_err() {
removed.push(arm.clone());
}
}
}
}
if !removed.is_empty() {
let mut broadcasts = self.live.broadcasts.lock();
if let Some(broadcast) = broadcasts.get_mut(&self.digest) {
broadcast
.arms
.retain(|kept| !removed.iter().any(|gone| gone.same_channel(kept)));
}
}
}
pub fn finish(self) {}
}
impl Drop for FanoutPublisher {
fn drop(&mut self) {
self.live.broadcasts.lock().remove(&self.digest);
}
}
impl FanoutRegistry {
pub fn new(max_subscribers: usize, channel_bound: usize) -> FanoutRegistry {
FanoutRegistry {
live: Arc::new(FanoutLive {
broadcasts: Mutex::new(BTreeMap::new()),
max_subscribers,
channel_bound,
}),
}
}
pub fn begin(&self, digest: &Key, policy: SlowSubscriberPolicy) -> Option<FanoutPublisher> {
let mut broadcasts = self.live.broadcasts.lock();
if broadcasts.contains_key(digest) {
return None;
}
broadcasts.insert(
digest.clone(),
LiveBroadcast {
policy,
arms: Vec::new(),
},
);
Some(FanoutPublisher {
live: Arc::clone(&self.live),
digest: digest.clone(),
})
}
pub fn subscribe(&self, digest: &Key) -> Option<FanoutReceiver> {
let mut broadcasts = self.live.broadcasts.lock();
let broadcast = broadcasts.get_mut(digest)?;
if broadcast.arms.len() >= self.live.max_subscribers {
return None;
}
let (sender, receiver) = mpsc::channel(self.live.channel_bound);
broadcast.arms.push(sender);
Some(receiver)
}
}
const FETCH_WINDOW: u64 = 64 * 1024;
pub struct FetchRange {
pub offset: u64,
pub len: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct RangedBounds {
pub max_ranges: usize,
pub max_total_bytes: u64,
}
enum FetchPlan {
Whole { blob: Blob },
Ranges { key: Key, ranges: Vec<FetchRange> },
Fanout { receiver: FanoutReceiver },
}
pub fn fetch_openables(ctx: Arc<OpsContext>) -> (OperationSpec, OpenEstablisher, OpenHandler) {
let ranged_bounds = RangedBounds {
max_ranges: ctx.config().fetch_max_ranges,
max_total_bytes: ctx.config().fetch_max_total_bytes,
};
let spec = OperationSpec::new(
FETCH_OP_ID,
OperationType::Sub,
Visibility::External,
json!({
"type": "object",
"properties": {
"namespace": namespace_schema(),
"digest": digest_wire_schema(),
"ranges": {
"type": "array",
"items": {
"type": "object",
"properties": {
"offset": { "type": "integer", "minimum": 0 },
"len": { "type": "integer", "minimum": 0 }
},
"required": ["offset", "len"],
"additionalProperties": false
},
"minItems": 1,
"maxItems": ranged_bounds.max_ranges
}
},
"required": ["namespace", "digest"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {
"channel_id": { "type": "integer", "minimum": 1 },
"source": { "type": "string", "enum": ["store", "fanout"] },
"len": { "type": "integer", "minimum": 0 },
"slice_digests": {
"type": "array",
"items": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
}
},
"required": ["channel_id"],
"additionalProperties": false
}),
vec![declared_error(
BLOB_MISSING,
"the digest is absent from the pool (no committed entry and no live \
broadcast): the establishment refusal answers channel:open_failed with \
reason unknown_resource, the digest in the refusal's message",
digest_details_schema(),
)],
AccessControl {
resource_type: Some("blob-namespace".to_owned()),
resource_action: Some("read".to_owned()),
..AccessControl::default()
},
Some("$.namespace".to_owned()),
)
.with_channel_open(ChannelOpenSpec::new(BLOBS_CHANNEL_ALPN))
.with_description(
"the verified fetch: {namespace, digest, ranges?} in; verified bytes out over \
the alk/blobs channel (consumer-side whole-blob hash-check; the ranged form \
acks the ADR-006 slice digests)",
);
let pump_store = Arc::clone(ctx.store());
let establisher: OpenEstablisher = {
Arc::new(move |input, _auth| {
let ctx = Arc::clone(&ctx);
Box::pin(async move { fetch_establish(&ctx, input, &ranged_bounds).await })
})
};
(spec, establisher, fetch_pump(pump_store))
}
async fn fetch_establish(
ctx: &OpsContext,
input: Value,
bounded: &RangedBounds,
) -> Result<Establishment, EstablishmentError> {
let digest_hex = input
.get("digest")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let key = match digest_ingress(&digest_hex) {
Ok(key) => key,
Err(_) => {
return Err(EstablishmentError::HandlerError {
message: "the digest ingress rejected after the schema gate (unreachable \
post-validation)"
.to_owned(),
})
}
};
let ranges = match fetch_ranges_ingress(&input, bounded) {
Ok(ranges) => ranges,
Err(message) => {
return Err(EstablishmentError::HandlerError { message });
}
};
if let Some(ranges) = ranges {
return fetch_establish_ranges(ctx, &digest_hex, &key, ranges).await;
}
match ctx.store().get(&key.key_bytes()).await {
Ok(Some((entry_len, blob))) => {
let plan: ChannelPlan = Arc::new(FetchPlan::Whole { blob });
Ok(Establishment::new(plan)
.with_reply_field("source", json!("store"))
.with_reply_field("len", json!(entry_len)))
}
Ok(None) => {
if let Some(receiver) = ctx.fanout().subscribe(&key) {
let plan: ChannelPlan = Arc::new(FetchPlan::Fanout { receiver });
Ok(Establishment::new(plan).with_reply_field("source", json!("fanout")))
} else {
Err(fetch_miss(&digest_hex))
}
}
Err(error) => Err(fetch_establish_failure("the verified read", error)),
}
}
async fn fetch_establish_ranges(
ctx: &OpsContext,
digest_hex: &str,
key: &Key,
ranges: Vec<FetchRange>,
) -> Result<Establishment, EstablishmentError> {
let mut slice_digest_hexes = Vec::with_capacity(ranges.len());
for range in &ranges {
match ctx
.store()
.read_range(&key.key_bytes(), range.offset, range.len)
.await
{
Ok((_slice, digest)) => slice_digest_hexes.push(json!(hex::encode(digest))),
Err(Error::Missing) => return Err(fetch_miss(digest_hex)),
Err(error) => {
return Err(fetch_establish_failure("the ranged read", error));
}
}
}
let plan: ChannelPlan = Arc::new(FetchPlan::Ranges {
key: key.clone(),
ranges,
});
Ok(Establishment::new(plan)
.with_reply_field("source", json!("store"))
.with_reply_field("slice_digests", Value::Array(slice_digest_hexes)))
}
pub fn fetch_ranges_ingress(
input: &Value,
bounds: &RangedBounds,
) -> Result<Option<Vec<FetchRange>>, String> {
let Some(values) = input.get("ranges") else {
return Ok(None);
};
let Some(items) = values.as_array() else {
return Err("the ranges field failed its ingress: expected an array of \
{offset, len} items"
.to_owned());
};
let mut ranges = Vec::with_capacity(items.len());
let mut total_bytes = 0u64;
for value in items {
match (
value.get("offset").and_then(Value::as_u64),
value.get("len").and_then(Value::as_u64),
) {
(Some(offset), Some(len)) => {
total_bytes = {
match total_bytes.checked_add(len) {
Some(summed) => summed,
None => {
return Err(format!(
"the ranges request exceeds the total-bytes bound: the \
requested lens sum past the limit of {} bytes",
bounds.max_total_bytes
))
}
}
};
ranges.push(FetchRange { offset, len });
}
_ => {
return Err(
"a range item failed its ingress: offset and len are required, \
non-negative integers"
.to_owned(),
)
}
}
}
if total_bytes > bounds.max_total_bytes {
return Err(format!(
"the ranges request exceeds the total-bytes bound: {} bytes requested, the \
limit is {} bytes",
total_bytes, bounds.max_total_bytes
));
}
Ok(Some(ranges))
}
fn fetch_miss(digest_hex: &str) -> EstablishmentError {
EstablishmentError::UnknownResource {
message: format!(
"the digest {digest_hex} is absent from the pool (no committed entry and no \
live broadcast)"
),
}
}
fn fetch_establish_failure(stage: &str, _error: Error) -> EstablishmentError {
EstablishmentError::HandlerError {
message: format!("the store core failed at {stage}"),
}
}
fn fetch_pump(store: Arc<Store>) -> OpenHandler {
Arc::new(move |_input, plan, channel_conn, _auth| {
let store = Arc::clone(&store);
tokio::spawn(async move {
let Some(bidi) = channel_conn.accept_bi().await.ok() else {
return;
};
let Some(plan) = plan.and_then(|p| p.downcast::<FetchPlan>().ok()) else {
return;
};
let plan = match Arc::try_unwrap(plan) {
Ok(plan) => plan,
Err(_) => return,
};
match plan {
FetchPlan::Whole { blob } => fetch_pump_whole(bidi, blob).await,
FetchPlan::Ranges { key, ranges } => {
fetch_pump_ranges(bidi, &store, &key, &ranges).await
}
FetchPlan::Fanout { receiver } => fetch_pump_fanout(bidi, receiver).await,
}
})
})
}
async fn fetch_pump_whole(mut stream: BiStream, blob: Blob) {
let len = blob.len();
let mut pos = 0u64;
while pos < len {
let window = FETCH_WINDOW.min(len - pos);
match blob.read_at(pos, window).await {
Ok(bytes) => {
if stream.write_all(&bytes).await.is_err() {
return;
}
pos += bytes.len() as u64;
}
Err(_) => return,
}
}
}
async fn fetch_pump_ranges(mut stream: BiStream, store: &Store, key: &Key, ranges: &[FetchRange]) {
for range in ranges {
match store
.read_range(&key.key_bytes(), range.offset, range.len)
.await
{
Ok((slice, _digest)) => {
if stream.write_all(&slice).await.is_err() {
return;
}
}
Err(_) => return,
}
}
}
async fn fetch_pump_fanout(mut stream: BiStream, mut receiver: FanoutReceiver) {
while let Some(chunk) = receiver.recv().await {
if stream.write_all(&chunk).await.is_err() {
return;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ops::tests::key_of;
#[tokio::test]
async fn fanout_subscriber_arms_receive_the_full_stream_and_late_joiners_degrade() {
let registry = FanoutRegistry::new(8, 16);
let digest = key_of(b"streamed");
let no_broadcast = key_of(b"not-a-broadcast");
assert!(
registry.subscribe(&no_broadcast).is_none(),
"no live broadcast: the late-joiner/absent degrade signal"
);
let publisher = registry
.begin(&digest, SlowSubscriberPolicy::Stall)
.unwrap();
assert_eq!(publisher.digest(), &digest);
let mut first = registry.subscribe(&digest).unwrap();
let mut second = registry.subscribe(&digest).unwrap();
for chunk in 0u8..4 {
publisher.broadcast(Arc::new(vec![chunk])).await;
}
publisher.finish();
for chunk in 0u8..4 {
assert_eq!(
first.recv().await,
Some(Arc::new(vec![chunk])),
"subscriber arms receive in order"
);
}
for chunk in 0u8..4 {
assert_eq!(second.recv().await, Some(Arc::new(vec![chunk])));
}
assert_eq!(
first.recv().await,
None,
"the broadcast end is the degrade signal"
);
assert!(
registry.subscribe(&digest).is_none(),
"an ended broadcast is gone"
);
}
#[tokio::test]
async fn stall_policy_backpressures_a_slow_subscriber() {
let registry = FanoutRegistry::new(8, 1);
let digest = key_of(b"stalled");
let publisher = registry
.begin(&digest, SlowSubscriberPolicy::Stall)
.unwrap();
let mut subscriber = registry.subscribe(&digest).unwrap();
publisher.broadcast(Arc::new(vec![0u8])).await;
let slow = tokio::time::timeout(
std::time::Duration::from_millis(50),
publisher.broadcast(Arc::new(vec![1u8])),
)
.await;
assert!(
slow.is_err(),
"the full bounded channel stalls the broadcast"
);
assert_eq!(subscriber.recv().await, Some(Arc::new(vec![0u8])));
publisher.broadcast(Arc::new(vec![1u8])).await;
assert_eq!(subscriber.recv().await, Some(Arc::new(vec![1u8])));
publisher.finish();
}
#[tokio::test]
async fn drop_policy_drops_a_stalled_arm_and_the_freed_bound_admits_a_late_join() {
let registry = FanoutRegistry::new(2, 1);
let digest = key_of(b"dropped-arm");
let publisher = registry.begin(&digest, SlowSubscriberPolicy::Drop).unwrap();
let mut stalled = registry.subscribe(&digest).unwrap();
publisher.broadcast(Arc::new(vec![0u8])).await;
publisher.broadcast(Arc::new(vec![1u8])).await;
publisher.broadcast(Arc::new(vec![2u8])).await;
assert_eq!(
stalled.recv().await,
Some(Arc::new(vec![0u8])),
"drainage carries the buffered chunk before the channel end"
);
assert_eq!(
stalled.recv().await,
None,
"the dropped arm's channel end degrades"
);
let mut rejoined = registry.subscribe(&digest).unwrap();
publisher.broadcast(Arc::new(vec![3u8])).await;
assert_eq!(rejoined.recv().await, Some(Arc::new(vec![3u8])));
publisher.finish();
}
#[tokio::test]
async fn publisher_drop_aborts_the_broadcast_and_late_joiners_degrade() {
let registry = FanoutRegistry::new(8, 2);
let digest = key_of(b"abandoned");
let publisher = registry
.begin(&digest, SlowSubscriberPolicy::Stall)
.unwrap();
let mut subscriber = registry.subscribe(&digest).unwrap();
drop(publisher);
assert_eq!(
subscriber.recv().await,
None,
"the abandoned broadcast ends"
);
assert!(registry.subscribe(&digest).is_none());
}
#[tokio::test]
async fn subscriber_bound_excess_degrades() {
let registry = FanoutRegistry::new(1, 4);
let digest = key_of(b"bounded");
let _publisher = registry.begin(&digest, SlowSubscriberPolicy::Drop).unwrap();
assert!(registry.subscribe(&digest).is_some());
assert!(
registry.subscribe(&digest).is_none(),
"the second arm exceeds the bound: the degrade signal"
);
}
}