use std::collections::BTreeSet;
use futures::StreamExt as _;
use futures::future::join_all;
use mkit_core::hash::Hash;
#[cfg(feature = "ssh")]
use mkit_core::refs::PACKMAP_REF_PREFIX;
use mkit_core::refs::RefWriteCondition;
use super::plan::Snapshot;
#[cfg(feature = "ssh")]
use super::{Authenticated, StoredResult, UpdateRefResult};
use super::{
HookSet, OpKind, Operation, Pipeline, RefUpdate, ServerError, StorageOp, internal, meta_error,
store_error,
};
#[cfg(feature = "ssh")]
use crate::policy::WritePolicy;
use crate::repo::Addressing;
#[cfg(feature = "ssh")]
use crate::repo::NamespaceKey;
#[cfg(feature = "ssh")]
use crate::store::outbox::MAX_TICKETS_PER_ADVANCE;
use crate::store::{BlobBody, BlobKey, MultipartBlobStore, NamespaceStore, codec, keys, read};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PendingPack {
pub(crate) pack: Hash,
pub(crate) bytes: u64,
pub(crate) packlist: bool,
}
pub(crate) const IMPLICIT_PACKMAP_UNKNOWN: &str =
"packmap names packs not uploaded to this repository";
pub(crate) const MAX_IMPLICIT_PACKLIST_BYTES: u64 = 1024 * 1024;
pub(crate) const MAX_IMPLICIT_LISTED_PACKS: usize = 1024;
const IMPLICIT_CHECK_CONCURRENCY: usize = 6;
fn refuse() -> ServerError {
ServerError::failed_precondition(IMPLICIT_PACKMAP_UNKNOWN)
}
impl<B: MultipartBlobStore, N: NamespaceStore, H: HookSet> Pipeline<B, N, H> {
#[cfg(feature = "ssh")]
pub(crate) fn implicit_tickets(&self) -> bool {
matches!(self.cfg.auth, super::AuthMode::TransportIdentity)
&& match &self.cfg.addressing {
Addressing::Multi(_) => true,
Addressing::Single { repo } => {
repo.namespace != NamespaceKey::deployment_default()
&& self.cfg.write_policy == WritePolicy::Owner
}
}
}
#[cfg(feature = "ssh")]
pub(crate) async fn update_packmap_consuming(
&self,
a: &Authenticated,
upd: RefUpdate,
pending: &[PendingPack],
) -> Result<UpdateRefResult, ServerError> {
if !upd.name.starts_with(PACKMAP_REF_PREFIX)
|| upd.new.is_none()
|| pending.len() > MAX_TICKETS_PER_ADVANCE
|| !self.implicit_tickets()
{
return Err(internal("not an implicit packmap write"));
}
self.observe(a, async {
super::check_ref_name(&upd.name)?;
if upd.new.is_none() && !matches!(upd.condition, RefWriteCondition::Match(_)) {
return Err(ServerError::invalid_argument(
"delete requires MATCH and an empty new_id",
));
}
match self
.write_with(a, OpKind::UpdateRef(upd), Some(pending))
.await?
{
(StoredResult::UpdateRef(result), _) => Ok(result),
(other, _) => Err(super::stored_mismatch(&other)),
}
})
.await
}
pub(super) async fn check_implicit_packmap(
&self,
op: &Operation,
pending: &[PendingPack],
ahead: Option<&Snapshot>,
upd: &mut RefUpdate,
) -> Result<(), ServerError> {
if !matches!(op.kind, OpKind::UpdateRef(_)) {
return Err(internal("implicit consumption needs an UpdateRef"));
}
let node = upd
.new
.ok_or_else(|| internal("implicit packmap write deletes"))?;
let bytes = self.read_node(&node).await?;
let listed = mkit_core::transfer::decode_packlist(&bytes).map_err(|_| refuse())?;
if listed.packs.len() > MAX_IMPLICIT_LISTED_PACKS {
return Err(refuse());
}
if !self.pack_known(op, &upd.name, node, pending).await? {
return Err(refuse());
}
let current = match upd.condition {
RefWriteCondition::Match(expected) => Some(expected),
RefWriteCondition::Missing => None,
RefWriteCondition::Any => self.current_ref(op, &upd.name, ahead).await?,
};
if listed.prev.is_some() && listed.prev != current {
return Err(refuse());
}
if matches!(upd.condition, RefWriteCondition::Any) {
upd.condition = match current {
Some(id) => RefWriteCondition::Match(id),
None => RefWriteCondition::Missing,
};
}
let mut packs = listed.packs;
packs.sort_unstable();
packs.dedup();
if packs
.iter()
.any(|pack| pending.iter().any(|p| p.pack == *pack && p.packlist))
{
return Err(refuse());
}
for chunk in packs.chunks(IMPLICIT_CHECK_CONCURRENCY) {
let results = join_all(
chunk
.iter()
.map(|&pack| self.pack_known(op, &upd.name, pack, pending)),
)
.await;
for known in results {
if !known? {
return Err(refuse());
}
}
}
if matches!(self.cfg.addressing, Addressing::Multi(_)) {
for chunk in pending.chunks(IMPLICIT_CHECK_CONCURRENCY) {
let heads = join_all(chunk.iter().map(|p| {
let key = BlobKey::pack(p.pack);
async move { self.blobs.head(&key).await }
}))
.await;
for head in heads {
if head
.map_err(|e| store_error(StorageOp::BlobHead, e))?
.is_none()
{
return Err(refuse());
}
}
}
}
Ok(())
}
pub(super) async fn current_ref(
&self,
op: &Operation,
ref_name: &str,
ahead: Option<&Snapshot>,
) -> Result<Option<Hash>, ServerError> {
let key = keys::ref_key(&op.repo.name, ref_name);
if let Some(snap) = ahead.filter(|snap| snap.contains(&key)) {
return snap
.get(&key)
.map(codec::decode_ref_id)
.transpose()
.map_err(meta_error);
}
let p = self.shards.ref_shard(&op.repo, ref_name);
read::read_ref(&self.meta, &p, &op.repo.name, ref_name)
.await
.map_err(meta_error)
}
async fn read_node(&self, node: &Hash) -> Result<Vec<u8>, ServerError> {
let body = self
.blobs
.get(&BlobKey::pack(*node), None)
.await
.map_err(|e| store_error(StorageOp::BlobGet, e))?;
let Some(body) = body else {
return Err(refuse());
};
match body {
BlobBody::Bytes(bytes) => {
if bytes.len() as u64 > MAX_IMPLICIT_PACKLIST_BYTES {
return Err(refuse());
}
Ok(bytes.to_vec())
}
BlobBody::Stream { len, mut stream } => {
if len > MAX_IMPLICIT_PACKLIST_BYTES {
return Err(refuse());
}
let mut out = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| store_error(StorageOp::BlobGet, e))?;
if out.len() as u64 + chunk.len() as u64 > MAX_IMPLICIT_PACKLIST_BYTES {
return Err(refuse());
}
out.extend_from_slice(&chunk);
}
Ok(out)
}
}
}
async fn pack_known(
&self,
op: &Operation,
ref_name: &str,
pack: Hash,
pending: &[PendingPack],
) -> Result<bool, ServerError> {
if pending.iter().any(|p| p.pack == pack) {
return Ok(true);
}
match &self.cfg.addressing {
Addressing::Single { .. } => Ok(self
.blobs
.head(&BlobKey::pack(pack))
.await
.map_err(|e| store_error(StorageOp::BlobHead, e))?
.is_some()),
Addressing::Multi(_) => read::is_member(
&self.meta,
self.shards.as_ref(),
&op.repo,
&pack,
Some(ref_name),
)
.await
.map_err(meta_error),
}
}
}
pub(crate) fn implicit_packs(pending: &[PendingPack]) -> Vec<Hash> {
pending
.iter()
.map(|p| p.pack)
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
#[cfg(test)]
mod concurrency_tests {
use super::*;
use crate::{
BlobMeta, BlobStore, ByteRange, ManualClock, MemoryBlobStore, MemoryKv, NamespaceKey,
NoopMetrics, Principal, RepoId, RepoName, StoreError,
};
use futures::{FutureExt, executor::block_on};
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
};
struct PendingMembership {
node: Vec<u8>,
release: AtomicBool,
active: AtomicUsize,
peak: AtomicUsize,
reads: AtomicUsize,
}
impl BlobStore for PendingMembership {
type Sink = <MemoryBlobStore as BlobStore>::Sink;
async fn begin(&self, _: BlobKey, _: u64) -> Result<Self::Sink, StoreError> {
unreachable!()
}
async fn get(
&self,
_: &BlobKey,
_: Option<ByteRange>,
) -> Result<Option<BlobBody>, StoreError> {
Ok(Some(BlobBody::Bytes(self.node.clone().into())))
}
async fn head(&self, _: &BlobKey) -> Result<Option<BlobMeta>, StoreError> {
self.reads.fetch_add(1, Ordering::SeqCst);
let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
self.peak.fetch_max(active, Ordering::SeqCst);
futures::future::poll_fn(|cx| {
if self.release.load(Ordering::SeqCst) {
std::task::Poll::Ready(())
} else {
cx.waker().wake_by_ref();
std::task::Poll::Pending
}
})
.await;
self.active.fetch_sub(1, Ordering::SeqCst);
Ok(Some(BlobMeta { len: 32 }))
}
async fn probe(&self) -> Result<(), StoreError> {
unreachable!()
}
async fn delete(&self, _: &BlobKey) -> Result<bool, StoreError> {
unreachable!()
}
}
impl MultipartBlobStore for PendingMembership {
type PartSink = crate::store::UnsupportedPartSink;
const MAX_PARTS: u32 = 10_000;
}
#[test]
fn implicit_packmap_checks_keep_at_most_six_backend_requests_active() {
let packs: Vec<_> = (0..17u8).map(|i| [i; 32]).collect();
let node = [99; 32];
let repo = RepoId {
namespace: NamespaceKey::deployment_default(),
name: RepoName::new("repo").unwrap(),
};
let pipe = Pipeline::new(
PendingMembership {
node: mkit_core::transfer::encode_packlist(None, &packs).unwrap(),
release: AtomicBool::new(false),
active: AtomicUsize::new(0),
peak: AtomicUsize::new(0),
reads: AtomicUsize::new(0),
},
MemoryKv::default(),
super::super::Hooks::new(),
super::super::PipelineConfig::new(
Addressing::Single { repo: repo.clone() },
super::super::AuthMode::Open,
crate::upload::UploadLimits {
max_total_bytes: 1024,
max_chunks: 16,
},
),
Arc::new(ManualClock::new(0)),
Arc::new(NoopMetrics),
)
.unwrap();
let mut upd = RefUpdate {
name: "refs/mkit/packmap/main".into(),
condition: RefWriteCondition::Missing,
new: Some(node),
};
let op = Operation::new(
repo,
Principal::Anonymous,
None,
OpKind::UpdateRef(upd.clone()),
);
let pending = [PendingPack {
pack: node,
bytes: 32,
packlist: true,
}];
let mut check = Box::pin(pipe.check_implicit_packmap(&op, &pending, None, &mut upd));
assert!((&mut check).now_or_never().is_none());
assert_eq!(pipe.blobs.active.load(Ordering::SeqCst), 6);
pipe.blobs.release.store(true, Ordering::SeqCst);
block_on(check).unwrap();
assert_eq!(pipe.blobs.reads.load(Ordering::SeqCst), packs.len());
assert_eq!(pipe.blobs.active.load(Ordering::SeqCst), 0);
assert!(pipe.blobs.peak.load(Ordering::SeqCst) <= 6);
assert_eq!(upd.new, Some(node));
}
}