2026-08-22 17:16:56 +00:00
|
|
|
//! Reusable namespace service lifecycle and public file-registration control.
|
|
|
|
|
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
use std::sync::Arc;
|
2026-08-27 22:09:26 +00:00
|
|
|
use std::time::Duration;
|
2026-08-22 17:16:56 +00:00
|
|
|
|
|
|
|
|
use swactor::actor::ActorAddress;
|
|
|
|
|
use swactor::runtime::Runtime;
|
2026-08-27 22:09:26 +00:00
|
|
|
use swactor_engine::EngineHandle;
|
2026-08-22 17:16:56 +00:00
|
|
|
|
|
|
|
|
use crate::blob::FileRegistration;
|
|
|
|
|
use crate::blob_transfer::BlobTransferSender;
|
2026-08-27 22:09:26 +00:00
|
|
|
use crate::namespace::{
|
|
|
|
|
DataDirectoryActor, DirectoryClient, NamespaceError, OperationId, RetirementRetry,
|
|
|
|
|
};
|
2026-08-22 17:16:56 +00:00
|
|
|
use crate::namespace_store::SourceRecovery;
|
|
|
|
|
use crate::path::DataPath;
|
|
|
|
|
use crate::source::{BlobSourceIn, BlobSourcePublisher, FileBlobSourceActor};
|
|
|
|
|
|
2026-08-27 22:09:26 +00:00
|
|
|
/// How often the directory re-sends retirements that no source has
|
|
|
|
|
/// acknowledged yet. Retire frames are at-most-once; this retry bounds how
|
|
|
|
|
/// long a lost frame can strand a published source and its binding.
|
|
|
|
|
const RETIREMENT_RETRY_PERIOD: Duration = Duration::from_millis(250);
|
|
|
|
|
|
2026-08-22 17:16:56 +00:00
|
|
|
pub struct DataNamespaceService {
|
|
|
|
|
directory: ActorAddress,
|
|
|
|
|
control: DataPlaneControl,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DataNamespaceService {
|
|
|
|
|
pub fn recover(
|
|
|
|
|
runtime: Runtime,
|
2026-08-27 22:09:26 +00:00
|
|
|
engine: EngineHandle,
|
2026-08-22 17:16:56 +00:00
|
|
|
store_path: impl AsRef<Path>,
|
|
|
|
|
source_sender: Arc<dyn BlobTransferSender>,
|
|
|
|
|
source_publisher: Arc<dyn BlobSourcePublisher>,
|
|
|
|
|
) -> Result<Self, NamespaceError> {
|
|
|
|
|
let recovery_runtime = runtime.clone();
|
|
|
|
|
let recovery_sender = Arc::clone(&source_sender);
|
|
|
|
|
let recovery_publisher = Arc::clone(&source_publisher);
|
2026-08-27 22:09:26 +00:00
|
|
|
let retire_retry =
|
|
|
|
|
RetirementRetry::new(engine, runtime.create_sender(), RETIREMENT_RETRY_PERIOD);
|
2026-08-22 17:16:56 +00:00
|
|
|
let directory = DataDirectoryActor::recover(
|
|
|
|
|
store_path,
|
2026-08-27 22:09:26 +00:00
|
|
|
Some(retire_retry),
|
2026-08-22 17:16:56 +00:00
|
|
|
move |recovery, expected_length| match recovery {
|
|
|
|
|
SourceRecovery::File { path } => {
|
|
|
|
|
let source = FileBlobSourceActor::recover(
|
|
|
|
|
recovery_runtime.clone(),
|
|
|
|
|
Arc::clone(&recovery_sender),
|
|
|
|
|
path,
|
|
|
|
|
expected_length,
|
|
|
|
|
)?;
|
|
|
|
|
let source = recovery_runtime.spawn(source).map_err(|error| {
|
|
|
|
|
NamespaceError::SourceRecovery(format!(
|
|
|
|
|
"spawn recovered file source {}: {error}",
|
|
|
|
|
path.display()
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
if let Err(error) = recovery_publisher.publish_source(source) {
|
2026-08-27 22:09:26 +00:00
|
|
|
let _ = recovery_runtime
|
|
|
|
|
.send_to(source, BlobSourceIn::Retire { reply_to: None });
|
2026-08-22 17:16:56 +00:00
|
|
|
return Err(NamespaceError::SourceRecovery(error));
|
|
|
|
|
}
|
|
|
|
|
Ok(source)
|
|
|
|
|
}
|
|
|
|
|
SourceRecovery::Actor { actor } => Ok(*actor),
|
|
|
|
|
},
|
|
|
|
|
)?;
|
|
|
|
|
let directory = runtime.spawn(directory).map_err(|error| {
|
|
|
|
|
NamespaceError::SourceRecovery(format!("spawn data directory: {error}"))
|
|
|
|
|
})?;
|
|
|
|
|
let control = DataPlaneControl {
|
|
|
|
|
runtime: runtime.clone(),
|
|
|
|
|
directory: DirectoryClient::new(runtime, directory),
|
|
|
|
|
source_sender,
|
|
|
|
|
source_publisher,
|
|
|
|
|
};
|
|
|
|
|
Ok(Self { directory, control })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn directory(&self) -> ActorAddress {
|
|
|
|
|
self.directory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn control(&self) -> DataPlaneControl {
|
|
|
|
|
self.control.clone()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct DataPlaneControl {
|
|
|
|
|
runtime: Runtime,
|
|
|
|
|
directory: DirectoryClient,
|
|
|
|
|
source_sender: Arc<dyn BlobTransferSender>,
|
|
|
|
|
source_publisher: Arc<dyn BlobSourcePublisher>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DataPlaneControl {
|
|
|
|
|
pub async fn ensure(
|
|
|
|
|
&self,
|
|
|
|
|
path: DataPath,
|
|
|
|
|
registration: FileRegistration,
|
|
|
|
|
) -> Result<(), NamespaceError> {
|
|
|
|
|
match self.directory.resolve(path.clone()).await {
|
|
|
|
|
Ok(_) => Ok(()),
|
|
|
|
|
Err(NamespaceError::PathNotFound(_)) | Err(NamespaceError::SourceRecovery(_)) => {
|
|
|
|
|
self.register(path, registration).await
|
|
|
|
|
}
|
|
|
|
|
Err(error) => Err(error),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn register(
|
|
|
|
|
&self,
|
|
|
|
|
path: DataPath,
|
|
|
|
|
registration: FileRegistration,
|
|
|
|
|
) -> Result<(), NamespaceError> {
|
|
|
|
|
let source = FileBlobSourceActor::open(
|
|
|
|
|
self.runtime.clone(),
|
|
|
|
|
Arc::clone(&self.source_sender),
|
|
|
|
|
registration.path(),
|
|
|
|
|
)?;
|
|
|
|
|
let length = source.length();
|
|
|
|
|
let recovery = source.recovery();
|
|
|
|
|
let source = self
|
|
|
|
|
.runtime
|
|
|
|
|
.spawn(source)
|
|
|
|
|
.map_err(|error| NamespaceError::SourceRecovery(error.to_string()))?;
|
|
|
|
|
let mut cleanup = PendingSourceRegistration {
|
|
|
|
|
runtime: self.runtime.clone(),
|
|
|
|
|
source,
|
|
|
|
|
armed: true,
|
|
|
|
|
};
|
|
|
|
|
self.source_publisher
|
|
|
|
|
.publish_source(source)
|
|
|
|
|
.map_err(NamespaceError::SourceRecovery)?;
|
|
|
|
|
self.directory
|
|
|
|
|
.register(path, source, length, recovery, random_operation_id())
|
|
|
|
|
.await?;
|
|
|
|
|
cleanup.armed = false;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn unregister(&self, path: DataPath) -> Result<(), NamespaceError> {
|
|
|
|
|
self.directory
|
|
|
|
|
.unregister(path, random_operation_id())
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-08-27 22:09:26 +00:00
|
|
|
|
|
|
|
|
pub async fn rename(
|
|
|
|
|
&self,
|
|
|
|
|
source: DataPath,
|
|
|
|
|
destination: DataPath,
|
|
|
|
|
replace: bool,
|
|
|
|
|
) -> Result<(), NamespaceError> {
|
|
|
|
|
self.directory
|
|
|
|
|
.rename(source, destination, replace, random_operation_id())
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-08-22 17:16:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct PendingSourceRegistration {
|
|
|
|
|
runtime: Runtime,
|
|
|
|
|
source: ActorAddress,
|
|
|
|
|
armed: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for PendingSourceRegistration {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
if self.armed {
|
2026-08-27 22:09:26 +00:00
|
|
|
let _ = self
|
|
|
|
|
.runtime
|
|
|
|
|
.send_to(self.source, BlobSourceIn::Retire { reply_to: None });
|
2026-08-22 17:16:56 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn random_operation_id() -> OperationId {
|
|
|
|
|
let actor = ActorAddress::new_random();
|
|
|
|
|
let mut bytes = [0_u8; 16];
|
|
|
|
|
bytes.copy_from_slice(&actor.0[..16]);
|
|
|
|
|
OperationId::from_u128(u128::from_be_bytes(bytes))
|
|
|
|
|
}
|