use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read as _, Seek as _, SeekFrom, Write as _};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use std::time::{Duration, Instant, UNIX_EPOCH};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use crate::plugin::{
register_protocol, serve, PluginAnswer, PluginError, PluginHello, PluginReply, PluginRequest,
PluginStored, PluginTorn, ProcessTransport, HELLO_SCHEMA, PLUGIN_PREFIX, PROTOCOL,
PROTOCOL_VERSION, REPLY_SCHEMA, REQUEST_SCHEMA,
};
pub trait Transport: Send + Sync + 'static {
fn append(&self, queue: &QueueName, record: &[u8]) -> Result<Position, TransportError>;
fn read(
&self,
queue: &QueueName,
from: Option<&Position>,
limit: usize,
) -> Result<Batch, TransportError>;
fn cursor(
&self,
queue: &QueueName,
consumer: &ConsumerName,
) -> Result<Option<Position>, TransportError>;
fn commit(
&self,
queue: &QueueName,
consumer: &ConsumerName,
at: &Position,
) -> Result<(), TransportError>;
fn exclusive(
&self,
queue: &QueueName,
body: &mut dyn FnMut(&dyn Transport) -> Result<(), TransportError>,
) -> Result<(), TransportError>;
fn fingerprint(&self, queue: &QueueName) -> Result<Fingerprint, TransportError>;
fn wait_for_change(
&self,
queue: &QueueName,
since: &Fingerprint,
timeout: Duration,
) -> Result<Changed, TransportError>;
fn document(
&self,
queue: &QueueName,
name: &DocumentName,
) -> Result<Option<Vec<u8>>, TransportError>;
fn replace_document(
&self,
queue: &QueueName,
name: &DocumentName,
bytes: &[u8],
) -> Result<(), TransportError>;
}
#[derive(Debug, thiserror::Error)]
pub enum TransportError {
#[error("cannot {action} {}: {source}", path.display())]
Io {
action: &'static str,
path: PathBuf,
source: io::Error,
},
#[error("{queue}: a record is one non-empty line of bytes, and this one {why}")]
NotARecord {
queue: QueueName,
why: &'static str,
},
#[error("{queue}: position {position} is past the end of the queue, which ends at {end}; the log was replaced or truncated")]
PastEnd {
queue: QueueName,
position: Position,
end: Position,
},
#[error("{queue}: position {position} is not a record boundary")]
NotABoundary {
queue: QueueName,
position: Position,
},
#[error("the {transport} transport: {detail}")]
Backend {
transport: String,
detail: String,
},
#[error("{why}")]
Config {
kind: String,
why: String,
},
#[error("{kind:?} is not a transport kind this build can open; the kinds there are: {known}. A plugin serves a kind as an executable named onemessagebus-transport-<kind> on PATH")]
UnknownKind {
kind: String,
known: String,
},
}
impl TransportError {
fn io(action: &'static str, path: &Path, source: io::Error) -> Self {
Self::Io {
action,
path: path.to_path_buf(),
source,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{text:?} is not a {what} name: {why}")]
pub struct NameError {
pub what: &'static str,
pub text: String,
pub why: String,
}
fn check_name(what: &'static str, text: &str, dots: bool) -> Result<(), NameError> {
let refuse = |why: String| NameError {
what,
text: text.to_owned(),
why,
};
if text.is_empty() {
return Err(refuse("it is empty".to_owned()));
}
if let Some(ch) = text
.chars()
.find(|ch| !(ch.is_ascii_alphanumeric() || *ch == '-' || *ch == '_' || dots && *ch == '.'))
{
let allowed = if dots {
"ASCII letters, digits, `-`, `_` and `.`"
} else {
"ASCII letters, digits, `-` and `_`"
};
return Err(refuse(format!("{ch:?} is not one of {allowed}")));
}
if !text.starts_with(|ch: char| ch.is_ascii_alphanumeric()) {
return Err(refuse(
"it does not start with a letter or digit".to_owned(),
));
}
Ok(())
}
macro_rules! name_type {
($(#[$doc:meta])* $name:ident, $what:literal, $check:expr) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
#[schemars(transparent)]
pub struct $name(String);
impl $name {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for $name {
type Err = NameError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let check: fn(&str) -> Result<(), NameError> = $check;
check(text).map(|()| Self(text.to_owned()))
}
}
impl TryFrom<&str> for $name {
type Error = NameError;
fn try_from(text: &str) -> Result<Self, Self::Error> {
text.parse()
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for $name {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
};
}
name_type!(
QueueName,
"queue",
|text| check_name("queue", text, false)
);
name_type!(
ConsumerName,
"consumer",
|text| check_name("consumer", text, false)
);
name_type!(
DocumentName,
"document",
|text| {
check_name("document", text, true)?;
let reserved = [".jsonl", ".torn", ".staging", ".lock"]
.iter()
.find(|suffix| text.ends_with(*suffix))
.map(|suffix| format!("it ends in `{suffix}`, which a queue's own files are kept under"))
.or_else(|| {
text.contains("-cursor.")
.then(|| "it holds `-cursor.`, which a consumer's cursor file is named with".to_owned())
});
match reserved {
Some(why) => Err(NameError {
what: "document",
text: text.to_owned(),
why,
}),
None => Ok(()),
}
}
);
impl ConsumerName {
pub const DEFAULT: &'static str = "default";
#[must_use]
pub fn default_consumer() -> Self {
Self(Self::DEFAULT.to_owned())
}
#[must_use]
pub fn is_default(&self) -> bool {
self.0 == Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct Position(u64);
impl Position {
#[must_use]
pub const fn from_token(token: u64) -> Self {
Self(token)
}
#[must_use]
pub const fn token(self) -> u64 {
self.0
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct Fingerprint(Vec<u64>);
impl Fingerprint {
#[must_use]
pub fn from_parts(parts: impl IntoIterator<Item = u64>) -> Self {
Self(parts.into_iter().collect())
}
#[must_use]
pub fn parts(&self) -> &[u64] {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stored {
pub bytes: Vec<u8>,
pub after: Position,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TornRecord {
pub at: Position,
pub bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Batch {
pub records: Vec<Stored>,
pub torn: Option<TornRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Changed {
Moved(Fingerprint),
Unchanged(Fingerprint),
}
const CHANGE_POLL: Duration = Duration::from_millis(20);
const UNBOUNDED_WAIT: Duration = Duration::from_secs(3600);
fn unpoisoned<'a, T>(
guard: Result<MutexGuard<'a, T>, PoisonError<MutexGuard<'a, T>>>,
) -> MutexGuard<'a, T> {
guard.unwrap_or_else(PoisonError::into_inner)
}
#[derive(Debug, Clone)]
pub struct LocalTransport {
dir: PathBuf,
}
type Span = (usize, usize);
static STAGING: AtomicU64 = AtomicU64::new(0);
impl LocalTransport {
pub fn open(dir: impl Into<PathBuf>) -> Result<Self, TransportError> {
let dir = dir.into();
fs::create_dir_all(&dir).map_err(|failure| TransportError::io("create", &dir, failure))?;
Ok(Self { dir })
}
#[must_use]
pub fn dir(&self) -> &Path {
&self.dir
}
#[must_use]
pub fn records_path(&self, queue: &QueueName) -> PathBuf {
self.dir.join(format!("{queue}.jsonl"))
}
#[must_use]
pub fn cursor_path(&self, queue: &QueueName, consumer: &ConsumerName) -> PathBuf {
if consumer.is_default() {
self.dir.join(format!("{queue}-cursor.json"))
} else {
self.dir.join(format!("{queue}-cursor.{consumer}.json"))
}
}
fn lock_path(&self, queue: &QueueName) -> PathBuf {
self.dir.join(".lock").join(format!("{queue}.lock"))
}
fn lock(&self, queue: &QueueName) -> Result<File, TransportError> {
let path = self.lock_path(queue);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|failure| TransportError::io("create", parent, failure))?;
}
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&path)
.map_err(|failure| TransportError::io("open", &path, failure))?;
file.lock()
.map_err(|failure| TransportError::io("lock", &path, failure))?;
Ok(file)
}
fn append_locked(&self, queue: &QueueName, record: &[u8]) -> Result<Position, TransportError> {
if record.is_empty() || record.iter().all(u8::is_ascii_whitespace) {
return Err(TransportError::NotARecord {
queue: queue.clone(),
why: "is empty",
});
}
if record.contains(&b'\n') {
return Err(TransportError::NotARecord {
queue: queue.clone(),
why: "holds a newline",
});
}
let path = self.records_path(queue);
let mut file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)
.map_err(|failure| TransportError::io("open", &path, failure))?;
self.heal(queue, &path, &mut file)?;
let boundary = file
.seek(SeekFrom::End(0))
.map_err(|failure| TransportError::io("seek", &path, failure))?;
let mut line = Vec::with_capacity(record.len() + 1);
line.extend_from_slice(record);
line.push(b'\n');
if let Err(failure) = file.write_all(&line).and_then(|()| file.flush()) {
let _ = file.set_len(boundary);
return Err(TransportError::io("append to", &path, failure));
}
Ok(Position(boundary + line.len() as u64))
}
fn heal(&self, queue: &QueueName, path: &Path, file: &mut File) -> Result<(), TransportError> {
let bytes = read_all(path, file, 0)?;
let Some(last) = bytes.last() else {
return Ok(());
};
if *last == b'\n' {
return Ok(());
}
let boundary = bytes
.iter()
.rposition(|byte| *byte == b'\n')
.map_or(0, |at| at + 1);
file.set_len(boundary as u64)
.map_err(|failure| TransportError::io("truncate", path, failure))?;
let loss = serde_json::json!({
"at": crate::clock::now_rfc3339(),
"offset": boundary,
"bytes": bytes.len() - boundary,
"healed_by": std::process::id(),
});
let torn_log = self.dir.join(format!("{queue}.jsonl.torn"));
let _ = OpenOptions::new()
.create(true)
.append(true)
.open(&torn_log)
.and_then(|mut log| log.write_all(format!("{loss}\n").as_bytes()));
Ok(())
}
fn scan(bytes: &[u8]) -> (Vec<Span>, Option<Span>) {
let mut lines = Vec::new();
let mut start = 0;
while start < bytes.len() {
match bytes[start..].iter().position(|byte| *byte == b'\n') {
Some(offset) => {
let end = start + offset;
if !bytes[start..end].iter().all(u8::is_ascii_whitespace) {
lines.push((start, end));
}
start = end + 1;
}
None => return (lines, Some((start, bytes.len() - start))),
}
}
(lines, None)
}
fn file_bytes(&self, queue: &QueueName) -> Result<Vec<u8>, TransportError> {
let path = self.records_path(queue);
match fs::read(&path) {
Ok(bytes) => Ok(bytes),
Err(failure) if failure.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(failure) => Err(TransportError::io("read", &path, failure)),
}
}
fn write_atomic(&self, path: &Path, bytes: &[u8]) -> Result<(), TransportError> {
let mut staging = path.as_os_str().to_owned();
staging.push(format!(
".{}.{}.staging",
std::process::id(),
STAGING.fetch_add(1, Ordering::Relaxed)
));
let staging = PathBuf::from(staging);
fs::write(&staging, bytes)
.map_err(|failure| TransportError::io("write", &staging, failure))?;
fs::rename(&staging, path).map_err(|failure| {
let _ = fs::remove_file(&staging);
TransportError::io("rename onto", path, failure)
})
}
fn mark(path: &Path, parts: &mut Vec<u64>) {
match fs::metadata(path) {
Ok(metadata) => {
let modified = metadata
.modified()
.ok()
.and_then(|at| at.duration_since(UNIX_EPOCH).ok())
.unwrap_or_default();
parts.extend([
1,
metadata.len(),
modified.as_secs(),
u64::from(modified.subsec_nanos()),
]);
}
Err(_) => parts.push(0),
}
}
fn read_in(
&self,
queue: &QueueName,
from: Option<&Position>,
limit: usize,
) -> Result<Batch, TransportError> {
let bytes = self.file_bytes(queue)?;
let from = from.map_or(0, |position| position.0);
let end = bytes.len() as u64;
if from > end {
return Err(TransportError::PastEnd {
queue: queue.clone(),
position: Position(from),
end: Position(end),
});
}
let from = usize::try_from(from).unwrap_or(usize::MAX);
if from > 0 && bytes[from - 1] != b'\n' {
return Err(TransportError::NotABoundary {
queue: queue.clone(),
position: Position(from as u64),
});
}
let (lines, torn) = Self::scan(&bytes[from..]);
let mut batch = Batch::default();
for (start, stop) in lines.iter().take(limit) {
batch.records.push(Stored {
bytes: bytes[from + start..from + stop].to_vec(),
after: Position((from + stop + 1) as u64),
});
}
if lines.len() <= limit {
batch.torn = torn.map(|(start, length)| TornRecord {
at: Position((from + start) as u64),
bytes: length as u64,
});
}
Ok(batch)
}
}
fn read_all(path: &Path, file: &mut File, from: u64) -> Result<Vec<u8>, TransportError> {
file.seek(SeekFrom::Start(from))
.map_err(|failure| TransportError::io("seek", path, failure))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|failure| TransportError::io("read", path, failure))?;
Ok(bytes)
}
impl Transport for LocalTransport {
fn append(&self, queue: &QueueName, record: &[u8]) -> Result<Position, TransportError> {
let _lock = self.lock(queue)?;
self.append_locked(queue, record)
}
fn read(
&self,
queue: &QueueName,
from: Option<&Position>,
limit: usize,
) -> Result<Batch, TransportError> {
self.read_in(queue, from, limit)
}
fn cursor(
&self,
queue: &QueueName,
consumer: &ConsumerName,
) -> Result<Option<Position>, TransportError> {
let path = self.cursor_path(queue, consumer);
let text = match fs::read_to_string(&path) {
Ok(text) => text,
Err(failure) if failure.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(failure) => return Err(TransportError::io("read", &path, failure)),
};
let Ok(count) = serde_json::from_str::<u64>(text.trim()) else {
return Ok(None);
};
let bytes = self.file_bytes(queue)?;
let (lines, _) = Self::scan(&bytes);
let count = usize::try_from(count).unwrap_or(usize::MAX);
let offset = match count {
0 => 0,
n => lines
.get(n - 1)
.or(lines.last())
.map_or(0, |(_, end)| end + 1),
};
Ok(Some(Position(offset as u64)))
}
fn commit(
&self,
queue: &QueueName,
consumer: &ConsumerName,
at: &Position,
) -> Result<(), TransportError> {
let bytes = self.file_bytes(queue)?;
let end = bytes.len() as u64;
if at.0 > end {
return Err(TransportError::PastEnd {
queue: queue.clone(),
position: *at,
end: Position(end),
});
}
let offset = usize::try_from(at.0).unwrap_or(usize::MAX);
if offset > 0 && bytes[offset - 1] != b'\n' {
return Err(TransportError::NotABoundary {
queue: queue.clone(),
position: *at,
});
}
let (lines, _) = Self::scan(&bytes);
let count = lines.iter().filter(|(_, stop)| *stop < offset).count();
let path = self.cursor_path(queue, consumer);
let text = serde_json::to_string_pretty(&count).unwrap_or_else(|_| count.to_string());
self.write_atomic(&path, text.as_bytes())
}
fn exclusive(
&self,
queue: &QueueName,
body: &mut dyn FnMut(&dyn Transport) -> Result<(), TransportError>,
) -> Result<(), TransportError> {
let lock = self.lock(queue)?;
let path = self.records_path(queue);
if path.exists() {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.map_err(|failure| TransportError::io("open", &path, failure))?;
self.heal(queue, &path, &mut file)?;
}
let held = LocalHeld {
local: self.clone(),
held: BTreeSet::from([queue.clone()]),
};
let result = body(&held);
drop(lock);
result
}
fn fingerprint(&self, queue: &QueueName) -> Result<Fingerprint, TransportError> {
let mut parts = Vec::new();
Self::mark(&self.records_path(queue), &mut parts);
let prefix = format!("{queue}-cursor.");
let mut cursors: Vec<PathBuf> = match fs::read_dir(&self.dir) {
Ok(entries) => entries
.filter_map(Result::ok)
.filter(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
name.starts_with(&prefix) && name.ends_with(".json")
})
.map(|entry| entry.path())
.collect(),
Err(failure) => return Err(TransportError::io("list", &self.dir, failure)),
};
cursors.sort();
for cursor in cursors {
Self::mark(&cursor, &mut parts);
}
Ok(Fingerprint(parts))
}
fn wait_for_change(
&self,
queue: &QueueName,
since: &Fingerprint,
timeout: Duration,
) -> Result<Changed, TransportError> {
poll_for_change(self, queue, since, timeout)
}
fn document(
&self,
_queue: &QueueName,
name: &DocumentName,
) -> Result<Option<Vec<u8>>, TransportError> {
let path = self.dir.join(name.as_str());
match fs::read(&path) {
Ok(bytes) => Ok(Some(bytes)),
Err(failure) if failure.kind() == io::ErrorKind::NotFound => Ok(None),
Err(failure) => Err(TransportError::io("read", &path, failure)),
}
}
fn replace_document(
&self,
_queue: &QueueName,
name: &DocumentName,
bytes: &[u8],
) -> Result<(), TransportError> {
self.write_atomic(&self.dir.join(name.as_str()), bytes)
}
}
pub fn poll_for_change(
transport: &dyn Transport,
queue: &QueueName,
since: &Fingerprint,
timeout: Duration,
) -> Result<Changed, TransportError> {
let deadline = Instant::now().checked_add(timeout);
loop {
let now = transport.fingerprint(queue)?;
if &now != since {
return Ok(Changed::Moved(now));
}
let left = deadline.map_or(CHANGE_POLL, |deadline| {
deadline.saturating_duration_since(Instant::now())
});
if left.is_zero() {
return Ok(Changed::Unchanged(now));
}
std::thread::sleep(left.min(CHANGE_POLL));
}
}
struct LocalHeld {
local: LocalTransport,
held: BTreeSet<QueueName>,
}
impl Transport for LocalHeld {
fn append(&self, queue: &QueueName, record: &[u8]) -> Result<Position, TransportError> {
if self.held.contains(queue) {
self.local.append_locked(queue, record)
} else {
self.local.append(queue, record)
}
}
fn read(
&self,
queue: &QueueName,
from: Option<&Position>,
limit: usize,
) -> Result<Batch, TransportError> {
self.local.read_in(queue, from, limit)
}
fn cursor(
&self,
queue: &QueueName,
consumer: &ConsumerName,
) -> Result<Option<Position>, TransportError> {
self.local.cursor(queue, consumer)
}
fn commit(
&self,
queue: &QueueName,
consumer: &ConsumerName,
at: &Position,
) -> Result<(), TransportError> {
self.local.commit(queue, consumer, at)
}
fn exclusive(
&self,
queue: &QueueName,
body: &mut dyn FnMut(&dyn Transport) -> Result<(), TransportError>,
) -> Result<(), TransportError> {
if self.held.contains(queue) {
return body(self);
}
let lock = self.local.lock(queue)?;
let mut held = self.held.clone();
held.insert(queue.clone());
let nested = LocalHeld {
local: self.local.clone(),
held,
};
let result = body(&nested);
drop(lock);
result
}
fn fingerprint(&self, queue: &QueueName) -> Result<Fingerprint, TransportError> {
self.local.fingerprint(queue)
}
fn wait_for_change(
&self,
queue: &QueueName,
since: &Fingerprint,
timeout: Duration,
) -> Result<Changed, TransportError> {
poll_for_change(self, queue, since, timeout)
}
fn document(
&self,
queue: &QueueName,
name: &DocumentName,
) -> Result<Option<Vec<u8>>, TransportError> {
self.local.document(queue, name)
}
fn replace_document(
&self,
queue: &QueueName,
name: &DocumentName,
bytes: &[u8],
) -> Result<(), TransportError> {
self.local.replace_document(queue, name, bytes)
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryTransport {
inner: Arc<MemoryInner>,
}
#[derive(Debug, Default)]
struct MemoryInner {
state: Mutex<MemoryState>,
changed: Condvar,
holders: AtomicU64,
}
#[derive(Debug, Default)]
struct MemoryState {
records: BTreeMap<QueueName, Vec<Vec<u8>>>,
cursors: BTreeMap<(QueueName, ConsumerName), u64>,
documents: BTreeMap<DocumentName, Vec<u8>>,
generations: BTreeMap<QueueName, u64>,
held: BTreeMap<QueueName, u64>,
}
impl MemoryTransport {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn state_for(&self, queue: &QueueName, holder: Option<u64>) -> MutexGuard<'_, MemoryState> {
let mut state = unpoisoned(self.inner.state.lock());
while let Some(owner) = state.held.get(queue) {
if Some(*owner) == holder {
break;
}
state = unpoisoned(self.inner.changed.wait(state));
}
state
}
fn bump(&self, state: &mut MemoryState, queue: &QueueName) {
*state.generations.entry(queue.clone()).or_default() += 1;
self.inner.changed.notify_all();
}
fn append_as(
&self,
holder: Option<u64>,
queue: &QueueName,
record: &[u8],
) -> Result<Position, TransportError> {
if record.is_empty() || record.iter().all(u8::is_ascii_whitespace) {
return Err(TransportError::NotARecord {
queue: queue.clone(),
why: "is empty",
});
}
if record.contains(&b'\n') {
return Err(TransportError::NotARecord {
queue: queue.clone(),
why: "holds a newline",
});
}
let mut state = self.state_for(queue, holder);
let records = state.records.entry(queue.clone()).or_default();
records.push(record.to_vec());
let after = Position(records.len() as u64);
self.bump(&mut state, queue);
Ok(after)
}
fn read_as(
&self,
queue: &QueueName,
from: Option<&Position>,
limit: usize,
) -> Result<Batch, TransportError> {
let state = unpoisoned(self.inner.state.lock());
let records = state.records.get(queue).map_or(&[][..], Vec::as_slice);
let from = from.map_or(0, |position| position.0);
let end = records.len() as u64;
if from > end {
return Err(TransportError::PastEnd {
queue: queue.clone(),
position: Position(from),
end: Position(end),
});
}
let start = usize::try_from(from).unwrap_or(usize::MAX);
Ok(Batch {
records: records[start..]
.iter()
.take(limit)
.enumerate()
.map(|(offset, bytes)| Stored {
bytes: bytes.clone(),
after: Position((start + offset + 1) as u64),
})
.collect(),
torn: None,
})
}
fn commit_as(
&self,
holder: Option<u64>,
queue: &QueueName,
consumer: &ConsumerName,
at: &Position,
) -> Result<(), TransportError> {
let mut state = self.state_for(queue, holder);
let end = state.records.get(queue).map_or(0, Vec::len) as u64;
if at.0 > end {
return Err(TransportError::PastEnd {
queue: queue.clone(),
position: *at,
end: Position(end),
});
}
state
.cursors
.insert((queue.clone(), consumer.clone()), at.0);
self.bump(&mut state, queue);
Ok(())
}
fn exclusive_as(
&self,
holding: &BTreeSet<QueueName>,
holder: u64,
queue: &QueueName,
body: &mut dyn FnMut(&dyn Transport) -> Result<(), TransportError>,
) -> Result<(), TransportError> {
if holding.contains(queue) {
let held = MemoryHeld {
memory: self.clone(),
holder,
holding: holding.clone(),
};
return body(&held);
}
{
let mut state = self.state_for(queue, None);
state.held.insert(queue.clone(), holder);
}
let mut holding = holding.clone();
holding.insert(queue.clone());
let held = MemoryHeld {
memory: self.clone(),
holder,
holding,
};
let result = body(&held);
let mut state = unpoisoned(self.inner.state.lock());
state.held.remove(queue);
self.inner.changed.notify_all();
result
}
fn replace_as(
&self,
holder: Option<u64>,
queue: &QueueName,
name: &DocumentName,
bytes: &[u8],
) -> Result<(), TransportError> {
let mut state = self.state_for(queue, holder);
state.documents.insert(name.clone(), bytes.to_vec());
Ok(())
}
fn fingerprint_of(&self, queue: &QueueName) -> Fingerprint {
let state = unpoisoned(self.inner.state.lock());
Fingerprint(vec![state.generations.get(queue).copied().unwrap_or(0)])
}
}
impl Transport for MemoryTransport {
fn append(&self, queue: &QueueName, record: &[u8]) -> Result<Position, TransportError> {
self.append_as(None, queue, record)
}
fn read(
&self,
queue: &QueueName,
from: Option<&Position>,
limit: usize,
) -> Result<Batch, TransportError> {
self.read_as(queue, from, limit)
}
fn cursor(
&self,
queue: &QueueName,
consumer: &ConsumerName,
) -> Result<Option<Position>, TransportError> {
let state = unpoisoned(self.inner.state.lock());
Ok(state
.cursors
.get(&(queue.clone(), consumer.clone()))
.map(|token| Position(*token)))
}
fn commit(
&self,
queue: &QueueName,
consumer: &ConsumerName,
at: &Position,
) -> Result<(), TransportError> {
self.commit_as(None, queue, consumer, at)
}
fn exclusive(
&self,
queue: &QueueName,
body: &mut dyn FnMut(&dyn Transport) -> Result<(), TransportError>,
) -> Result<(), TransportError> {
let holder = self.inner.holders.fetch_add(1, Ordering::Relaxed) + 1;
self.exclusive_as(&BTreeSet::new(), holder, queue, body)
}
fn fingerprint(&self, queue: &QueueName) -> Result<Fingerprint, TransportError> {
Ok(self.fingerprint_of(queue))
}
fn wait_for_change(
&self,
queue: &QueueName,
since: &Fingerprint,
timeout: Duration,
) -> Result<Changed, TransportError> {
let deadline = Instant::now().checked_add(timeout);
let mut state = unpoisoned(self.inner.state.lock());
loop {
let now = Fingerprint(vec![state.generations.get(queue).copied().unwrap_or(0)]);
if &now != since {
return Ok(Changed::Moved(now));
}
let left = deadline.map_or(UNBOUNDED_WAIT, |deadline| {
deadline.saturating_duration_since(Instant::now())
});
if left.is_zero() {
return Ok(Changed::Unchanged(now));
}
state = self
.inner
.changed
.wait_timeout(state, left)
.unwrap_or_else(PoisonError::into_inner)
.0;
}
}
fn document(
&self,
_queue: &QueueName,
name: &DocumentName,
) -> Result<Option<Vec<u8>>, TransportError> {
let state = unpoisoned(self.inner.state.lock());
Ok(state.documents.get(name).cloned())
}
fn replace_document(
&self,
queue: &QueueName,
name: &DocumentName,
bytes: &[u8],
) -> Result<(), TransportError> {
self.replace_as(None, queue, name, bytes)
}
}
struct MemoryHeld {
memory: MemoryTransport,
holder: u64,
holding: BTreeSet<QueueName>,
}
impl MemoryHeld {
fn holder_for(&self, queue: &QueueName) -> Option<u64> {
self.holding.contains(queue).then_some(self.holder)
}
}
impl Transport for MemoryHeld {
fn append(&self, queue: &QueueName, record: &[u8]) -> Result<Position, TransportError> {
self.memory.append_as(self.holder_for(queue), queue, record)
}
fn read(
&self,
queue: &QueueName,
from: Option<&Position>,
limit: usize,
) -> Result<Batch, TransportError> {
self.memory.read_as(queue, from, limit)
}
fn cursor(
&self,
queue: &QueueName,
consumer: &ConsumerName,
) -> Result<Option<Position>, TransportError> {
self.memory.cursor(queue, consumer)
}
fn commit(
&self,
queue: &QueueName,
consumer: &ConsumerName,
at: &Position,
) -> Result<(), TransportError> {
self.memory
.commit_as(self.holder_for(queue), queue, consumer, at)
}
fn exclusive(
&self,
queue: &QueueName,
body: &mut dyn FnMut(&dyn Transport) -> Result<(), TransportError>,
) -> Result<(), TransportError> {
self.memory
.exclusive_as(&self.holding, self.holder, queue, body)
}
fn fingerprint(&self, queue: &QueueName) -> Result<Fingerprint, TransportError> {
self.memory.fingerprint(queue)
}
fn wait_for_change(
&self,
queue: &QueueName,
since: &Fingerprint,
timeout: Duration,
) -> Result<Changed, TransportError> {
self.memory.wait_for_change(queue, since, timeout)
}
fn document(
&self,
queue: &QueueName,
name: &DocumentName,
) -> Result<Option<Vec<u8>>, TransportError> {
self.memory.document(queue, name)
}
fn replace_document(
&self,
queue: &QueueName,
name: &DocumentName,
bytes: &[u8],
) -> Result<(), TransportError> {
self.memory
.replace_as(self.holder_for(queue), queue, name, bytes)
}
}