use std::collections::BTreeSet;
use mkit_core::hash::Hash;
use serde::{Deserialize, Serialize};
use super::outbox::{OutboxBuilder, guard};
use super::{
BlobKey, Key, NamespaceStore, Partition, Precondition, StoreError, Value, Write, codec, keys,
};
use crate::pipeline::ShardMap;
use crate::repo::{RepoId, RepoName};
pub const MAX_UNPUBLISHED_ADVANCES: u64 = 64;
pub const MAX_ADVANCE_ITEMS: usize = 4096;
pub const RECHECK_MS: u64 = 5_000;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Pair {
pub head: Option<Hash>,
pub packmap: Option<Hash>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Clearance {
Pending,
Cleared,
Held,
Hit,
Resolved,
}
impl Clearance {
#[must_use]
pub fn publishable(self) -> bool {
matches!(self, Self::Cleared | Self::Resolved)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Obligation {
pub id: Hash,
pub state: Clearance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Advance {
pub sequence: u64,
pub generation: u64,
pub value: Pair,
pub additions: Vec<Hash>,
pub dependencies: Vec<Hash>,
pub external_bases: Vec<Hash>,
pub obligations: Vec<Obligation>,
pub state: Clearance,
pub operation: Hash,
}
impl Advance {
fn validate(&self) -> Result<(), StoreError> {
if self.sequence == 0
|| self.additions.len() > super::outbox::MAX_TICKETS_PER_ADVANCE
|| self.dependencies.len() > MAX_ADVANCE_ITEMS
|| self.external_bases.len() > MAX_ADVANCE_ITEMS
|| self.obligations.len() > MAX_ADVANCE_ITEMS
|| !unique(&self.additions)
|| !unique(&self.dependencies)
|| !unique(&self.external_bases)
{
return Err(StoreError::Invalid("invalid publication advance".into()));
}
let ids: BTreeSet<_> = self.obligations.iter().map(|o| o.id).collect();
if ids.len() != self.obligations.len()
|| self.state.publishable() && self.obligations.iter().any(|o| !o.state.publishable())
{
return Err(StoreError::Invalid(
"invalid publication obligations".into(),
));
}
Ok(())
}
pub fn encode(&self) -> Result<Value, StoreError> {
self.validate()?;
encode(self)
}
pub fn decode(value: &Value) -> Result<Self, StoreError> {
let row: Self = decode(value)?;
row.validate()
.map_err(|e| StoreError::Corrupt(e.to_string().into()))?;
Ok(row)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Publication {
pub sequence: u64,
pub published: u64,
pub boundary: u64,
pub generation: u64,
pub value: Pair,
}
impl Publication {
pub fn decode(value: Option<&Value>) -> Result<Self, StoreError> {
let row: Self = value.map(decode).transpose()?.unwrap_or_default();
if row.boundary > row.published || row.published > row.sequence {
return Err(StoreError::Corrupt("invalid publication prefix".into()));
}
Ok(row)
}
pub fn encode(&self) -> Result<Value, StoreError> {
let value = encode(self)?;
Self::decode(Some(&value))?;
Ok(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Witness {
pub generation: u64,
pub sequence: u64,
pub published: bool,
pub held: bool,
}
impl Witness {
#[must_use]
pub fn encode(self) -> Value {
let mut bytes = vec![1, u8::from(self.published), u8::from(self.held)];
bytes.extend_from_slice(&self.generation.to_be_bytes());
bytes.extend_from_slice(&self.sequence.to_be_bytes());
Value::new(bytes)
}
pub fn decode(value: &Value) -> Result<Self, StoreError> {
let bytes = value.as_bytes();
if bytes.is_empty() {
return Ok(Self {
generation: 0,
sequence: 0,
published: true,
held: false,
});
}
if bytes.len() != 19 || bytes[0] != 1 || bytes[1] > 1 || bytes[2] > 1 {
return Err(StoreError::Corrupt("invalid clearance witness".into()));
}
let number = |offset| -> Result<u64, StoreError> {
bytes
.get(offset..offset + 8)
.and_then(|b| b.try_into().ok())
.map(u64::from_be_bytes)
.ok_or_else(|| StoreError::Corrupt("short clearance witness".into()))
};
Ok(Self {
generation: number(3)?,
sequence: number(11)?,
published: bytes[1] != 0,
held: bytes[2] != 0,
})
}
#[must_use]
pub fn visible(self, writer: bool, generation: u64) -> bool {
!self.held && self.generation == generation && (writer || self.published)
}
}
fn unique(ids: &[Hash]) -> bool {
ids.iter().collect::<BTreeSet<_>>().len() == ids.len()
}
fn encode<T: Serialize>(row: &T) -> Result<Value, StoreError> {
let mut bytes = vec![1];
bytes.extend(
serde_json::to_vec(row).map_err(|_| StoreError::Invalid("publication encoding".into()))?,
);
if bytes.len() > super::MAX_VALUE_BYTES {
return Err(StoreError::Invalid(
"publication value exceeds limit".into(),
));
}
Ok(Value::new(bytes))
}
fn decode<T: serde::de::DeserializeOwned>(value: &Value) -> Result<T, StoreError> {
let bytes = value.as_bytes();
if bytes.first() != Some(&1) || bytes.len() > super::MAX_VALUE_BYTES {
return Err(StoreError::Corrupt(
"invalid publication version or size".into(),
));
}
serde_json::from_slice(&bytes[1..])
.map_err(|_| StoreError::Corrupt("invalid publication record".into()))
}
#[must_use]
pub fn sequence_ref(name: &str) -> String {
mkit_attest::grant::packmap_head(name).unwrap_or_else(|| name.to_owned())
}
#[must_use]
pub fn value_refs(name: &str, value: &Pair) -> Vec<(String, Option<Hash>)> {
let mut refs = vec![(name.to_owned(), value.head)];
if let Some(packmap) = mkit_attest::grant::head_packmap(name) {
refs.push((packmap, value.packmap));
}
refs
}
#[allow(clippy::too_many_arguments)]
pub fn append(
repo: &RepoId,
name: &str,
source: &Partition,
shards: &dyn ShardMap,
prior: Option<&Value>,
mut advance: Advance,
deleted: bool,
pre: &mut Vec<Precondition>,
writes: &mut Vec<Write>,
outbox: &mut OutboxBuilder,
) -> Result<Publication, StoreError> {
let name = sequence_ref(name);
let mut state = Publication::decode(prior)?;
if !deleted && state.sequence - state.published >= MAX_UNPUBLISHED_ADVANCES {
return Err(StoreError::unavailable("publication backlog full"));
}
state.sequence = state
.sequence
.checked_add(1)
.ok_or_else(|| StoreError::Corrupt("advance sequence overflow".into()))?;
advance.sequence = state.sequence;
advance.generation = state.generation;
advance.validate()?;
if !advance.state.publishable() {
writes.push(Write::Put(
keys::timer(
0,
crate::timers::registry::kinds::PUBLICATION_RECHECK.get(),
keys::advance(&repo.name, &name, advance.sequence).as_bytes(),
),
crate::timers::publication_recheck::initial_value(),
));
}
let key = keys::publication(&repo.name, &name);
pre.push(guard(key.clone(), prior));
if deleted {
state.boundary = state.sequence;
state.published = state.sequence;
state.value = Pair {
head: advance.value.head.and(state.value.head),
packmap: advance.value.packmap.and(state.value.packmap),
};
project_refs(repo, &name, source, shards, &state.value, writes, outbox);
} else if advance.state.publishable() && state.published + 1 == state.sequence {
state.published = state.sequence;
state.value = advance.value.clone();
project_refs(repo, &name, source, shards, &state.value, writes, outbox);
}
if advance.sequence > state.published || !advance.obligations.is_empty() {
writes.push(Write::Put(
keys::advance(&repo.name, &name, advance.sequence),
advance.encode()?,
));
}
project_members(repo, source, shards, &advance, writes, outbox);
writes.push(Write::Put(key, state.encode()?));
Ok(state)
}
fn project_refs(
repo: &RepoId,
name: &str,
source: &Partition,
shards: &dyn ShardMap,
value: &Pair,
writes: &mut Vec<Write>,
outbox: &mut OutboxBuilder,
) {
for (name, id) in value_refs(name, value) {
let key = keys::published_ref(&repo.name, &name);
writes.push(id.map_or_else(
|| Write::Delete(key.clone()),
|id| Write::Put(key.clone(), codec::encode_ref_id(&id)),
));
let target = shards.ref_index(repo, &name);
if target != *source {
let key = keys::published_index(&repo.name, &name);
match id {
Some(id) => outbox.relay(&target, vec![(key, codec::encode_ref_id(&id))]),
None => outbox.relay_delete(&target, vec![key]),
}
}
}
}
fn project_members(
repo: &RepoId,
source: &Partition,
shards: &dyn ShardMap,
advance: &Advance,
writes: &mut Vec<Write>,
outbox: &mut OutboxBuilder,
) {
for pack in &advance.additions {
let witness = Witness {
generation: advance.generation,
sequence: advance.sequence,
published: advance.state.publishable(),
held: matches!(advance.state, Clearance::Held | Clearance::Hit),
};
let key = keys::membership(&repo.name, pack);
writes.retain(|w| !matches!(w, Write::Put(k, _) | Write::Delete(k) if k == &key));
writes.push(Write::Put(key.clone(), witness.encode()));
let target = shards.membership(repo, &BlobKey::pack(*pack));
if target != *source {
outbox.relay(&target, vec![(key, witness.encode())]);
if witness.published {
outbox.relay(
&target,
vec![(keys::published_member(&repo.name, pack), witness.encode())],
);
}
}
}
}
pub async fn read<S: NamespaceStore>(
store: &S,
source: &Partition,
repo: &RepoName,
name: &str,
) -> Result<Publication, StoreError> {
Publication::decode(
store
.get(source, &keys::publication(repo, &sequence_ref(name)))
.await?
.as_ref(),
)
}
pub async fn prefix<S: NamespaceStore>(
store: &S,
source: &Partition,
repo: &RepoName,
name: &str,
state: &Publication,
changed: &Advance,
) -> Result<(u64, Pair), StoreError> {
if state.sequence - state.published > MAX_UNPUBLISHED_ADVANCES {
return Err(StoreError::Corrupt(
"publication prefix exceeds bound".into(),
));
}
if state.published == state.sequence {
return Ok((state.published, state.value.clone()));
}
let name = sequence_ref(name);
let wanted: Vec<Key> = (state.published + 1..=state.sequence)
.map(|sequence| keys::advance(repo, &name, sequence))
.collect();
let rows = store.get_many(source, &wanted).await?;
if rows.len() != wanted.len() {
return Err(StoreError::Corrupt("short publication prefix read".into()));
}
let mut result = (state.published, state.value.clone());
for (sequence, raw) in (state.published + 1..=state.sequence).zip(rows) {
let current = if changed.sequence == sequence {
changed.clone()
} else {
Advance::decode(
raw.as_ref()
.ok_or_else(|| StoreError::Corrupt("missing retained advance".into()))?,
)?
};
if current.sequence != sequence || current.generation != state.generation {
return Err(StoreError::Corrupt(
"retained advance binding mismatch".into(),
));
}
if !current.state.publishable() {
break;
}
result = (sequence, current.value);
}
Ok(result)
}
#[allow(clippy::too_many_arguments)]
pub fn clear(
repo: &RepoId,
name: &str,
source: &Partition,
shards: &dyn ShardMap,
state_raw: &Value,
advance_raw: &Value,
changed: &Advance,
eligible: (u64, Pair),
pre: &mut Vec<Precondition>,
writes: &mut Vec<Write>,
outbox: &mut OutboxBuilder,
) -> Result<(), StoreError> {
let name = sequence_ref(name);
let mut state = Publication::decode(Some(state_raw))?;
let old = Advance::decode(advance_raw)?;
changed.validate()?;
if changed.sequence != old.sequence
|| changed.sequence > state.sequence
|| changed.generation != old.generation
|| changed.generation != state.generation
|| eligible.0 < state.published
|| eligible.0 > state.sequence
|| old.state == Clearance::Hit && changed.state == Clearance::Cleared
{
return Err(StoreError::Invalid(
"invalid publication clearance transition".into(),
));
}
let key = keys::advance(&repo.name, &name, changed.sequence);
pre.push(guard(key.clone(), Some(advance_raw)));
writes.push(Write::Put(key, changed.encode()?));
pre.push(guard(keys::publication(&repo.name, &name), Some(state_raw)));
project_members(repo, source, shards, changed, writes, outbox);
if eligible.0 > state.published {
state.published = eligible.0;
state.value = eligible.1;
project_refs(repo, &name, source, shards, &state.value, writes, outbox);
}
writes.push(Write::Put(
keys::publication(&repo.name, &name),
state.encode()?,
));
Ok(())
}
#[cfg(test)]
#[path = "publication_tests.rs"]
mod tests;