swactor/crates/data-plane/src/host.rs

1618 lines
55 KiB
Rust
Raw Normal View History

//! Host-side session, binding, and arena-allocation actors.
use std::collections::{BTreeMap, HashSet};
use std::sync::Arc;
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
use crate::arena::{
ArenaEvent, ArenaManager, ArenaRequest, LeaseRequestId, LeaseRing, QuiescenceProof, RingId,
RingSpec,
};
use crate::blob::{
BLOB_HEADER_LEN, BlobLease, BlobMetadata, ContentDigest, abort_host_blob,
install_filling_read_blob, install_read_blob, install_writable_blob, mark_host_released,
seal_host_read_blob, validate_host_sealed, write_host_blob_chunk,
};
use crate::bootstrap::JobHandoff;
use crate::path::{DataPath, JobContext};
use crate::protocol::{
AttachmentFailure, ChildSessionIn, DataOperation, DataPlaneError, HostSessionIn, JobCapability,
PublishedBlobInfo,
};
const BLOB_ALIGNMENT: u64 = 64;
const FIRST_BLOB_REQUEST_ID: u64 = 2;
#[derive(Clone)]
pub struct BlobSource {
bytes: Arc<[u8]>,
digest: Option<ContentDigest>,
}
impl BlobSource {
pub fn new(bytes: impl Into<Arc<[u8]>>) -> Self {
Self {
bytes: bytes.into(),
digest: None,
}
}
pub fn with_sha256(bytes: impl Into<Arc<[u8]>>) -> Self {
let bytes = bytes.into();
let digest = Some(ContentDigest::sha256(&bytes));
Self { bytes, digest }
}
pub fn length(&self) -> u64 {
self.bytes.len() as u64
}
pub fn digest(&self) -> Option<ContentDigest> {
self.digest
}
}
pub trait HostRouteRegistrar: Send + Sync + 'static {
fn register_child(
&self,
child_session: ActorAddress,
child_node: [u8; 32],
) -> Result<(), String>;
}
pub struct HostDataPlaneConfig {
pub arena: ArenaManager,
pub arena_generation: u64,
pub session_generation: u64,
pub capability: JobCapability,
pub job_context: JobContext,
pub blobs: BTreeMap<DataPath, BlobSource>,
pub route_registrar: Option<Arc<dyn HostRouteRegistrar>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HostSessionState {
AwaitingAttachment,
Running,
Closing,
Closed,
}
#[derive(Clone)]
struct PublishedBlob {
binding: ActorAddress,
lease: BlobLease,
metadata: BlobMetadata,
}
#[derive(Clone)]
enum BlobEntry {
Complete(BlobSource),
Filling {
source: ActorAddress,
waiters: HashSet<ActorAddress>,
},
Arena {
source: ActorAddress,
lease: BlobLease,
metadata: BlobMetadata,
},
}
pub struct HostDataPlaneSessionActor {
arena: Option<ArenaManager>,
arena_generation: u64,
session_generation: u64,
capability: JobCapability,
job_context: JobContext,
blobs: BTreeMap<DataPath, BlobEntry>,
route_registrar: Option<Arc<dyn HostRouteRegistrar>>,
child_session: Option<ActorAddress>,
allocator: Option<ActorAddress>,
active_bindings: HashSet<ActorAddress>,
published: BTreeMap<DataPath, PublishedBlob>,
state: HostSessionState,
}
impl HostDataPlaneSessionActor {
pub fn new(config: HostDataPlaneConfig) -> Result<Self, DataPlaneError> {
if config.arena_generation == 0 || config.session_generation == 0 {
return Err(DataPlaneError::SessionFailed(
"session and arena generations must be nonzero".to_owned(),
));
}
config
.job_context
.validate()
.map_err(|error| DataPlaneError::InvalidPath(error.to_string()))?;
Ok(Self {
arena: Some(config.arena),
arena_generation: config.arena_generation,
session_generation: config.session_generation,
capability: config.capability,
job_context: config.job_context,
blobs: config
.blobs
.into_iter()
.map(|(path, source)| (path, BlobEntry::Complete(source)))
.collect(),
route_registrar: config.route_registrar,
child_session: None,
allocator: None,
active_bindings: HashSet::new(),
published: BTreeMap::new(),
state: HostSessionState::AwaitingAttachment,
})
}
pub fn state(&self) -> HostSessionState {
self.state
}
fn send_open_failure(
&self,
ctx: &Ctx<'_>,
child_session: ActorAddress,
operation: ActorAddress,
error: DataPlaneError,
) {
let _ = ctx.send(
child_session,
ChildSessionIn::OperationFailed { operation, error },
);
}
fn validate_open(
&self,
child_session: ActorAddress,
logical: &DataPath,
operation: DataOperation,
) -> Result<DataPath, DataPlaneError> {
if self.state != HostSessionState::Running || self.child_session != Some(child_session) {
return Err(DataPlaneError::SessionNotRunning);
}
let resolved = self
.job_context
.resolve(logical)
.map_err(|error| DataPlaneError::InvalidPath(error.to_string()))?;
let authorized = match operation {
DataOperation::ReadBlob | DataOperation::ReadStream => {
self.job_context.can_read(&resolved)
}
DataOperation::WriteBlob | DataOperation::WriteStream => {
self.job_context.can_write(&resolved)
}
};
if !authorized {
return Err(DataPlaneError::Unauthorized {
path: resolved,
operation,
});
}
Ok(resolved)
}
fn maybe_finish_close(&mut self) {
if self.state == HostSessionState::Closing && self.active_bindings.is_empty() {
self.state = HostSessionState::Closed;
}
}
}
impl ActorInterface for HostDataPlaneSessionActor {
type Incoming = HostSessionIn;
type Response = ();
fn on_start(&mut self, ctx: &Ctx<'_>) {
let arena = self.arena.take().expect("host arena is installed once");
let allocator = ctx
.spawn(ArenaAllocatorActor::new(arena, self.arena_generation))
.expect("spawn arena allocator actor");
self.allocator = Some(allocator);
}
fn handle(&mut self, ctx: &Ctx<'_>, message: HostSessionIn) {
match message {
HostSessionIn::Attach {
child_session,
arena_generation,
job_capability,
child_node,
} => {
let route_failure = if let (Some(registrar), Some(child_node)) =
(&self.route_registrar, child_node)
{
registrar
.register_child(child_session, child_node)
.err()
.map(AttachmentFailure::RouteRejected)
} else {
None
};
let failure = if matches!(
self.state,
HostSessionState::Closing | HostSessionState::Closed
) {
Some(AttachmentFailure::SessionClosed)
} else if self.child_session.is_some() {
Some(AttachmentFailure::DuplicateAttachment)
} else if let Some(reason) = route_failure {
Some(reason)
} else if arena_generation != self.arena_generation {
Some(AttachmentFailure::ArenaGenerationMismatch {
expected: self.arena_generation,
found: arena_generation,
})
} else if job_capability != self.capability {
Some(AttachmentFailure::CapabilityRejected)
} else {
None
};
if let Some(reason) = failure {
let _ = ctx.send(
child_session,
ChildSessionIn::AttachmentFailed {
error: DataPlaneError::Attachment(reason),
},
);
} else {
self.child_session = Some(child_session);
self.state = HostSessionState::Running;
let _ = ctx.send(
child_session,
ChildSessionIn::Attached {
session_generation: self.session_generation,
},
);
}
}
HostSessionIn::OpenReadBlob {
path,
child_session,
operation,
} => {
let resolved =
match self.validate_open(child_session, &path, DataOperation::ReadBlob) {
Ok(path) => path,
Err(error) => {
self.send_open_failure(ctx, child_session, operation, error);
return;
}
};
let Some(source) = self.blobs.get(&resolved).cloned() else {
self.send_open_failure(
ctx,
child_session,
operation,
DataPlaneError::PathNotFound(resolved),
);
return;
};
let binding = match source {
BlobEntry::Complete(source) => HostBlobBindingActor::read(
ctx.self_addr(),
self.allocator.expect("allocator started"),
child_session,
operation,
resolved.clone(),
source,
),
BlobEntry::Filling { source, .. } => HostBlobBindingActor::waiting(
ctx.self_addr(),
self.allocator.expect("allocator started"),
child_session,
operation,
resolved.clone(),
source,
),
BlobEntry::Arena {
source,
lease,
metadata,
} => HostBlobBindingActor::presealed(
ctx.self_addr(),
self.allocator.expect("allocator started"),
child_session,
operation,
resolved.clone(),
source,
lease,
metadata,
),
};
match ctx.spawn(binding) {
Ok(binding) => {
self.active_bindings.insert(binding);
if let Some(BlobEntry::Filling { waiters, .. }) =
self.blobs.get_mut(&resolved)
{
waiters.insert(binding);
}
}
Err(error) => self.send_open_failure(
ctx,
child_session,
operation,
DataPlaneError::SessionFailed(error.to_string()),
),
}
}
HostSessionIn::OpenWriteBlob {
path,
length,
child_session,
operation,
} => {
let resolved =
match self.validate_open(child_session, &path, DataOperation::WriteBlob) {
Ok(path) => path,
Err(error) => {
self.send_open_failure(ctx, child_session, operation, error);
return;
}
};
if self.published.contains_key(&resolved) {
self.send_open_failure(
ctx,
child_session,
operation,
DataPlaneError::PathAlreadyExists(resolved),
);
return;
}
let binding = HostBlobBindingActor::write(
ctx.self_addr(),
self.allocator.expect("allocator started"),
child_session,
operation,
resolved,
length,
);
match ctx.spawn(binding) {
Ok(binding) => {
self.active_bindings.insert(binding);
}
Err(error) => self.send_open_failure(
ctx,
child_session,
operation,
DataPlaneError::SessionFailed(error.to_string()),
),
}
}
HostSessionIn::ReleaseBlob {
binding,
lease_id,
generation,
} => {
if self.active_bindings.contains(&binding) {
let _ = ctx.send(
binding,
HostBindingIn::Release {
lease_id,
generation,
},
);
}
}
HostSessionIn::SealWriteBlob {
binding,
operation,
lease,
metadata,
} => {
if self.active_bindings.contains(&binding) {
let _ = ctx.send(
binding,
HostBindingIn::Seal {
operation,
lease,
metadata,
},
);
}
}
HostSessionIn::AbortWriteBlob {
binding,
operation,
lease_id,
generation,
} => {
if self.active_bindings.contains(&binding) {
let _ = ctx.send(
binding,
HostBindingIn::Abort {
operation,
lease_id,
generation,
},
);
}
}
HostSessionIn::BindingPublished {
binding,
operation,
path,
lease,
metadata,
} => {
if !self.active_bindings.contains(&binding) {
return;
}
if self.published.contains_key(&path) {
let _ = ctx.send(
binding,
HostBindingIn::PublicationRejected(DataPlaneError::PathAlreadyExists(path)),
);
return;
}
self.published.insert(
path.clone(),
PublishedBlob {
binding,
lease,
metadata,
},
);
let _ = ctx.send(binding, HostBindingIn::PublicationAccepted { operation });
}
HostSessionIn::BindingFaulted {
binding,
operation,
error,
} => {
if self.active_bindings.contains(&binding) {
if let Some(child) = self.child_session {
let _ =
ctx.send(child, ChildSessionIn::OperationFailed { operation, error });
}
}
}
HostSessionIn::BindingDone { binding } => {
self.active_bindings.remove(&binding);
self.published.retain(|_, blob| blob.binding != binding);
self.maybe_finish_close();
}
HostSessionIn::InspectPublished { path, reply_to } => {
let resolved = self.job_context.resolve(&path).ok();
let published = resolved.and_then(|path| {
self.published.get(&path).map(|blob| PublishedBlobInfo {
path,
binding: blob.binding,
lease: blob.lease,
metadata: blob.metadata.clone(),
})
});
let _ = ctx.send(reply_to, published);
}
HostSessionIn::ReleasePublished { path } => {
if let Ok(path) = self.job_context.resolve(&path) {
if let Some(blob) = self.published.remove(&path) {
let _ = ctx.send(blob.binding, HostBindingIn::ReleasePublished);
}
}
}
HostSessionIn::BeginBlobSource {
path,
metadata,
reply_to,
} => {
let resolved = self
.job_context
.resolve(&path)
.map_err(|error| DataPlaneError::InvalidPath(error.to_string()));
let resolved = match resolved {
Ok(path) if self.job_context.can_read(&path) => path,
Ok(path) => {
let _ = ctx.send(
reply_to,
Err::<(), _>(DataPlaneError::Unauthorized {
path,
operation: DataOperation::ReadBlob,
}),
);
return;
}
Err(error) => {
let _ = ctx.send(reply_to, Err::<(), _>(error));
return;
}
};
if self.blobs.contains_key(&resolved) {
let _ = ctx.send(
reply_to,
Err::<(), _>(DataPlaneError::PathAlreadyExists(resolved)),
);
return;
}
let source_actor = IncomingBlobSourceActor::new(
ctx.self_addr(),
self.allocator.expect("allocator started"),
resolved.clone(),
metadata,
reply_to,
);
match ctx.spawn(source_actor) {
Ok(source) => {
self.blobs.insert(
resolved,
BlobEntry::Filling {
source,
waiters: HashSet::new(),
},
);
}
Err(error) => {
let _ = ctx.send(
reply_to,
Err::<(), _>(DataPlaneError::SessionFailed(error.to_string())),
);
}
}
}
HostSessionIn::BlobSourceChunk { path, bytes } => {
if let Ok(path) = self.job_context.resolve(&path) {
if let Some(BlobEntry::Filling { source, .. }) = self.blobs.get(&path) {
let _ = ctx.send(*source, IncomingSourceIn::Chunk(bytes));
}
}
}
HostSessionIn::FinishBlobSource { path } => {
if let Ok(path) = self.job_context.resolve(&path) {
if let Some(BlobEntry::Filling { source, .. }) = self.blobs.get(&path) {
let _ = ctx.send(*source, IncomingSourceIn::Finish);
}
}
}
HostSessionIn::FailBlobSource { path, reason } => {
if let Ok(path) = self.job_context.resolve(&path) {
if let Some(BlobEntry::Filling { source, .. }) = self.blobs.get(&path) {
let _ = ctx.send(*source, IncomingSourceIn::Fail(reason));
}
}
}
HostSessionIn::SourceReady {
path,
source,
lease,
metadata,
} => {
let waiters = match self.blobs.remove(&path) {
Some(BlobEntry::Filling {
source: expected,
waiters,
}) if expected == source => waiters,
Some(entry) => {
self.blobs.insert(path, entry);
return;
}
None => return,
};
self.blobs.insert(
path,
BlobEntry::Arena {
source,
lease,
metadata: metadata.clone(),
},
);
for binding in waiters {
let _ = ctx.send(source, IncomingSourceIn::Acquire { binding });
}
}
HostSessionIn::SourceFaulted {
path,
source,
error,
} => {
let waiters = match self.blobs.remove(&path) {
Some(BlobEntry::Filling {
source: expected,
waiters,
}) if expected == source => waiters,
Some(entry) => {
self.blobs.insert(path, entry);
return;
}
None => return,
};
for binding in waiters {
let _ = ctx.send(binding, HostBindingIn::Allocated(Err(error.clone())));
}
}
HostSessionIn::ConfigureRun { run_id, reply_to } => {
let result = if self.state != HostSessionState::AwaitingAttachment
|| self.child_session.is_some()
{
Err(DataPlaneError::SessionNotRunning)
} else {
let mut context = self.job_context.clone();
context.run_id = run_id;
match context.validate() {
Ok(()) => {
self.job_context = context;
Ok(())
}
Err(error) => Err(DataPlaneError::InvalidPath(error.to_string())),
}
};
let _ = ctx.send(reply_to, result);
}
HostSessionIn::Close => {
if matches!(
self.state,
HostSessionState::Closing | HostSessionState::Closed
) {
return;
}
self.state = HostSessionState::Closing;
for binding in self.active_bindings.iter().copied() {
let _ = ctx.send(binding, HostBindingIn::SessionClosed);
}
let mut sources = HashSet::new();
for entry in self.blobs.values() {
match entry {
BlobEntry::Filling { source, .. } | BlobEntry::Arena { source, .. } => {
sources.insert(*source);
}
BlobEntry::Complete(_) => {}
}
}
for source in sources {
let _ = ctx.send(source, IncomingSourceIn::SessionClosed);
}
self.maybe_finish_close();
}
}
}
}
pub fn install_session_env(
handoff: &mut JobHandoff,
host_session: ActorAddress,
capability: JobCapability,
) {
handoff.env.insert(
crate::bootstrap::ENV_DATA_PLANE_ACTOR.to_owned(),
host_session.to_full_hex(),
);
handoff.env.insert(
crate::bootstrap::ENV_JOB_CAPABILITY.to_owned(),
capability.to_hex(),
);
}
#[derive(Clone)]
enum AllocationKind {
Read {
bytes: Arc<[u8]>,
digest: Option<ContentDigest>,
},
FillingRead {
length: u64,
digest: Option<ContentDigest>,
},
Write {
length: u64,
digest: Option<ContentDigest>,
},
}
#[derive(Clone)]
enum ArenaAllocatorIn {
Allocate {
binding: ActorAddress,
kind: AllocationKind,
},
AllocateSource {
source: ActorAddress,
kind: AllocationKind,
},
ValidateSealed {
binding: ActorAddress,
lease: BlobLease,
metadata: BlobMetadata,
},
WriteSource {
source: ActorAddress,
lease: BlobLease,
offset: u64,
bytes: Vec<u8>,
},
SealSource {
source: ActorAddress,
lease: BlobLease,
metadata: BlobMetadata,
written: u64,
},
AbortSource {
source: ActorAddress,
lease: BlobLease,
},
ReleaseSource {
source: ActorAddress,
lease: BlobLease,
},
Release {
binding: ActorAddress,
lease: BlobLease,
},
}
struct ArenaAllocatorActor {
arena: ArenaManager,
next_request_id: u64,
next_generation: u64,
}
impl ArenaAllocatorActor {
fn new(arena: ArenaManager, arena_generation: u64) -> Self {
Self {
arena,
next_request_id: FIRST_BLOB_REQUEST_ID,
next_generation: arena_generation,
}
}
fn allocate(
&mut self,
kind: AllocationKind,
) -> Result<(BlobLease, BlobMetadata), DataPlaneError> {
let length = match &kind {
AllocationKind::Read { bytes, .. } => bytes.len() as u64,
AllocationKind::Write { length, .. } | AllocationKind::FillingRead { length, .. } => {
*length
}
};
let request_id = self.next_request_id;
self.next_request_id = self
.next_request_id
.checked_add(1)
.ok_or_else(|| DataPlaneError::SessionFailed("blob request id exhausted".to_owned()))?;
let generation = self.next_generation;
self.next_generation = self
.next_generation
.checked_add(1)
.filter(|next| *next != 0)
.ok_or_else(|| DataPlaneError::SessionFailed("blob generation exhausted".to_owned()))?;
let mut events = self.arena.request(ArenaRequest::LeaseRing(LeaseRing {
request_id: LeaseRequestId(request_id),
ring_spec: RingSpec {
header_bytes: BLOB_HEADER_LEN,
data_bytes: length,
alignment: BLOB_ALIGNMENT,
},
}));
let allocation = match (events.len(), events.pop()) {
(1, Some(ArenaEvent::RingLeased { lease })) => lease,
(1, Some(ArenaEvent::RingLeaseQueued { request_id })) => {
self.arena.request(ArenaRequest::CancelLease { request_id });
return Err(DataPlaneError::ArenaExhausted);
}
(1, Some(ArenaEvent::RingLeaseRejected { .. })) => {
return Err(DataPlaneError::ArenaExhausted);
}
_ => {
return Err(DataPlaneError::SessionFailed(
"unexpected blob arena allocation outcome".to_owned(),
));
}
};
let installed = match kind {
AllocationKind::Read { bytes, digest } => {
install_read_blob(&self.arena, &allocation, generation, &bytes, digest)
}
AllocationKind::Write { length, digest } => {
install_writable_blob(&self.arena, &allocation, generation, length, digest)
}
AllocationKind::FillingRead { length, digest } => {
install_filling_read_blob(&self.arena, &allocation, generation, length, digest)
}
};
match installed {
Ok(grant) => Ok(grant),
Err(error) => {
let _ = self.arena.request(ArenaRequest::ReleaseRing {
ring_id: allocation.ring_id,
proof: QuiescenceProof::verified(),
});
Err(error.into())
}
}
}
fn release(&mut self, lease: BlobLease) -> Result<(), DataPlaneError> {
let ring_id = RingId(lease.lease_id.0);
let Some(allocation) = self.arena.lookup_lease(ring_id) else {
return Err(DataPlaneError::Blob(
crate::protocol::BlobFailure::InvalidLease,
));
};
if allocation.layout.start_offset != lease.offset
|| allocation.layout.data_bytes != lease.length
{
return Err(DataPlaneError::Blob(
crate::protocol::BlobFailure::InvalidLease,
));
}
mark_host_released(&self.arena, lease)?;
let events = self.arena.request(ArenaRequest::ReleaseRing {
ring_id,
proof: QuiescenceProof::verified(),
});
if matches!(events.as_slice(), [ArenaEvent::RingReleased { .. }]) {
Ok(())
} else {
Err(DataPlaneError::SessionFailed(
"arena rejected blob release".to_owned(),
))
}
}
}
impl ActorInterface for ArenaAllocatorActor {
type Incoming = ArenaAllocatorIn;
type Response = ();
fn handle(&mut self, ctx: &Ctx<'_>, message: ArenaAllocatorIn) {
match message {
ArenaAllocatorIn::Allocate { binding, kind } => {
let result = self.allocate(kind);
let _ = ctx.send(binding, HostBindingIn::Allocated(result));
}
ArenaAllocatorIn::AllocateSource { source, kind } => {
let result = self.allocate(kind);
let _ = ctx.send(source, IncomingSourceIn::Allocated(result));
}
ArenaAllocatorIn::ValidateSealed {
binding,
lease,
metadata,
} => {
let result =
validate_host_sealed(&self.arena, lease, &metadata).map_err(Into::into);
let _ = ctx.send(binding, HostBindingIn::SealValidated(result));
}
ArenaAllocatorIn::WriteSource {
source,
lease,
offset,
bytes,
} => {
if let Err(error) = write_host_blob_chunk(&self.arena, lease, offset, &bytes) {
let _ = ctx.send(source, IncomingSourceIn::AllocatorFault(error.into()));
}
}
ArenaAllocatorIn::SealSource {
source,
lease,
metadata,
written,
} => {
let result =
seal_host_read_blob(&self.arena, lease, &metadata, written).map_err(Into::into);
let _ = ctx.send(source, IncomingSourceIn::Sealed(result));
}
ArenaAllocatorIn::AbortSource { source, lease } => {
let result = abort_host_blob(&self.arena, lease)
.map_err(DataPlaneError::from)
.and_then(|()| self.release(lease));
let _ = ctx.send(source, IncomingSourceIn::Released(result));
}
ArenaAllocatorIn::ReleaseSource { source, lease } => {
let result = self.release(lease);
let _ = ctx.send(source, IncomingSourceIn::Released(result));
}
ArenaAllocatorIn::Release { binding, lease } => {
let result = self.release(lease);
let _ = ctx.send(binding, HostBindingIn::Released(result));
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum IncomingSourceState {
Allocating,
Filling,
Sealing,
Sealed,
Releasing,
Faulted,
}
#[derive(Clone)]
enum IncomingSourceIn {
Allocated(Result<(BlobLease, BlobMetadata), DataPlaneError>),
Chunk(Vec<u8>),
Finish,
Fail(String),
Sealed(Result<(), DataPlaneError>),
AllocatorFault(DataPlaneError),
Acquire { binding: ActorAddress },
ReleaseGrant { binding: ActorAddress },
Released(Result<(), DataPlaneError>),
SessionClosed,
}
struct IncomingBlobSourceActor {
host_session: ActorAddress,
allocator: ActorAddress,
path: DataPath,
requested: BlobMetadata,
ready_reply: Option<ActorAddress>,
lease: Option<BlobLease>,
metadata: Option<BlobMetadata>,
written: u64,
grants: HashSet<ActorAddress>,
state: IncomingSourceState,
}
impl IncomingBlobSourceActor {
fn new(
host_session: ActorAddress,
allocator: ActorAddress,
path: DataPath,
requested: BlobMetadata,
ready_reply: ActorAddress,
) -> Self {
Self {
host_session,
allocator,
path,
requested,
ready_reply: Some(ready_reply),
lease: None,
metadata: None,
written: 0,
grants: HashSet::new(),
state: IncomingSourceState::Allocating,
}
}
fn fault(&mut self, ctx: &Ctx<'_>, error: DataPlaneError) {
self.state = IncomingSourceState::Faulted;
if let Some(reply_to) = self.ready_reply.take() {
let _ = ctx.send(reply_to, Err::<(), _>(error.clone()));
}
let _ = ctx.send(
self.host_session,
HostSessionIn::SourceFaulted {
path: self.path.clone(),
source: ctx.self_addr(),
error,
},
);
if let Some(lease) = self.lease {
self.state = IncomingSourceState::Releasing;
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::AbortSource {
source: ctx.self_addr(),
lease,
},
);
} else {
ctx.stop_self();
}
}
}
impl ActorInterface for IncomingBlobSourceActor {
type Incoming = IncomingSourceIn;
type Response = ();
fn on_start(&mut self, ctx: &Ctx<'_>) {
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::AllocateSource {
source: ctx.self_addr(),
kind: AllocationKind::FillingRead {
length: self.requested.length,
digest: self.requested.digest,
},
},
);
}
fn handle(&mut self, ctx: &Ctx<'_>, message: IncomingSourceIn) {
match message {
IncomingSourceIn::Allocated(Ok((lease, metadata)))
if self.state == IncomingSourceState::Allocating =>
{
self.lease = Some(lease);
self.metadata = Some(metadata);
self.state = IncomingSourceState::Filling;
if let Some(reply_to) = self.ready_reply.take() {
let _ = ctx.send(reply_to, Ok::<_, DataPlaneError>(()));
}
}
IncomingSourceIn::Allocated(Err(error))
if self.state == IncomingSourceState::Allocating =>
{
self.fault(ctx, error);
}
IncomingSourceIn::Chunk(bytes) if self.state == IncomingSourceState::Filling => {
let count = bytes.len() as u64;
let Some(next_written) = self
.written
.checked_add(count)
.filter(|written| *written <= self.requested.length)
else {
self.fault(
ctx,
DataPlaneError::Blob(crate::protocol::BlobFailure::Length {
expected: self.requested.length,
found: self.written.saturating_add(count),
}),
);
return;
};
let lease = self.lease.expect("filling source lease");
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::WriteSource {
source: ctx.self_addr(),
lease,
offset: self.written,
bytes,
},
);
self.written = next_written;
}
IncomingSourceIn::Finish if self.state == IncomingSourceState::Filling => {
self.state = IncomingSourceState::Sealing;
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::SealSource {
source: ctx.self_addr(),
lease: self.lease.expect("sealing source lease"),
metadata: self.metadata.clone().expect("sealing source metadata"),
written: self.written,
},
);
}
IncomingSourceIn::Fail(reason)
if matches!(
self.state,
IncomingSourceState::Allocating | IncomingSourceState::Filling
) =>
{
self.fault(ctx, DataPlaneError::SourceFailure(reason));
}
IncomingSourceIn::Sealed(Ok(())) if self.state == IncomingSourceState::Sealing => {
self.state = IncomingSourceState::Sealed;
let _ = ctx.send(
self.host_session,
HostSessionIn::SourceReady {
path: self.path.clone(),
source: ctx.self_addr(),
lease: self.lease.expect("sealed source lease"),
metadata: self.metadata.clone().expect("sealed source metadata"),
},
);
}
IncomingSourceIn::Sealed(Err(error)) if self.state == IncomingSourceState::Sealing => {
self.fault(ctx, error);
}
IncomingSourceIn::AllocatorFault(error) => self.fault(ctx, error),
IncomingSourceIn::Acquire { binding } if self.state == IncomingSourceState::Sealed => {
self.grants.insert(binding);
let _ = ctx.send(
binding,
HostBindingIn::Presealed {
source: ctx.self_addr(),
lease: self.lease.expect("sealed source lease"),
metadata: self.metadata.clone().expect("sealed source metadata"),
},
);
}
IncomingSourceIn::ReleaseGrant { binding }
if self.state == IncomingSourceState::Sealed =>
{
self.grants.remove(&binding);
let _ = ctx.send(binding, HostBindingIn::SourceReleased);
}
IncomingSourceIn::SessionClosed
if !matches!(
self.state,
IncomingSourceState::Releasing | IncomingSourceState::Faulted
) =>
{
if let Some(lease) = self.lease {
self.state = IncomingSourceState::Releasing;
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::ReleaseSource {
source: ctx.self_addr(),
lease,
},
);
} else {
ctx.stop_self();
}
}
IncomingSourceIn::Released(result) if self.state == IncomingSourceState::Releasing => {
if let Err(error) = result {
let _ = ctx.send(
self.host_session,
HostSessionIn::SourceFaulted {
path: self.path.clone(),
source: ctx.self_addr(),
error,
},
);
}
ctx.stop_self();
}
_ => {}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HostBindingState {
Allocated,
Filling,
Granted,
Sealing,
Publishing,
Published,
Releasing,
Released,
Faulted,
}
#[derive(Clone)]
enum BindingMode {
Read { source: BlobSource },
ArenaSource { source: ActorAddress },
Write { length: u64 },
}
#[derive(Clone)]
enum ReleaseOutcome {
ReadReleased,
WriteAborted { operation: ActorAddress },
PublishedReleased,
SessionClosed,
Faulted,
}
#[derive(Clone)]
enum HostBindingIn {
Allocated(Result<(BlobLease, BlobMetadata), DataPlaneError>),
Presealed {
source: ActorAddress,
lease: BlobLease,
metadata: BlobMetadata,
},
SourceReleased,
Release {
lease_id: crate::ids::BlobLeaseId,
generation: u64,
},
Seal {
operation: ActorAddress,
lease: BlobLease,
metadata: BlobMetadata,
},
SealValidated(Result<(), DataPlaneError>),
Abort {
operation: ActorAddress,
lease_id: crate::ids::BlobLeaseId,
generation: u64,
},
PublicationAccepted {
operation: ActorAddress,
},
PublicationRejected(DataPlaneError),
ReleasePublished,
SessionClosed,
Released(Result<(), DataPlaneError>),
}
struct HostBlobBindingActor {
host_session: ActorAddress,
allocator: ActorAddress,
child_session: ActorAddress,
operation: ActorAddress,
path: DataPath,
mode: BindingMode,
state: HostBindingState,
lease: Option<BlobLease>,
metadata: Option<BlobMetadata>,
release_outcome: Option<ReleaseOutcome>,
}
impl HostBlobBindingActor {
fn read(
host_session: ActorAddress,
allocator: ActorAddress,
child_session: ActorAddress,
operation: ActorAddress,
path: DataPath,
source: BlobSource,
) -> Self {
Self {
host_session,
allocator,
child_session,
operation,
path,
mode: BindingMode::Read { source },
state: HostBindingState::Allocated,
lease: None,
metadata: None,
release_outcome: None,
}
}
fn waiting(
host_session: ActorAddress,
allocator: ActorAddress,
child_session: ActorAddress,
operation: ActorAddress,
path: DataPath,
source: ActorAddress,
) -> Self {
Self {
host_session,
allocator,
child_session,
operation,
path,
mode: BindingMode::ArenaSource { source },
state: HostBindingState::Allocated,
lease: None,
metadata: None,
release_outcome: None,
}
}
fn presealed(
host_session: ActorAddress,
allocator: ActorAddress,
child_session: ActorAddress,
operation: ActorAddress,
path: DataPath,
source: ActorAddress,
lease: BlobLease,
metadata: BlobMetadata,
) -> Self {
Self {
host_session,
allocator,
child_session,
operation,
path,
mode: BindingMode::ArenaSource { source },
state: HostBindingState::Allocated,
lease: Some(lease),
metadata: Some(metadata),
release_outcome: None,
}
}
fn write(
host_session: ActorAddress,
allocator: ActorAddress,
child_session: ActorAddress,
operation: ActorAddress,
path: DataPath,
length: u64,
) -> Self {
Self {
host_session,
allocator,
child_session,
operation,
path,
mode: BindingMode::Write { length },
state: HostBindingState::Allocated,
lease: None,
metadata: None,
release_outcome: None,
}
}
fn begin_release(&mut self, ctx: &Ctx<'_>, outcome: ReleaseOutcome) {
let Some(lease) = self.lease else {
self.finish_without_lease(ctx, outcome);
return;
};
self.state = HostBindingState::Releasing;
self.release_outcome = Some(outcome);
match self.mode {
BindingMode::ArenaSource { source } => {
let _ = ctx.send(
source,
IncomingSourceIn::ReleaseGrant {
binding: ctx.self_addr(),
},
);
}
BindingMode::Read { .. } | BindingMode::Write { .. } => {
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::Release {
binding: ctx.self_addr(),
lease,
},
);
}
}
}
fn finish_without_lease(&mut self, ctx: &Ctx<'_>, outcome: ReleaseOutcome) {
self.state = HostBindingState::Released;
if let ReleaseOutcome::WriteAborted { operation } = outcome {
let _ = ctx.send(
self.child_session,
ChildSessionIn::WriteAborted { operation },
);
}
let _ = ctx.send(
self.host_session,
HostSessionIn::BindingDone {
binding: ctx.self_addr(),
},
);
ctx.stop_self();
}
fn fail(&mut self, ctx: &Ctx<'_>, error: DataPlaneError) {
self.state = HostBindingState::Faulted;
let _ = ctx.send(
self.host_session,
HostSessionIn::BindingFaulted {
binding: ctx.self_addr(),
operation: self.operation,
error,
},
);
if self.lease.is_some() {
self.begin_release(ctx, ReleaseOutcome::Faulted);
} else {
self.finish_without_lease(ctx, ReleaseOutcome::Faulted);
}
}
fn lease_matches(&self, lease_id: crate::ids::BlobLeaseId, generation: u64) -> bool {
self.lease
.is_some_and(|lease| lease.lease_id == lease_id && lease.generation == generation)
}
}
impl ActorInterface for HostBlobBindingActor {
type Incoming = HostBindingIn;
type Response = ();
fn on_start(&mut self, ctx: &Ctx<'_>) {
self.state = HostBindingState::Filling;
let kind = match &self.mode {
BindingMode::Read { source } => Some(AllocationKind::Read {
bytes: source.bytes.clone(),
digest: source.digest,
}),
BindingMode::Write { length } => Some(AllocationKind::Write {
length: *length,
digest: None,
}),
BindingMode::ArenaSource { source } => {
let _ = ctx.send(
*source,
IncomingSourceIn::Acquire {
binding: ctx.self_addr(),
},
);
None
}
};
if let Some(kind) = kind {
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::Allocate {
binding: ctx.self_addr(),
kind,
},
);
}
}
fn handle(&mut self, ctx: &Ctx<'_>, message: HostBindingIn) {
match message {
HostBindingIn::Allocated(Ok((lease, metadata)))
if self.state == HostBindingState::Filling =>
{
self.lease = Some(lease);
self.metadata = Some(metadata.clone());
self.state = HostBindingState::Granted;
let response = match self.mode {
BindingMode::Read { .. } => ChildSessionIn::BlobOpened {
operation: self.operation,
host_binding: ctx.self_addr(),
lease,
metadata,
},
BindingMode::Write { .. } => ChildSessionIn::WriteBlobOpened {
operation: self.operation,
host_binding: ctx.self_addr(),
lease,
metadata,
},
BindingMode::ArenaSource { .. } => ChildSessionIn::BlobOpened {
operation: self.operation,
host_binding: ctx.self_addr(),
lease,
metadata,
},
};
let _ = ctx.send(self.child_session, response);
}
HostBindingIn::Presealed {
source,
lease,
metadata,
} if self.state == HostBindingState::Filling
&& matches!(
self.mode,
BindingMode::ArenaSource {
source: expected
} if expected == source
) =>
{
if self.lease.is_some_and(|expected| expected != lease)
|| self
.metadata
.as_ref()
.is_some_and(|expected| expected != &metadata)
{
self.fail(
ctx,
DataPlaneError::Blob(crate::protocol::BlobFailure::InvalidLease),
);
return;
}
self.lease = Some(lease);
self.metadata = Some(metadata.clone());
self.state = HostBindingState::Granted;
let _ = ctx.send(
self.child_session,
ChildSessionIn::BlobOpened {
operation: self.operation,
host_binding: ctx.self_addr(),
lease,
metadata,
},
);
}
HostBindingIn::Allocated(Err(error)) if self.state == HostBindingState::Filling => {
self.fail(ctx, error);
}
HostBindingIn::Release {
lease_id,
generation,
} if self.state == HostBindingState::Granted
&& matches!(
self.mode,
BindingMode::Read { .. } | BindingMode::ArenaSource { .. }
) =>
{
if self.lease_matches(lease_id, generation) {
self.begin_release(ctx, ReleaseOutcome::ReadReleased);
}
}
HostBindingIn::Seal {
operation,
lease,
metadata,
} if self.state == HostBindingState::Granted
&& matches!(self.mode, BindingMode::Write { .. }) =>
{
if operation != self.operation
|| self.lease != Some(lease)
|| self.metadata.as_ref() != Some(&metadata)
{
self.fail(
ctx,
DataPlaneError::Blob(crate::protocol::BlobFailure::InvalidLease),
);
return;
}
self.state = HostBindingState::Sealing;
let _ = ctx.send(
self.allocator,
ArenaAllocatorIn::ValidateSealed {
binding: ctx.self_addr(),
lease,
metadata,
},
);
}
HostBindingIn::SealValidated(Ok(())) if self.state == HostBindingState::Sealing => {
let lease = self.lease.expect("validated write lease");
let metadata = self.metadata.clone().expect("validated write metadata");
self.state = HostBindingState::Publishing;
let _ = ctx.send(
self.host_session,
HostSessionIn::BindingPublished {
binding: ctx.self_addr(),
operation: self.operation,
path: self.path.clone(),
lease,
metadata,
},
);
}
HostBindingIn::SealValidated(Err(error)) if self.state == HostBindingState::Sealing => {
self.fail(ctx, error);
}
HostBindingIn::Abort {
operation,
lease_id,
generation,
} if self.state == HostBindingState::Granted
&& matches!(self.mode, BindingMode::Write { .. }) =>
{
if operation == self.operation && self.lease_matches(lease_id, generation) {
self.begin_release(ctx, ReleaseOutcome::WriteAborted { operation });
}
}
HostBindingIn::PublicationAccepted { operation }
if self.state == HostBindingState::Publishing && operation == self.operation =>
{
self.state = HostBindingState::Published;
let _ = ctx.send(
self.child_session,
ChildSessionIn::WritePublished { operation },
);
}
HostBindingIn::PublicationRejected(error)
if self.state == HostBindingState::Publishing =>
{
self.fail(ctx, error);
}
HostBindingIn::ReleasePublished if self.state == HostBindingState::Published => {
self.begin_release(ctx, ReleaseOutcome::PublishedReleased);
}
HostBindingIn::SessionClosed
if !matches!(
self.state,
HostBindingState::Released | HostBindingState::Releasing
) =>
{
self.begin_release(ctx, ReleaseOutcome::SessionClosed);
}
HostBindingIn::Released(result) if self.state == HostBindingState::Releasing => {
if let Err(error) = result {
let _ = ctx.send(
self.host_session,
HostSessionIn::BindingFaulted {
binding: ctx.self_addr(),
operation: self.operation,
error,
},
);
}
let outcome = self
.release_outcome
.take()
.unwrap_or(ReleaseOutcome::Faulted);
self.finish_without_lease(ctx, outcome);
}
HostBindingIn::SourceReleased if self.state == HostBindingState::Releasing => {
let outcome = self
.release_outcome
.take()
.unwrap_or(ReleaseOutcome::ReadReleased);
self.finish_without_lease(ctx, outcome);
}
_ => {}
}
}
}