blob: 43b56df1110f66f2cbce738478ed29b7bdcf17a2 [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.
*/
use super::*;
use crate::root::{RootNamespaces, RootOwner};
use maka_plugin_core::composition::{Environment, ExecutionLocation, InstanceId};
use std::sync::Arc;
pub(super) fn input() -> Input {
Input {
environment: Environment {
location: ExecutionLocation::Host,
presentation: None,
},
instances: BTreeMap::new(),
recovery: None,
launch: None,
}
}
pub(super) async fn store() -> (tempfile::TempDir, EventLog) {
let dir = tempfile::tempdir().unwrap();
let namespaces = RootNamespaces {
ownership: dir.path().join("owners"),
control: dir.path().join("control"),
};
let owner = Arc::new(RootOwner::create(&dir.path().join("root"), &namespaces).unwrap());
let store = EventLog::for_root(owner).await.unwrap();
(dir, store)
}
#[tokio::test]
async fn exact_source_cas_and_domain() {
let (_dir, store) = store().await;
let id = Uuid::new_v4();
assert!(store.load_application(id).await.unwrap().is_none());
let first = store
.commit_application(id, 0, input(), vec![], vec![])
.await
.unwrap();
first.validate().unwrap();
assert_eq!(
first.state_domain,
store.root_owner.as_ref().unwrap().root_id()
);
let loaded = store.load_application(id).await.unwrap().unwrap();
assert_eq!(loaded.source.reference, first);
assert_eq!(
serde_json::to_value(loaded.source.input).unwrap(),
serde_json::to_value(input()).unwrap()
);
assert!(matches!(
store
.commit_application(id, 0, input(), vec![], vec![])
.await,
Err(StoreError::RevisionConflict { .. })
));
let second = store
.commit_application(id, 1, input(), vec![], vec![])
.await
.unwrap();
assert_eq!(second.revision, 2);
assert_eq!(
store
.load_application(id)
.await
.unwrap()
.unwrap()
.source
.reference,
second
);
assert_eq!(
store
.load_application_at(&first)
.await
.unwrap()
.unwrap()
.source
.reference,
first
);
let mut absent = first.clone();
absent.revision = 3;
assert!(store.load_application_at(&absent).await.unwrap().is_none());
let mut foreign = first.clone();
foreign.state_domain = "0".repeat(64);
assert!(store.load_application_at(&foreign).await.is_err());
store.close().await.unwrap();
}
#[tokio::test]
async fn malformed_typed_source_is_rejected_when_loading() {
let (_dir, store) = store().await;
let id = Uuid::new_v4();
store
.commit_application(id, 0, input(), vec![], vec![])
.await
.unwrap();
store.connection.run(move |conn| Box::pin(async move {
sqlx::query("UPDATE application_sources SET input_json = json_object('environment', 'invalid') WHERE deployment = ?")
.bind(id.to_string()).execute(conn).await?;
Ok(())
})).await.unwrap();
let error = store.load_application(id).await.err().unwrap().to_string();
assert!(error.contains("invalid type"), "{error}");
store.close().await.unwrap();
}
#[tokio::test]
async fn requires_real_state_root_and_non_nil_identity() {
let dir = tempfile::tempdir().unwrap();
let unowned = EventLog::open(&dir.path().join("test.sqlite"))
.await
.unwrap();
assert!(
unowned
.commit_application(Uuid::new_v4(), 0, input(), vec![], vec![])
.await
.is_err()
);
unowned.close().await.unwrap();
let (_dir, owned) = store().await;
assert!(
owned
.commit_application(Uuid::nil(), 0, input(), vec![], vec![])
.await
.is_err()
);
assert!(
owned
.commit_application(
Uuid::new_v4(),
9_007_199_254_740_991,
input(),
vec![],
vec![]
)
.await
.is_err()
);
owned.close().await.unwrap();
}
fn package(marker: &str) -> Package {
use maka_plugin_core::package::{
Declaration, EntryDeclaration, EntryTarget, Implementation, Realm,
};
let declaration = Declaration {
entries: BTreeMap::from([(
EntryTarget::Host,
EntryDeclaration {
scene: None,
implementation: Implementation::JavaScript {
module: "host.mjs".into(),
realm: Realm::Shared,
},
configuration: serde_json::json!({"type":"object"}).into(),
imports: BTreeMap::new(),
exports: BTreeMap::new(),
},
)]),
composition: None,
};
Package::new(BTreeMap::from([
(
maka_packages::MANIFEST_FILE.into(),
serde_json::to_vec(
&serde_json::json!({"schemaVersion":1,"id":"sample","application":declaration}),
)
.unwrap(),
),
("host.mjs".into(), marker.as_bytes().to_vec()),
]))
.unwrap()
}
fn selected(package: &Package) -> Input {
use maka_plugin_core::composition::{Configuration, Node, PackageUse};
let mut input = input();
input.instances.insert(
"view".into(),
Node {
key: Uuid::from_u128(1),
package: package.application_ref().into(),
use_package: PackageUse::Entry,
configuration: Configuration::default(),
parameters: Configuration::default(),
bindings: BTreeMap::new(),
disabled: false,
overrides: vec![],
},
);
input
}
#[tokio::test]
async fn immutable_bytes_preserve_every_committed_source_revision() {
let (_dir, store) = store().await;
let a = Uuid::new_v4();
let b = Uuid::new_v4();
let old = package("export default {}; // original");
let new = package("export default {}; // replacement");
let original = store
.commit_application(a, 0, selected(&old), vec![], vec![old.clone()])
.await
.unwrap();
store
.commit_application(b, 0, selected(&old), vec![], vec![old.clone()])
.await
.unwrap();
store
.commit_application(a, 1, selected(&new), vec![], vec![new.clone()])
.await
.unwrap();
assert_eq!(
store.load_application(b).await.unwrap().unwrap().packages[0].files(),
old.files()
);
assert_eq!(
store.load_application(a).await.unwrap().unwrap().packages[0].files(),
new.files()
);
store
.commit_application(b, 1, input(), vec![], vec![])
.await
.unwrap();
let count = store
.connection
.run(|conn| {
Box::pin(async move {
Ok(
sqlx::query_scalar::<_, i64>("SELECT count(*) FROM application_packages")
.fetch_one(conn)
.await?,
)
})
})
.await
.unwrap();
assert_eq!(count, 2);
assert_eq!(
store
.load_application_at(&original)
.await
.unwrap()
.unwrap()
.packages[0]
.files(),
old.files()
);
store.close().await.unwrap();
}
#[tokio::test]
async fn corrupted_exact_bytes_cannot_be_loaded_or_silently_overwritten() {
let (_dir, store) = store().await;
let id = Uuid::new_v4();
let package = package("export default {};");
store
.commit_application(id, 0, selected(&package), vec![], vec![package.clone()])
.await
.unwrap();
store
.connection
.run(|conn| {
Box::pin(async move {
sqlx::query(
"UPDATE application_package_files SET payload = X'00' WHERE path = 'host.mjs'",
)
.execute(conn)
.await?;
Ok(())
})
})
.await
.unwrap();
assert!(
store
.load_application(id)
.await
.err()
.unwrap()
.to_string()
.contains("digest mismatch")
);
assert!(
store
.commit_application(id, 1, selected(&package), vec![], vec![package])
.await
.is_err()
);
let revision = store
.connection
.run(move |conn| {
Box::pin(async move {
Ok(sqlx::query_scalar::<_, i64>(
"SELECT revision FROM application_deployments WHERE id = ?",
)
.bind(id.to_string())
.fetch_one(conn)
.await?)
})
})
.await
.unwrap();
assert_eq!(revision, 1);
store.close().await.unwrap();
}
#[tokio::test]
async fn stored_package_paths_types_and_immutable_digests_are_checked() {
for corruption in [
"UPDATE application_package_files SET path = '../escape' WHERE path='host.mjs'",
"UPDATE application_package_files SET payload = zeroblob(8388609) WHERE path='host.mjs'",
"UPDATE application_package_files SET payload = 'x' || char(0) || printf('%.*c', 8388609, 'z') WHERE path='host.mjs'",
] {
let (_dir, store) = store().await;
let id = Uuid::new_v4();
let package = package("export default {};");
store
.commit_application(id, 0, selected(&package), vec![], vec![package])
.await
.unwrap();
store
.connection
.run(move |conn| {
Box::pin(async move {
sqlx::query(corruption).execute(conn).await?;
Ok(())
})
})
.await
.unwrap();
let error = store.load_application(id).await.err().unwrap().to_string();
if corruption.contains("char(0)") {
assert!(error.contains("payload must be a BLOB"), "{error}");
} else if corruption.contains("zeroblob") {
assert!(error.contains("digest mismatch"), "{error}");
} else {
assert!(error.contains("invalid package path"), "{error}");
}
store.close().await.unwrap();
}
}
#[tokio::test]
async fn concurrent_cas_has_one_commit_and_reopen_keeps_original_domain() {
let (dir, store) = store().await;
let id = Uuid::new_v4();
let (a, b) = tokio::join!(
store.commit_application(id, 0, input(), vec![], vec![]),
store.commit_application(id, 0, input(), vec![], vec![])
);
assert_ne!(a.is_ok(), b.is_ok());
let source = a.or(b).unwrap();
store.close().await.unwrap();
let namespaces = RootNamespaces {
ownership: dir.path().join("owners"),
control: dir.path().join("control"),
};
let owner = Arc::new(RootOwner::open(&dir.path().join("root"), &namespaces).unwrap());
let store = EventLog::for_root(owner).await.unwrap();
assert_eq!(
store
.load_application(id)
.await
.unwrap()
.unwrap()
.source
.reference,
source
);
store.close().await.unwrap();
}
#[tokio::test]
async fn duplicate_package_selection_and_missing_exact_revision_commit_nothing() {
let (_dir, store) = store().await;
let id = Uuid::new_v4();
let package = package("export default {};");
let error = store
.commit_application(
id,
0,
selected(&package),
vec![],
vec![package.clone(), package.clone()],
)
.await
.err()
.unwrap()
.to_string();
assert!(
error.contains("duplicate application package revision"),
"{error}"
);
assert!(store.load_application(id).await.unwrap().is_none());
assert!(
store
.commit_application(id, 0, selected(&package), vec![], vec![])
.await
.is_err()
);
assert!(store.load_application(id).await.unwrap().is_none());
store.close().await.unwrap();
}
#[tokio::test]
async fn application_packages_have_no_aggregate_byte_limit() {
let (_dir, store) = store().await;
let id = Uuid::new_v4();
let mut source = input();
let mut packages = Vec::new();
for index in 0..17u128 {
let package = package(
&char::from(b'a' + index as u8)
.to_string()
.repeat(8 * 1024 * 1024),
);
let mut node = selected(&package).instances.remove("view").unwrap();
node.key = Uuid::from_u128(index + 1);
source.instances.insert(format!("plugin{index}"), node);
packages.push(package);
}
let reference = store
.commit_application(id, 0, source, vec![], packages)
.await
.unwrap();
let loaded = store.load_application(id).await.unwrap().unwrap();
assert_eq!(loaded.source.reference, reference);
assert_eq!(loaded.packages.len(), 17);
assert!(
loaded
.packages
.iter()
.map(|package| package.files().values().map(Vec::len).sum::<usize>())
.sum::<usize>()
> 128 * 1024 * 1024
);
store.close().await.unwrap();
}
#[tokio::test]
async fn metadata_snapshot_never_materializes_package_payloads() {
let (_dir, store) = store().await;
let package = package(&"x".repeat(8 * 1024 * 1024));
let reference = package.application_ref();
let deployment = Uuid::new_v4();
let source = store
.commit_application(deployment, 0, selected(&package), vec![], vec![package])
.await
.unwrap();
let metadata = store
.read_application_metadata(deployment)
.await
.unwrap()
.unwrap();
assert_eq!(metadata.source.reference, source);
assert_eq!(metadata.packages, vec![reference]);
let mut empty = metadata.source.input.clone();
empty.instances.clear();
let next = store
.commit_application(deployment, 1, empty, vec![], vec![])
.await
.unwrap();
assert!(
store
.read_application_metadata_at(&next)
.await
.unwrap()
.unwrap()
.packages
.is_empty()
);
let original = serde_json::to_vec(&metadata).unwrap();
assert!(original.len() < 4096);
// A payload that cannot be loaded still must not enter metadata reads. This
// tests the physical DAO boundary, not a profiler estimate or source string.
store.connection.run(|connection| Box::pin(async move {
sqlx::query("UPDATE application_package_files SET payload = zeroblob(8388609) WHERE path='host.mjs'").execute(connection).await?;
Ok(())
})).await.unwrap();
assert_eq!(
serde_json::to_vec(
&store
.read_application_metadata_at(&source)
.await
.unwrap()
.unwrap()
)
.unwrap(),
original
);
assert!(store.load_application_at(&source).await.is_err());
assert_eq!(
store
.load_application(deployment)
.await
.unwrap()
.unwrap()
.source
.reference,
next
);
assert!(store.read_application_metadata(Uuid::nil()).await.is_err());
assert!(
store
.read_application_metadata(Uuid::new_v4())
.await
.unwrap()
.is_none()
);
store.close().await.unwrap();
}
#[tokio::test]
async fn checked_deployment_cannot_cross_root_owners() {
let (_first_dir, first) = store().await;
let (_other_dir, other) = store().await;
let id = Uuid::new_v4();
first
.commit_application(id, 0, input(), vec![], vec![])
.await
.unwrap();
let loaded = first.load_application(id).await.unwrap().unwrap();
loaded.check_store(&first).unwrap();
assert!(loaded.check_store(&other).is_err());
first.drain().await;
drop(first);
assert!(
loaded
.check_store(&other)
.unwrap_err()
.to_string()
.contains("original source Root has closed")
);
other.drain().await;
}
#[tokio::test]
async fn prepared_commit_preserves_exact_source_and_cas() {
let (_directory, store) = store().await;
let selected_package = package("export const value = 'selected';");
let extra_package = package("export const value = 'unused';");
let original = selected(&selected_package);
let prepared = prepare_applications(
&[(&original, &[])],
&[selected_package.clone(), extra_package],
)
.unwrap()
.pop()
.unwrap();
assert_eq!(prepared.packages().len(), 1);
assert_eq!(prepared.packages()[0].files(), selected_package.files());
let id = Uuid::new_v4();
let committed = store
.commit_prepared_application(id, 0, prepared)
.await
.unwrap();
committed.check_store(&store).unwrap();
assert_eq!(committed.source().reference.revision, 1);
assert_eq!(
serde_json::to_value(&committed.source().input).unwrap(),
serde_json::to_value(&original).unwrap()
);
let persisted = store.load_application(id).await.unwrap().unwrap();
assert_eq!(persisted.source().reference, committed.source().reference);
assert_eq!(
persisted.packages()[0].files(),
committed.packages()[0].files()
);
assert_eq!(
persisted.plan().instances().len(),
committed.plan().instances().len()
);
let stale = PreparedApplication::new(original, vec![], vec![selected_package]).unwrap();
assert!(matches!(
store.commit_prepared_application(id, 0, stale).await,
Err(StoreError::RevisionConflict { .. })
));
assert_eq!(
store
.read_application_metadata(id)
.await
.unwrap()
.unwrap()
.source
.reference
.revision,
1
);
store.drain().await;
}
#[tokio::test]
async fn prepared_commit_snapshot_cannot_cross_reopened_root_lease() {
let (directory, original) = store().await;
let id = Uuid::new_v4();
let prepared = PreparedApplication::new(input(), vec![], vec![]).unwrap();
let loaded = original
.commit_prepared_application(id, 0, prepared)
.await
.unwrap();
loaded.check_store(&original).unwrap();
original.drain().await;
drop(original);
let namespaces = RootNamespaces {
ownership: directory.path().join("owners"),
control: directory.path().join("control"),
};
let reopened = EventLog::for_root(Arc::new(
RootOwner::open(&directory.path().join("root"), &namespaces).unwrap(),
))
.await
.unwrap();
assert_eq!(
reopened.owned_root().unwrap().root_id(),
loaded.source().reference.state_domain
);
assert!(loaded.check_store(&reopened).is_err());
reopened
.load_application(id)
.await
.unwrap()
.unwrap()
.check_store(&reopened)
.unwrap();
reopened.drain().await;
}
#[tokio::test]
async fn selected_shared_package_pool_preserves_complete_source_and_original_selection() {
let (_directory, store) = store().await;
let first = package("export default 'first';");
let second = package("export default 'second';");
let mut source = selected(&first);
source
.instances
.get_mut("view")
.unwrap()
.configuration
.values
.insert(
"padding".into(),
serde_json::json!("甲🙂z".repeat((2 * 1024 * 1024 + 128 * 1024) / 8)),
);
validate_application(&source, &[], std::slice::from_ref(&first)).unwrap();
let pool = vec![first.clone(), second.clone()];
// Ordinary preparation retains all explicitly supplied packages, while the
// shared-pool path below selects only the packages used by each source.
let complete = PreparedApplication::new(source.clone(), vec![], pool.clone()).unwrap();
assert_eq!(complete.packages().len(), 2);
let other = selected(&second);
let mut prepared = prepare_applications(&[(&source, &[]), (&other, &[])], &pool).unwrap();
assert_eq!(prepared[0].packages().len(), 1);
assert_eq!(
prepared[0].packages()[0].application_ref(),
first.application_ref()
);
assert_eq!(
prepared[1].packages()[0].application_ref(),
second.application_ref()
);
let id = Uuid::new_v4();
let committed = store
.commit_prepared_application(id, 0, prepared.remove(0))
.await
.unwrap();
let loaded = store.load_application(id).await.unwrap().unwrap();
assert_eq!(loaded.source().reference, committed.source().reference);
assert!(
loaded.source().input == source,
"complete source changed during commit"
);
assert_eq!(loaded.packages().len(), 1);
// Recovery decodes all immutable bytes again and retains each source's
// original package selection from the shared pool.
let recovered_pool: Vec<_> = pool
.iter()
.map(|package| Package::from_bundle(&package.to_bundle()).unwrap())
.collect();
let recovered =
prepare_applications(&[(&source, &[]), (&other, &[])], &recovered_pool).unwrap();
assert!(
recovered[0].input() == &source,
"recovery lost the complete original source"
);
assert_eq!(
recovered[0].packages()[0].files(),
loaded.packages()[0].files()
);
assert_eq!(
store
.read_application_metadata(id)
.await
.unwrap()
.unwrap()
.source
.reference
.revision,
1
);
store.drain().await;
}
#[tokio::test]
async fn complete_source_over_two_mib_survives_commit_and_original_root_reopen() {
let (directory, store) = store().await;
let package = package("export default {}; // complete source");
let mut expected = selected(&package);
let padding = "甲🙂z".repeat(3 * 1024 * 1024 / 8);
expected
.instances
.get_mut("view")
.unwrap()
.configuration
.values
.insert("padding".into(), serde_json::json!(padding));
let deployment = Uuid::new_v4();
let committed = store
.commit_application(
deployment,
0,
expected.clone(),
vec![],
vec![package.clone()],
)
.await;
store.shutdown().await.unwrap();
drop(store);
// Close the original owner before checking the commit and acquiring a new lease.
assert!(
committed.is_ok(),
"complete source commit failed: {committed:?}"
);
let original = committed.unwrap();
let namespaces = RootNamespaces {
ownership: directory.path().join("owners"),
control: directory.path().join("control"),
};
let reopened = EventLog::for_root(Arc::new(
RootOwner::open(&directory.path().join("root"), &namespaces).unwrap(),
))
.await
.unwrap();
let loaded = reopened.load_application_at(&original).await;
let metadata = reopened.read_application_metadata_at(&original).await;
let stale = reopened
.commit_application(
deployment,
0,
expected.clone(),
vec![],
vec![package.clone()],
)
.await;
let mut foreign = original.clone();
foreign.state_domain = "0".repeat(64);
let foreign_rejected = reopened.load_application_at(&foreign).await.is_err();
reopened.shutdown().await.unwrap();
assert_eq!(original.deployment, deployment);
assert_eq!(original.revision, 1);
let loaded = loaded.unwrap().unwrap();
assert_eq!(loaded.source().reference, original);
assert!(
loaded.source().input == expected,
"reopened source lost original configuration bytes"
);
assert!(loaded.source().remote_exports.is_empty());
assert_eq!(loaded.packages()[0].files(), package.files());
assert_eq!(loaded.plan().instances().len(), 1);
assert!(
loaded.plan().instances()[&InstanceId(vec![Uuid::from_u128(1)])].configuration["padding"]
== serde_json::json!(padding),
"reopened plan lost complete configuration"
);
let metadata = metadata.unwrap().unwrap();
assert_eq!(metadata.source.reference, original);
assert!(
metadata.source.input == expected,
"metadata read lost original configuration bytes"
);
assert!(matches!(stale, Err(StoreError::RevisionConflict { .. })));
assert!(foreign_rejected);
}