blob: debbf6e62c32832bc05dad096bbb47a3ca9a549d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#[allow(dead_code)]
mod support;
use futures_util::future::BoxFuture;
use maka_capabilities::call::Attachments;
use maka_capabilities::call::Identity;
use maka_event_log::{
EventLog,
root::{RootNamespaces, RootOwner},
};
use maka_host_runtime::application::{
Application, Context, Handler, NativeActivation, NativeOwner, NativeRegistry, NativeRequest,
NativeServices,
};
use maka_plugin_core::{
composition::InstanceId,
operation::{OperationOutcome, Receipt},
package::{EntryTarget, Implementation},
runtime::Published,
};
use maka_transport::{
GATEWAY,
transport::{NativeGrant, PeerPolicy},
};
use serde_json::{Value, json};
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
time::Duration,
};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
#[derive(Default)]
struct Jobs(Mutex<Vec<Arc<NativeRequest>>>);
impl NativeOwner for Jobs {
fn close(&self) -> BoxFuture<'static, Result<(), String>> {
let requests = self.0.lock().unwrap().clone();
Box::pin(async move {
for request in requests {
request.finish().await?;
}
Ok(())
})
}
}
fn native(jobs: Arc<Jobs>, captured: Arc<Mutex<Option<NativeServices>>>) -> NativeRegistry {
let mut registry = NativeRegistry::default();
registry
.insert(
GATEWAY.into(),
Arc::new(move |context, _| {
*captured.lock().unwrap() = Some(context.services());
let jobs = jobs.clone();
Box::pin(async move {
NativeActivation {
exports: Ok(BTreeMap::new()),
owner: jobs,
}
})
}),
)
.unwrap();
registry
}
fn grant(provider: InstanceId, recovery: bool) -> NativeGrant {
NativeGrant {
origin: support::identity(13),
consumer: support::identity(13),
provider,
export: "Counter".into(),
methods: ["read".into(), "write".into()].into(),
recovery,
}
}
fn request(services: &NativeServices, jobs: &Jobs) -> Arc<NativeRequest> {
let request = Arc::new(
services
.request(
Identity::Remote {
request_id: Uuid::new_v4(),
},
CancellationToken::new(),
)
.unwrap(),
);
jobs.0.lock().unwrap().push(request.clone());
request
}
async fn store() -> (tempfile::TempDir, Arc<EventLog>) {
let directory = tempfile::tempdir().unwrap();
let namespaces = RootNamespaces {
ownership: directory.path().join("owners"),
control: directory.path().join("control"),
};
let root = Arc::new(RootOwner::create(&directory.path().join("root"), &namespaces).unwrap());
let store = Arc::new(EventLog::for_root(root).await.unwrap());
(directory, store)
}
#[tokio::test]
async fn native_domain_calls_require_explicit_grants_and_ignore_public_identity_claims() {
let (_directory, store) = store().await;
let deployment = Uuid::new_v4();
let (input, packages) = support::host();
store
.commit_application(deployment, 0, input, vec![], packages)
.await
.unwrap();
let captured = Arc::new(Mutex::new(None));
let jobs = Arc::new(Jobs::default());
let policy = Arc::new(PeerPolicy::default());
let app = Application::start_from_store(
store.clone(),
deployment,
native(jobs.clone(), captured.clone()),
policy.issuer(),
policy.clone(),
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let services = captured.lock().unwrap().clone().unwrap();
assert_eq!(
services
.imports()
.find(|(name, _)| *name == "business")
.unwrap()
.1[0]
.provider()
.instance(),
&support::identity(11)
);
let denied = request(&services, &jobs);
assert!(
denied
.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default()
)
.unwrap()
.await
.unwrap_err()
.contains("not granted")
);
denied.finish().await.unwrap();
app.shutdown().await.unwrap();
drop(app);
let jobs = Arc::new(Jobs::default());
let policy = Arc::new(
PeerPolicy::with_native_grants(vec![grant(support::identity(11), false)]).unwrap(),
);
let app = Application::start_from_store(
store.clone(),
deployment,
native(jobs.clone(), captured.clone()),
policy.issuer(),
policy.clone(),
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let services = captured.lock().unwrap().clone().unwrap();
let job = request(&services, &jobs);
assert_eq!(
job.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default()
)
.unwrap()
.await
.unwrap(),
json!({"effects":0})
);
// Reusing public observation with a Host issuer does not create NativeOrigin.
let claimed = policy
.issuer()
.issue(
Identity::Remote {
request_id: Uuid::new_v4(),
},
CancellationToken::new(),
)
.unwrap();
let binding = app
.client()
.bind(
app.client().activation(&support::identity(13)).unwrap(),
"business".into(),
)
.await
.unwrap()
.pop()
.unwrap();
assert!(
app.client()
.start(
binding,
"read".into(),
json!({}),
claimed.clone(),
Attachments::default().into()
)
.await
.is_err()
);
claimed.finish().await.unwrap();
let operation = Uuid::new_v4();
let mutation = job
.mutate(
"business",
0,
"write".into(),
json!({}),
operation,
Attachments::default(),
)
.unwrap()
.await
.unwrap();
assert!(
matches!(mutation.receipt, Receipt::Recorded { outcome: OperationOutcome::Returned { ref value }, .. } if value == &json!({"effects":1}))
);
assert!(
job.recover(mutation.reference.clone())
.unwrap()
.await
.is_err(),
"mutation admission is not recovery approval"
);
job.finish().await.unwrap();
assert!(
job.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default()
)
.is_err()
);
app.client()
.retire(app.client().activation(&support::identity(13)).unwrap())
.await
.unwrap();
let retired = request(&services, &jobs);
assert!(
retired
.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default()
)
.unwrap()
.await
.is_err(),
"old native origin cannot start work after retirement"
);
retired.finish().await.unwrap();
app.shutdown().await.unwrap();
drop((app, job, services, denied));
store.shutdown().await.unwrap();
}
#[tokio::test]
async fn old_native_bindings_never_redirect_and_recovery_uses_removed_original_provider() {
let (_directory, store) = store().await;
let deployment = Uuid::new_v4();
let (mut input, packages) = support::host();
store
.commit_application(deployment, 0, input.clone(), vec![], packages.clone())
.await
.unwrap();
let captured = Arc::new(Mutex::new(None));
let jobs = Arc::new(Jobs::default());
let policy = Arc::new(
PeerPolicy::with_native_grants(vec![
grant(support::identity(11), true),
grant(support::identity(21), false),
])
.unwrap(),
);
let app = Application::start_from_store(
store.clone(),
deployment,
native(jobs.clone(), captured.clone()),
policy.issuer(),
policy.clone(),
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let original_services = captured.lock().unwrap().clone().unwrap();
let job = request(&original_services, &jobs);
let reply = job
.mutate(
"business",
0,
"write".into(),
json!({}),
Uuid::new_v4(),
Attachments::default(),
)
.unwrap()
.await
.unwrap();
let reference = reply.reference;
job.finish().await.unwrap();
app.shutdown().await.unwrap();
drop((app, job));
input.instances.get_mut("business").unwrap().key = Uuid::from_u128(21);
store
.commit_application(deployment, 1, input, vec![], packages)
.await
.unwrap();
let jobs = Arc::new(Jobs::default());
let app = Application::start_from_store(
store.clone(),
deployment,
native(jobs.clone(), captured.clone()),
policy.issuer(),
policy.clone(),
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let services = captured.lock().unwrap().clone().unwrap();
assert_eq!(
services
.imports()
.find(|(name, _)| *name == "business")
.unwrap()
.1[0]
.provider()
.instance(),
&support::identity(21)
);
let stale = original_services
.request(
Identity::Remote {
request_id: Uuid::new_v4(),
},
CancellationToken::new(),
)
.unwrap();
assert!(
stale
.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default()
)
.is_err()
);
stale.finish().await.unwrap();
let job = request(&services, &jobs);
let old = job
.recover(reference.clone())
.unwrap()
.await
.unwrap()
.unwrap();
assert_eq!(old.reference, reference);
assert!(
matches!(&old.outcome,OperationOutcome::Returned{value} if value==&json!({"effects":1}))
);
assert_eq!(
job.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default()
)
.unwrap()
.await
.unwrap(),
json!({"effects":0}),
"recovery did not execute current replacement"
);
job.finish().await.unwrap();
app.shutdown().await.unwrap();
drop((app, job, stale, services, original_services));
store.shutdown().await.unwrap();
}
struct Blocking {
entered: Arc<tokio::sync::Semaphore>,
release: Arc<tokio::sync::Semaphore>,
}
impl Handler for Blocking {
fn call(&self, _: String, _: Value, _: Context) -> BoxFuture<'static, Result<Value, String>> {
let entered = self.entered.clone();
let release = self.release.clone();
Box::pin(async move {
entered.add_permits(1);
release.acquire_owned().await.unwrap().forget();
Ok(json!({"effects":0}))
})
}
}
impl NativeOwner for Blocking {
fn close(&self) -> BoxFuture<'static, Result<(), String>> {
Box::pin(async { Ok(()) })
}
}
#[tokio::test]
async fn native_owner_close_waits_for_actual_child_cleanup_after_observer_drop() {
tokio::time::timeout(Duration::from_secs(15), async {
let (_directory, store) = store().await;
let deployment = Uuid::new_v4();
let (input, mut packages) = support::host();
let provider = packages
.iter_mut()
.find(|p| p.manifest().id == "local.business")
.unwrap();
let mut declaration = provider
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
declaration
.entries
.get_mut(&EntryTarget::Host)
.unwrap()
.implementation = Implementation::Native {
implementation: "native.blocking".into(),
};
*provider = support::counter::package("local.business", declaration, None);
let mut input = input;
input.instances.get_mut("business").unwrap().package = provider.application_ref().into();
store
.commit_application(deployment, 0, input, vec![], packages)
.await
.unwrap();
let captured = Arc::new(Mutex::new(None));
let jobs = Arc::new(Jobs::default());
let entered = Arc::new(tokio::sync::Semaphore::new(0));
let release = Arc::new(tokio::sync::Semaphore::new(0));
let blocking = Arc::new(Blocking {
entered: entered.clone(),
release: release.clone(),
});
let mut registry = native(jobs.clone(), captured.clone());
registry
.insert(
"native.blocking".into(),
Arc::new(move |_, _| {
let blocking = blocking.clone();
Box::pin(async move {
NativeActivation {
exports: Ok(BTreeMap::from([(
"Counter".into(),
Published {
contract: support::business(),
value: blocking.clone() as Arc<dyn Handler>,
},
)])),
owner: blocking,
}
})
}),
)
.unwrap();
let policy = Arc::new(
PeerPolicy::with_native_grants(vec![grant(support::identity(11), false)]).unwrap(),
);
let app = Application::start_from_store(
store.clone(),
deployment,
registry,
policy.issuer(),
policy.clone(),
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let services = captured.lock().unwrap().clone().unwrap();
let job = request(&services, &jobs);
let observer = job
.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default(),
)
.unwrap();
entered.acquire_owned().await.unwrap().forget();
drop(observer);
let mut close = tokio::spawn(async move {
app.shutdown().await.unwrap();
app
});
assert!(
tokio::time::timeout(Duration::from_millis(30), &mut close)
.await
.is_err(),
"dropping observer or native close cannot acknowledge a blocked child"
);
release.add_permits(1);
let app = close.await.unwrap();
job.finish().await.unwrap();
drop((app, job, services));
store.shutdown().await.unwrap();
})
.await
.unwrap();
}
struct Forward;
impl Handler for Forward {
fn call(
&self,
_: String,
input: Value,
context: Context,
) -> BoxFuture<'static, Result<Value, String>> {
Box::pin(async move {
context
.call("next", 0, "read".into(), input, Attachments::default())?
.await
})
}
}
impl NativeOwner for Forward {
fn close(&self) -> BoxFuture<'static, Result<(), String>> {
Box::pin(async { Ok(()) })
}
}
#[tokio::test]
async fn native_origin_survives_forwarding_but_never_grants_an_unlisted_downstream_edge() {
for allowed in [false, true] {
let (_directory, store) = store().await;
let deployment = Uuid::new_v4();
let (mut input, mut packages) = support::host();
let provider = packages
.iter_mut()
.find(|package| package.manifest().id == "local.business")
.unwrap();
let next_package = provider.clone();
let mut declaration = provider
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
let entry = declaration.entries.get_mut(&EntryTarget::Host).unwrap();
entry.implementation = Implementation::Native {
implementation: "native.forward".into(),
};
entry.imports.insert(
"next".into(),
maka_plugin_core::contract::Import {
contract: support::business(),
cardinality: maka_plugin_core::contract::Cardinality::One,
},
);
*provider = support::counter::package("local.business", declaration, None);
let current = input.instances.get_mut("business").unwrap();
current.package = provider.application_ref().into();
current.bindings.insert(
"next".into(),
vec![maka_plugin_core::composition::Target::Instance {
path: vec!["next".into()],
export: "Counter".into(),
}],
);
input
.instances
.insert("next".into(), support::counter::node(&next_package, 21));
packages.push(next_package);
store
.commit_application(deployment, 0, input, vec![], packages)
.await
.unwrap();
let captured = Arc::new(Mutex::new(None));
let jobs = Arc::new(Jobs::default());
let mut registry = native(jobs.clone(), captured.clone());
registry
.insert(
"native.forward".into(),
Arc::new(|_, _| {
Box::pin(async {
NativeActivation {
exports: Ok(BTreeMap::from([(
"Counter".into(),
Published {
contract: support::business(),
value: Arc::new(Forward) as Arc<dyn Handler>,
},
)])),
owner: Arc::new(Forward),
}
})
}),
)
.unwrap();
let mut grants = vec![grant(support::identity(11), false)];
if allowed {
let mut edge = grant(support::identity(21), false);
edge.consumer = support::identity(11);
grants.push(edge);
}
let policy = Arc::new(PeerPolicy::with_native_grants(grants).unwrap());
let app = Application::start_from_store(
store.clone(),
deployment,
registry,
policy.issuer(),
policy.clone(),
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let services = captured.lock().unwrap().clone().unwrap();
let job = request(&services, &jobs);
let result = job
.call(
"business",
0,
"read".into(),
json!({}),
Attachments::default(),
)
.unwrap()
.await;
if allowed {
assert_eq!(result.unwrap(), json!({"effects":0}));
} else {
assert!(result.unwrap_err().contains("not granted"));
}
job.finish().await.unwrap();
app.shutdown().await.unwrap();
drop((app, job, services));
store.shutdown().await.unwrap();
}
}
struct FailedEffect(Arc<std::sync::atomic::AtomicUsize>);
impl Handler for FailedEffect {
fn call(
&self,
_: String,
input: Value,
context: Context,
) -> BoxFuture<'static, Result<Value, String>> {
self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Box::pin(async move {
if input["cleanup"].as_bool() == Some(true) {
let mut ticket = context.scope().resources.reserve().unwrap();
ticket.start();
ticket.complete(Err("original native effect cleanup unconfirmed".into()));
Ok(json!({"effects":1}))
} else {
Err("reply lost after effect".into())
}
})
}
}
impl NativeOwner for FailedEffect {
fn close(&self) -> BoxFuture<'static, Result<(), String>> {
Box::pin(async { Ok(()) })
}
}
#[tokio::test]
async fn native_requests_preserve_unknown_receipts_and_independent_failed_cleanup() {
for cleanup in [false, true] {
let (_directory, store) = store().await;
let deployment = Uuid::new_v4();
let (mut input, mut packages) = support::host();
let provider = packages
.iter_mut()
.find(|package| package.manifest().id == "local.business")
.unwrap();
let mut declaration = provider
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
declaration
.entries
.get_mut(&EntryTarget::Host)
.unwrap()
.implementation = Implementation::Native {
implementation: "native.failure".into(),
};
*provider = support::counter::package("local.business", declaration, None);
input.instances.get_mut("business").unwrap().package = provider.application_ref().into();
store
.commit_application(deployment, 0, input, vec![], packages)
.await
.unwrap();
let captured = Arc::new(Mutex::new(None));
let jobs = Arc::new(Jobs::default());
let mut registry = native(jobs.clone(), captured.clone());
let effects = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let capture = effects.clone();
registry
.insert(
"native.failure".into(),
Arc::new(move |_, _| {
let failed = Arc::new(FailedEffect(capture.clone()));
Box::pin(async move {
NativeActivation {
exports: Ok(BTreeMap::from([(
"Counter".into(),
Published {
contract: support::business(),
value: failed.clone() as Arc<dyn Handler>,
},
)])),
owner: failed,
}
})
}),
)
.unwrap();
let policy = Arc::new(
PeerPolicy::with_native_grants(vec![grant(support::identity(11), true)]).unwrap(),
);
let app = Application::start_from_store(
store.clone(),
deployment,
registry,
policy.issuer(),
policy.clone(),
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let services = captured.lock().unwrap().clone().unwrap();
let job = request(&services, &jobs);
let operation = Uuid::new_v4();
let reply = job
.mutate(
"business",
0,
"write".into(),
json!({"cleanup":cleanup}),
operation,
Attachments::default(),
)
.unwrap()
.await
.unwrap();
if cleanup {
assert!(matches!(
reply.receipt,
Receipt::Recorded {
outcome: OperationOutcome::Returned { .. },
cleanup: maka_plugin_core::operation::OperationCleanup::Failed { .. }
}
));
assert!(
job.finish()
.await
.unwrap_err()
.contains("cleanup unconfirmed")
);
assert!(app.shutdown().await.is_err());
let deadline = std::time::Instant::now() + Duration::from_secs(3);
let snapshot = loop {
let snapshot = app.client().snapshot();
if snapshot.instances.iter().any(|status| {
status.activation.instance() == &support::identity(13)
&& !status.failures.is_empty()
}) {
break snapshot;
}
assert!(
std::time::Instant::now() < deadline,
"native owner close did not publish original request cleanup failure"
);
tokio::task::yield_now().await;
};
assert!(!snapshot.closed);
assert!(snapshot.cleanup_error.is_some());
assert_eq!(effects.load(std::sync::atomic::Ordering::SeqCst), 1);
// Retain this intentional unconfirmed-cleanup Root; do not delete its
// files underneath the original failed owner during test teardown.
let retained = _directory.keep();
eprintln!(
"retained negative cleanup fixture Root: {}",
retained.display()
);
assert!(
snapshot
.instances
.iter()
.filter(|status| status.activation.instance() == &support::identity(13))
.any(|status| status.resources > 0 && !status.failures.is_empty()),
"original native caller retains its failed owner resource fence"
);
assert!(
snapshot
.instances
.iter()
.any(
|status| status.activation.instance() == &support::identity(11)
&& status.calls > 0
&& !status.failures.is_empty()
),
"original provider retains its failed execution fence"
);
} else {
assert!(matches!(
reply.receipt,
Receipt::Recorded {
outcome: OperationOutcome::Unknown { .. },
cleanup: maka_plugin_core::operation::OperationCleanup::Confirmed
}
));
let repeated = job
.mutate(
"business",
0,
"write".into(),
json!({"cleanup":cleanup}),
operation,
Attachments::default(),
)
.unwrap()
.await
.unwrap();
assert_eq!(
reply, repeated,
"an unknown original operation remains the same receipt"
);
assert_eq!(
effects.load(std::sync::atomic::Ordering::SeqCst),
1,
"historical receipt lookup must not replay an unknown effect"
);
let original = job
.recover(reply.reference.clone())
.unwrap()
.await
.unwrap()
.unwrap();
assert!(matches!(original.outcome, OperationOutcome::Unknown { .. }));
job.finish().await.unwrap();
app.shutdown().await.unwrap();
drop((app, job, services));
store.shutdown().await.unwrap();
}
}
}
#[tokio::test]
async fn native_resource_capture_shares_host_budget_and_cannot_cross_request_scope() {
use maka_capabilities::call::ResourceEncoding;
use maka_host_runtime::application::ResourceDescriptor;
let (_directory, store) = store().await;
let deployment = Uuid::new_v4();
let (input, packages) = support::host();
store
.commit_application(deployment, 0, input, vec![], packages)
.await
.unwrap();
let captured = Arc::new(Mutex::new(None));
let jobs = Arc::new(Jobs::default());
let policy =
Arc::new(PeerPolicy::with_native_grants(vec![grant(support::identity(11), true)]).unwrap());
let app = Application::start_from_store(
store,
deployment,
native(jobs.clone(), captured.clone()),
policy.issuer(),
policy,
Some(maka_host_js::factory(Default::default())),
)
.await
.unwrap();
app.ready().await.unwrap();
let services = captured.lock().unwrap().clone().unwrap();
let original = request(&services, &jobs);
let other = request(&services, &jobs);
let descriptors = |length| {
BTreeMap::from([(
"file".into(),
ResourceDescriptor {
resource: maka_plugin_core::contract::binary_resource_type(),
encoding: ResourceEncoding::Bytes,
length,
metadata: json!({}),
},
)])
};
let receiving = original
.capture_resources(descriptors(64 * 1024 * 1024))
.unwrap();
assert!(
other
.capture_resources(descriptors(1))
.err()
.unwrap()
.contains("budget exhausted")
);
assert!(
app.client()
.capture_resources(other.scope().clone(), descriptors(1))
.err()
.unwrap()
.contains("budget exhausted")
);
drop(receiving);
drop(other.capture_resources(descriptors(1)).unwrap());
let foreign = original
.capture_resources(BTreeMap::new())
.unwrap()
.finish()
.unwrap();
assert!(
other
.mutate(
"business",
0,
"write".into(),
json!({}),
Uuid::new_v4(),
foreign
)
.err()
.unwrap()
.contains("another request Scope")
);
let resources = original
.capture_resources(BTreeMap::new())
.unwrap()
.finish()
.unwrap();
let reply = original
.mutate(
"business",
0,
"write".into(),
json!({}),
Uuid::new_v4(),
resources,
)
.unwrap()
.await
.unwrap();
assert!(
matches!(reply.receipt, Receipt::Recorded {outcome:OperationOutcome::Returned{ref value},cleanup:maka_plugin_core::operation::OperationCleanup::Confirmed} if value==&json!({"effects":1}))
);
let recovered = original
.recover(reply.reference)
.unwrap()
.await
.unwrap()
.unwrap();
assert_eq!(
Receipt::Recorded {
outcome: recovered.outcome,
cleanup: recovered.cleanup
},
reply.receipt
);
original.finish().await.unwrap();
other.finish().await.unwrap();
assert!(original.capture_resources(BTreeMap::new()).is_err());
app.shutdown().await.unwrap();
}