use polyc_eventlog::{BoundedReplay, Event};
use polyc_eventlog_host::{AppendError, EventLogHost};
use polyc_state::{journal::partition_incarnation_from_record, revision::PartitionIncarnation};
use super::{JournalError, PartitionJournal};
fn canonical_incarnation(
events: &[(u64, Event)],
) -> Result<Option<PartitionIncarnation>, JournalError> {
if events.is_empty() {
return Ok(None);
}
let mut observed = None;
for (_, event) in events {
let marker = partition_incarnation_from_record(&event.kind, &event.payload)
.map_err(|error| JournalError::Unreadable(error.to_string()))?;
if let Some(marker) = marker
&& observed.replace(marker).is_some()
{
return Err(JournalError::Unreadable(
"a physical journal carries exactly one incarnation marker".to_owned(),
));
}
}
observed.map_or_else(
|| {
Err(JournalError::Unreadable(
"a nonempty physical journal carries an incarnation marker".to_owned(),
))
},
|incarnation| Ok(Some(incarnation)),
)
}
pub(crate) fn classify_host_error(error: &AppendError) -> JournalError {
match error {
AppendError::Verify(_)
| AppendError::Log(_)
| AppendError::PayloadTooLarge { .. }
| AppendError::PartitionName(_) => JournalError::Unreadable(error.to_string()),
AppendError::Listing(listing) => match listing {
polyc_eventlog_host::ListPartitionsError::Storage(_) => {
JournalError::Unreachable(error.to_string())
}
polyc_eventlog_host::ListPartitionsError::Corrupt { .. }
| polyc_eventlog_host::ListPartitionsError::EntriesExceeded { .. }
| polyc_eventlog_host::ListPartitionsError::NameBytesExceeded { .. } => {
JournalError::Unreadable(error.to_string())
}
},
AppendError::Closed
| AppendError::AppendOutcomeUnknown(_)
| AppendError::AttestationOutcomeUnknown(_) => JournalError::Unreachable(error.to_string()),
}
}
#[async_trait::async_trait]
impl PartitionJournal for EventLogHost {
async fn partition_incarnation(
&self,
partition: String,
) -> Result<Option<PartitionIncarnation>, JournalError> {
let events = Self::replay_with_positions(self, partition)
.await
.map_err(|error| classify_host_error(&error))?;
canonical_incarnation(&events)
}
async fn list_partitions(&self) -> Result<Vec<String>, JournalError> {
Self::list_partitions(self)
.await
.map_err(|e| classify_host_error(&e))
}
async fn partition_event_count(&self, partition: String) -> Result<u64, JournalError> {
Self::partition_event_count(self, partition)
.await
.map_err(|e| classify_host_error(&e))
}
async fn replay_with_positions(
&self,
partition: String,
) -> Result<Vec<(u64, Event)>, JournalError> {
Self::replay_with_positions(self, partition)
.await
.map_err(|e| classify_host_error(&e))
}
async fn replay_with_positions_bounded(
&self,
partition: String,
max_bytes: u64,
) -> Result<BoundedReplay, JournalError> {
Self::replay_with_positions_bounded(self, partition, max_bytes)
.await
.map_err(|e| classify_host_error(&e))
}
async fn replay_from_with_positions_bounded(
&self,
partition: String,
start: u64,
max_bytes: u64,
) -> Result<BoundedReplay, JournalError> {
Self::replay_from_with_positions_bounded(self, partition, start, max_bytes)
.await
.map_err(|e| classify_host_error(&e))
}
async fn replay_range_with_positions_bounded(
&self,
partition: String,
start: u64,
end: u64,
max_bytes: u64,
) -> Result<BoundedReplay, JournalError> {
Self::replay_range_with_positions_bounded(self, partition, start, end, max_bytes)
.await
.map_err(|e| classify_host_error(&e))
}
#[cfg(test)]
async fn append_batch(
&self,
partition: String,
events: Vec<Event>,
) -> Result<(), JournalError> {
Self::append_batch(self, partition, events)
.await
.map(|_| ())
.map_err(|e| classify_host_error(&e))
}
fn is_stopping(&self) -> bool {
Self::is_shutting_down(self)
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn over_host(
host: std::sync::Arc<EventLogHost>,
) -> std::sync::Arc<dyn PartitionJournal> {
host
}
#[cfg(test)]
mod tests {
use super::*;
use polyc_state::journal::{INCARNATION_MARKER_KIND, incarnation_marker_payload};
fn marker(byte: u8) -> Event {
Event::new(
INCARNATION_MARKER_KIND,
incarnation_marker_payload(PartitionIncarnation::from_bytes([byte; 32])),
)
}
#[test]
fn exact_source_read_refuses_missing_duplicate_or_malformed_markers() {
assert!(canonical_incarnation(&[(0, Event::new("record", vec![]))]).is_err());
assert!(canonical_incarnation(&[(0, marker(1)), (1, marker(2))]).is_err());
assert!(
canonical_incarnation(&[(0, Event::new(INCARNATION_MARKER_KIND, vec![1; 3]))]).is_err()
);
assert_eq!(canonical_incarnation(&[]).unwrap(), None);
assert_eq!(
canonical_incarnation(&[(0, marker(7)), (1, Event::new("record", vec![]))]).unwrap(),
Some(PartitionIncarnation::from_bytes([7; 32]))
);
}
#[test]
fn a_marker_the_state_plane_appended_at_the_tail_names_the_same_lineage() {
assert_eq!(
canonical_incarnation(&[(0, Event::new("record", vec![])), (1, marker(1))]).unwrap(),
Some(PartitionIncarnation::from_bytes([1; 32])),
"a tail marker is the shape the State plane's adoption leaves"
);
assert!(
canonical_incarnation(&[
(0, Event::new("record", vec![])),
(1, marker(1)),
(2, marker(2)),
])
.is_err(),
"two markers stay a refusal wherever they sit"
);
}
}