feat: unify object observation API

This commit is contained in:
2026-09-30 18:19:12 +09:00
parent 2e4196a1ec
commit ea7b0f00ea
32 changed files with 1035 additions and 1251 deletions
+15 -16
View File
@@ -68,29 +68,29 @@ The following examples use development HTTP for readability. The JSON is the
standard `wip-https` profile; validators and operation `Bytes` are base64 body
fields, never `ETag` or `If-Match` headers.
Fetch root:
Observe only the root Object:
```console
curl -sS -X POST http://127.0.0.1:8080/wip/v1/fetch \
curl -sS -X POST http://127.0.0.1:8080/wip/v1/observe \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{"path":"/"}'
--data '{"path":"/","depth":0}'
```
Fetch a bounded tree (the root counts toward `limit`):
Observe the root and two levels of indexable children:
```console
curl -sS -X POST http://127.0.0.1:8080/wip/v1/fetch_tree \
curl -sS -X POST http://127.0.0.1:8080/wip/v1/observe \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{"path":"/","depth":2,"limit":100}'
--data '{"path":"/","depth":2}'
```
A regular-file fetch returns the opaque read interface reference and an object
A regular-file observation returns the opaque read interface reference and an object
validator. Retrieve that descriptor with:
```console
curl -sS -X POST http://127.0.0.1:8080/wip/v1/fetch_interface \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{"interface":"<read-interface-from-fetch>"}'
--data '{"interface":"<read-interface-from-observation>"}'
```
Then call a bounded range read:
@@ -120,8 +120,8 @@ curl -sS -X POST http://127.0.0.1:8080/wip/v1/call_operation \
}'
```
`fetch`, `fetch_tree`, and `fetch_interface` can also be combined through the
standard `POST /wip/v1/batch` route. `call_operation` is never batchable.
`observe` and `fetch_interface` can also be combined through the standard
`POST /wip/v1/batch` route. `call_operation` is never batchable.
## Resource and failure behavior
@@ -133,12 +133,11 @@ stalled clients are closed and cannot occupy a worker beyond that bound. Workers
accept connections directly, so the listener never moves more than the configured
concurrency into active userspace service. Advisory file-lock acquisition uses a
nonblocking loop and checks the same deadline/cancellation rather than allowing a
contended lock to stall a worker. Tree traversal is breadth-first and opens only
requested entries. Once a limit or depth boundary is reached it reads only far
enough to
prove whether an additional visible child exists, so `truncated` is true only
when something was actually omitted. I/O failure and cancellation remain errors,
not clean exhaustion.
contended lock to stall a worker. Object observation recursively opens only the
requested depth. A successful response is complete through that depth; if the
configured projected-node budget would be exceeded, the interaction fails with
`ResourceLimitExceeded` instead of returning a partial observation. I/O failure
and cancellation remain errors.
Whole-file replacement compares the request validator against the identity and
state of the same pinned file handle used for the write. Failures before mutation
+9 -9
View File
@@ -51,10 +51,10 @@ pub struct ResourceLimits {
pub max_file_read_bytes: usize,
/// Maximum bytes accepted by one file replacement.
pub max_file_write_bytes: usize,
/// Maximum accepted `fetch_tree` depth.
pub max_tree_depth: u32,
/// Maximum accepted `fetch_tree` node count, including its root.
pub max_tree_nodes: u32,
/// Maximum accepted `observe` depth.
pub max_observation_depth: u32,
/// Maximum accepted `observe` node count, including its root.
pub max_observation_nodes: u32,
/// Maximum simultaneously pinned projected filesystem handles.
pub max_open_handles: usize,
/// Maximum bytes accepted for one HTTP request head, including its terminator.
@@ -72,8 +72,8 @@ impl Default for ResourceLimits {
.expect("default wire limits are nonzero"),
max_file_read_bytes: 1024 * 1024,
max_file_write_bytes: 1024 * 1024,
max_tree_depth: 32,
max_tree_nodes: 1024,
max_observation_depth: 32,
max_observation_nodes: 1024,
max_open_handles: 2048,
max_http_header_bytes: 16 * 1024,
max_concurrent_requests: 32,
@@ -125,7 +125,7 @@ impl ServerConfig {
}
if self.limits.max_file_read_bytes == 0
|| self.limits.max_file_write_bytes == 0
|| self.limits.max_tree_nodes == 0
|| self.limits.max_observation_nodes == 0
|| self.limits.max_open_handles == 0
|| self.limits.max_http_header_bytes == 0
|| self.limits.max_concurrent_requests == 0
@@ -133,7 +133,7 @@ impl ServerConfig {
{
return Err(ConfigError::ZeroLimit);
}
if self.limits.max_open_handles < self.limits.max_tree_nodes as usize {
if self.limits.max_open_handles < self.limits.max_observation_nodes as usize {
return Err(ConfigError::InsufficientHandles);
}
if self.limits.wire.max_response_bytes() < 512 {
@@ -171,7 +171,7 @@ impl std::fmt::Display for ConfigError {
"endpoint base path must be absolute, canonical, and query-free"
}
Self::ZeroLimit => "all resource limits and the request timeout must be nonzero",
Self::InsufficientHandles => "max_open_handles must cover max_tree_nodes",
Self::InsufficientHandles => "max_open_handles must cover max_observation_nodes",
Self::ResponseLimitTooSmall => "max_response_bytes must be at least 512",
})
}
+102 -110
View File
@@ -1,4 +1,3 @@
use std::collections::VecDeque;
use std::ffi::CStr;
use std::mem::MaybeUninit;
use std::os::fd::{AsRawFd, OwnedFd};
@@ -17,8 +16,8 @@ use wip_host::{
};
use wip_interface as wip;
use wip_protocol::{
FetchInterfaceRequest, FetchInterfaceResponse, FetchTreeRequest, FetchTreeResponse, Object,
ObjectTreeNode, ProtocolError, ProtocolErrorCode,
FetchInterfaceRequest, FetchInterfaceResponse, ObjectObservation, ObserveRequest,
ObserveResponse, ProtocolError, ProtocolErrorCode,
};
use crate::interfaces::{
@@ -223,37 +222,60 @@ impl Projection {
.protocol_response()
}
pub(crate) fn fetch_tree(
pub(crate) fn observe(
&self,
context: &ExecutionContext,
request: &FetchTreeRequest,
) -> Result<FetchTreeResponse, ProtocolError> {
request: &ObserveRequest,
) -> Result<ObserveResponse, ProtocolError> {
request.validate().map_err(|_| ProtocolError {
code: ProtocolErrorCode::InvalidRequest,
message: "invalid fetch_tree request".into(),
message: "invalid observe request".into(),
})?;
if request.depth > self.limits.max_tree_depth || request.limit > self.limits.max_tree_nodes
{
if request.depth > self.limits.max_observation_depth {
return Err(ProjectionError::Resource.protocol());
}
let root = self
.resolve(context, &request.path)
.map_err(ProjectionError::protocol)?;
let root_object = self
.publication(root.clone(), context.clone())
.protocol_object();
let mut objects = vec![root_object];
let mut children: Vec<Vec<usize>> = vec![Vec::new()];
let mut queue = VecDeque::from([(0_usize, root, 0_u32)]);
let mut truncated = false;
let mut node_count = 0_u32;
let response = self.observe_entry(context, root, 0, request.depth, &mut node_count)?;
request
.validate_response(&response)
.map_err(|_| ProtocolError {
code: ProtocolErrorCode::Internal,
message: "Host produced an invalid observation response".into(),
})?;
Ok(response)
}
while let Some((parent_index, parent, depth)) = queue.pop_front() {
check_context(context).map_err(ProjectionError::protocol)?;
if parent.kind != EntryKind::Directory {
continue;
}
let directory_fd = reopen(parent.fd(), OFlags::RDONLY | OFlags::DIRECTORY)
fn observe_entry(
&self,
context: &ExecutionContext,
entry: Entry,
depth: u32,
maximum_depth: u32,
node_count: &mut u32,
) -> Result<ObjectObservation, ProtocolError> {
check_context(context).map_err(ProjectionError::protocol)?;
*node_count = node_count.saturating_add(1);
if *node_count > self.limits.max_observation_nodes {
return Err(ProjectionError::Resource.protocol());
}
let object = self
.publication(entry.clone(), context.clone())
.protocol_object();
if depth == maximum_depth {
return Ok(ObjectObservation {
object,
children: None,
});
}
let mut children = Vec::new();
if entry.kind == EntryKind::Directory {
let directory_fd = reopen(entry.fd(), OFlags::RDONLY | OFlags::DIRECTORY)
.map_err(ProjectionError::protocol)?;
let mut buffer = [MaybeUninit::<u8>::uninit(); 8192];
let mut directory = RawDir::new(directory_fd, &mut buffer);
@@ -271,50 +293,25 @@ impl Projection {
if name.contains('/') || name.is_empty() || name == "." || name == ".." {
continue;
}
let child = match self.open_child(context, parent.fd(), name) {
let child = match self.open_child(context, entry.fd(), name) {
Ok(child) => child,
Err(ProjectionError::NotFound) => continue,
Err(error) => return Err(error.protocol()),
};
if depth == request.depth || objects.len() == request.limit as usize {
truncated = true;
break;
}
let child_index = objects.len();
objects.push(
self.publication(child.clone(), context.clone())
.protocol_object(),
);
children.push(Vec::new());
children[parent_index].push(child_index);
if child.kind == EntryKind::Directory {
queue.push_back((child_index, child, depth + 1));
}
}
if truncated && objects.len() == request.limit as usize {
break;
children.push(self.observe_entry(
context,
child,
depth + 1,
maximum_depth,
node_count,
)?);
}
}
fn build(index: usize, objects: &[Object], children: &[Vec<usize>]) -> ObjectTreeNode {
ObjectTreeNode {
object: objects[index].clone(),
children: children[index]
.iter()
.map(|child| build(*child, objects, children))
.collect(),
}
}
let response = FetchTreeResponse {
root: build(0, &objects, &children),
truncated,
};
response.validate_for(request).map_err(|_| ProtocolError {
code: ProtocolErrorCode::Internal,
message: "Host produced an invalid tree response".into(),
})?;
Ok(response)
Ok(ObjectObservation {
object,
children: Some(children),
})
}
fn publication(
@@ -809,7 +806,7 @@ mod tests {
use wip_host::{
CallErrorKind, CallRequest, ExecutionContext, InterfaceTarget, ObjectProvider, Target,
};
use wip_protocol::{FetchTreeRequest, ProtocolErrorCode, Value};
use wip_protocol::{ObserveRequest, ProtocolErrorCode, Value};
use super::{FailureInjection, FlockOperation, Projection};
use crate::{AccessMode, ResourceLimits};
@@ -850,23 +847,29 @@ mod tests {
.expect("create symlink");
let projection = projection(temporary.path(), AccessMode::ReadOnly);
let root = projection.fetch("/").expect("fetch root");
let root = projection.observe_object("/").expect("observe root");
assert_eq!(root.name, "");
assert_eq!(root.description.as_deref(), Some("Filesystem directory"));
assert!(root.interfaces.is_empty());
assert!(root.validator.is_none());
let file = projection.fetch("/file.txt").expect("fetch file");
let file = projection
.observe_object("/file.txt")
.expect("observe file");
assert_eq!(file.interfaces.len(), 1);
assert!(file.validator.is_some());
assert!(file.r#ref.is_none());
let directory = projection.fetch("/dir").expect("fetch directory");
let directory = projection
.observe_object("/dir")
.expect("observe directory");
assert!(directory.interfaces.is_empty());
#[cfg(unix)]
{
let link = projection.fetch("/link").expect("fetch link itself");
let link = projection
.observe_object("/link")
.expect("observe link itself");
assert_eq!(
link.description.as_deref(),
Some("Symbolic link (not followed)")
@@ -874,7 +877,7 @@ mod tests {
assert_eq!(link.interfaces.len(), 1);
assert_eq!(
projection
.fetch("/link/shadow")
.observe_object("/link/shadow")
.expect_err("must not follow")
.kind(),
CallErrorKind::NotFound
@@ -892,18 +895,21 @@ mod tests {
}
assert_eq!(
projection.fetch("/missing").expect_err("missing").kind(),
projection
.observe_object("/missing")
.expect_err("missing")
.kind(),
CallErrorKind::NotFound
);
}
#[test]
fn reads_are_bounded_and_tree_truncation_is_exact() {
fn reads_are_bounded_and_observation_is_complete() {
let temporary = tempdir().expect("temporary root");
fs::write(temporary.path().join("a"), b"abcdef").expect("write file");
fs::write(temporary.path().join("b"), b"b").expect("write file");
let projection = projection(temporary.path(), AccessMode::ReadOnly);
let file = projection.fetch("/a").expect("fetch file");
let file = projection.observe_object("/a").expect("observe file");
let result = call(
&projection,
"/a",
@@ -919,30 +925,17 @@ mod tests {
assert_eq!(result.result, Value::Bytes(b"cde".to_vec()));
let complete = projection
.fetch_tree(
.observe(
&ExecutionContext::new(),
&FetchTreeRequest {
&ObserveRequest {
path: "/".into(),
depth: 1,
limit: 3,
},
)
.expect("complete tree");
assert!(!complete.truncated);
assert_eq!(complete.root.children.len(), 2);
let limited = projection
.fetch_tree(
&ExecutionContext::new(),
&FetchTreeRequest {
path: "/".into(),
depth: 1,
limit: 2,
},
)
.expect("limited tree");
assert!(limited.truncated);
assert_eq!(limited.root.children.len(), 1);
.expect("complete observation");
let children = complete.children.as_ref().expect("root children observed");
assert_eq!(children.len(), 2);
assert!(children.iter().all(|child| child.children.is_none()));
}
#[test]
@@ -951,7 +944,7 @@ mod tests {
let path = temporary.path().join("file");
fs::write(&path, b"old").expect("write file");
let projection = projection(temporary.path(), AccessMode::ReadWrite);
let observed = projection.fetch("/file").expect("fetch file");
let observed = projection.observe_object("/file").expect("observe file");
assert_eq!(observed.interfaces.len(), 2);
let write_interface = observed.interfaces[1].clone();
@@ -981,7 +974,9 @@ mod tests {
assert_eq!(stale.kind(), CallErrorKind::ValidatorMismatch);
assert_eq!(fs::read(&path).expect("read replacement"), b"replacement");
let current = projection.fetch("/file").expect("fetch replacement");
let current = projection
.observe_object("/file")
.expect("observe replacement");
let result = call(
&projection,
"/file",
@@ -1001,7 +996,7 @@ mod tests {
let path = temporary.path().join("file");
fs::write(&path, b"old").expect("write file");
let projection = projection(temporary.path(), AccessMode::ReadWrite);
let observed = projection.fetch("/file").expect("fetch file");
let observed = projection.observe_object("/file").expect("observe file");
let request = || {
call(
&projection,
@@ -1029,25 +1024,23 @@ mod tests {
}
#[test]
fn large_directory_stops_after_proving_truncation() {
fn large_directory_fails_without_partial_observation() {
let temporary = tempdir().expect("temporary root");
for index in 0..2_000 {
fs::write(temporary.path().join(format!("entry-{index}")), b"x")
.expect("write directory entry");
}
let projection = projection(temporary.path(), AccessMode::ReadOnly);
let tree = projection
.fetch_tree(
let error = projection
.observe(
&ExecutionContext::new(),
&FetchTreeRequest {
&ObserveRequest {
path: "/".into(),
depth: 1,
limit: 2,
},
)
.expect("bounded tree");
assert!(tree.truncated);
assert_eq!(tree.root.children.len(), 1);
.expect_err("complete observation exceeds node budget");
assert_eq!(error.code, ProtocolErrorCode::ResourceLimitExceeded);
}
#[cfg(unix)]
@@ -1065,22 +1058,22 @@ mod tests {
let projection = projection(temporary.path(), AccessMode::ReadOnly);
let tree = projection
.fetch_tree(
.observe(
&ExecutionContext::new(),
&FetchTreeRequest {
&ObserveRequest {
path: "/".into(),
depth: 1,
limit: 10,
},
)
.expect("list root");
assert_eq!(tree.root.children.len(), 1);
assert_eq!(tree.root.children[0].object.name, "socket");
let children = tree.children.as_ref().expect("root children observed");
assert_eq!(children.len(), 1);
assert_eq!(children[0].object.name, "socket");
assert_eq!(
tree.root.children[0].object.description.as_deref(),
children[0].object.description.as_deref(),
Some("Unsupported filesystem entry")
);
assert!(tree.root.children[0].object.interfaces.is_empty());
assert!(children[0].object.interfaces.is_empty());
}
#[test]
@@ -1091,12 +1084,11 @@ mod tests {
let cancellation = wip_host::CancellationToken::new();
cancellation.cancel();
let error = projection
.fetch_tree(
.observe(
&ExecutionContext::new().with_cancellation(cancellation),
&FetchTreeRequest {
&ObserveRequest {
path: "/".into(),
depth: 1,
limit: 2,
},
)
.expect_err("cancelled traversal");
@@ -1108,7 +1100,7 @@ mod tests {
let temporary = tempdir().expect("temporary root");
let projection = projection(temporary.path(), AccessMode::ReadOnly);
let error = projection
.fetch("/bad\0name")
.observe_object("/bad\0name")
.expect_err("NUL cannot name a backend entry");
assert_eq!(error.kind(), CallErrorKind::NotFound);
}
@@ -1144,7 +1136,7 @@ mod tests {
};
let projection = Projection::open(temporary.path(), AccessMode::ReadOnly, limits)
.expect("open projection");
let observed = projection.fetch("/file").expect("fetch file");
let observed = projection.observe_object("/file").expect("observe file");
let started = std::time::Instant::now();
let error = call(
&projection,
+19 -45
View File
@@ -10,12 +10,10 @@ use wip_host::{ExecutionContext, ObjectProvider};
use wip_https::{
BatchRequestKind, BatchResponse, BatchResponseItem, BatchSuccess, CodecError, EncodeHttpError,
Endpoint, RequestMetadataError, ResourceLimitKind, Route, decode_batch_request,
decode_call_operation_metadata, decode_fetch_interface_request, decode_fetch_request,
decode_fetch_tree_request, encode_batch_error_response,
encode_batch_resource_limit_error_response, encode_batch_response,
encode_call_operation_response, encode_fetch_interface_response, encode_fetch_response,
encode_fetch_tree_response, encode_protocol_error_response,
encode_resource_limit_error_response, validate_http_request,
decode_call_operation_metadata, decode_fetch_interface_request, decode_observe_request,
encode_batch_error_response, encode_batch_resource_limit_error_response, encode_batch_response,
encode_call_operation_response, encode_fetch_interface_response, encode_observe_response,
encode_protocol_error_response, encode_resource_limit_error_response, validate_http_request,
};
use wip_protocol::{ProtocolError, ProtocolErrorCode, ProtocolInteraction};
@@ -78,10 +76,9 @@ impl WipFsServer {
}
let context = ExecutionContext::new()
.with_deadline(deadline)
.with_resource_budget(u64::from(self.config.limits.max_tree_nodes));
.with_resource_budget(u64::from(self.config.limits.max_observation_nodes));
match route {
Route::Fetch => self.fetch(request.body(), &context),
Route::FetchTree => self.fetch_tree(request.body(), &context),
Route::Observe => self.observe(request.body(), &context),
Route::FetchInterface => self.fetch_interface(request.body()),
Route::CallOperation => self.call_operation(request.body(), &context),
Route::Batch => self.batch(request.body(), &context),
@@ -110,8 +107,7 @@ impl WipFsServer {
fn route(&self, path: &str) -> Option<Route> {
[
Route::Fetch,
Route::FetchTree,
Route::Observe,
Route::FetchInterface,
Route::CallOperation,
Route::Batch,
@@ -120,33 +116,17 @@ impl WipFsServer {
.find(|route| self.endpoint.route(*route).path() == path)
}
fn fetch(&self, body: &[u8], context: &ExecutionContext) -> Response<Vec<u8>> {
let request = match decode_fetch_request(body, self.config.limits.wire) {
fn observe(&self, body: &[u8], context: &ExecutionContext) -> Response<Vec<u8>> {
let request = match decode_observe_request(body, self.config.limits.wire) {
Ok(request) => request,
Err(error) => return self.codec_error(ProtocolInteraction::Fetch, error),
Err(error) => return self.codec_error(ProtocolInteraction::Observe, error),
};
match self
.projection
.fetch_with_context(context, &request.path)
.map_err(|error| error.protocol_error())
{
Ok(object) => encode_fetch_response(&request, &object, self.config.limits.wire)
.unwrap_or_else(|error| self.read_encode_error(ProtocolInteraction::Fetch, error)),
Err(error) => self.protocol_error(ProtocolInteraction::Fetch, &error, None),
}
}
fn fetch_tree(&self, body: &[u8], context: &ExecutionContext) -> Response<Vec<u8>> {
let request = match decode_fetch_tree_request(body, self.config.limits.wire) {
Ok(request) => request,
Err(error) => return self.codec_error(ProtocolInteraction::FetchTree, error),
};
match self.projection.fetch_tree(context, &request) {
Ok(tree) => encode_fetch_tree_response(&request, &tree, self.config.limits.wire)
match self.projection.observe(context, &request) {
Ok(tree) => encode_observe_response(&request, &tree, self.config.limits.wire)
.unwrap_or_else(|error| {
self.read_encode_error(ProtocolInteraction::FetchTree, error)
self.read_encode_error(ProtocolInteraction::Observe, error)
}),
Err(error) => self.protocol_error(ProtocolInteraction::FetchTree, &error, None),
Err(error) => self.protocol_error(ProtocolInteraction::Observe, &error, None),
}
}
@@ -236,15 +216,10 @@ impl WipFsServer {
.iter()
.map(|item| {
let outcome = match &item.request {
BatchRequestKind::Fetch(request) => self
BatchRequestKind::Observe(request) => self
.projection
.fetch_with_context(context, &request.path)
.map(BatchSuccess::Fetch)
.map_err(|error| error.protocol_error()),
BatchRequestKind::FetchTree(request) => self
.projection
.fetch_tree(context, request)
.map(BatchSuccess::FetchTree),
.observe(context, request)
.map(BatchSuccess::Observe),
BatchRequestKind::FetchInterface(request) => self
.projection
.fetch_interface(request)
@@ -674,11 +649,10 @@ fn write_all_with_deadline(
fn interaction(route: Route) -> ProtocolInteraction {
match route {
Route::Fetch => ProtocolInteraction::Fetch,
Route::FetchTree => ProtocolInteraction::FetchTree,
Route::Observe => ProtocolInteraction::Observe,
Route::FetchInterface => ProtocolInteraction::FetchInterface,
Route::CallOperation => ProtocolInteraction::CallOperation,
Route::Batch => ProtocolInteraction::Fetch,
Route::Batch => ProtocolInteraction::Observe,
}
}
+52 -34
View File
@@ -11,17 +11,17 @@ use wip_fs_server::{AccessMode, ServerConfig, TransportSecurity, WipFsServer};
use wip_https::{
BatchRequest, BatchRequestItem, BatchRequestKind, BatchResponse, BatchSuccess, DecodedResponse,
Endpoint, JSON_CONTENT_TYPE, Limits, Route, decode_batch_request, decode_batch_response,
decode_call_operation_response, decode_fetch_interface_response, decode_fetch_response,
decode_fetch_tree_response, encode_batch_request, encode_call_operation_request,
encode_fetch_interface_request, encode_fetch_request, encode_fetch_tree_request,
decode_call_operation_response, decode_fetch_interface_response, decode_observe_response,
encode_batch_request, encode_call_operation_request, encode_fetch_interface_request,
encode_observe_request,
};
use wip_protocol::{
CallOperationRequest, FetchInterfaceRequest, FetchRequest, FetchTreeRequest, InterfaceTarget,
CallOperationRequest, FetchInterfaceRequest, InterfaceTarget, ObserveRequest,
ProtocolErrorCode, Target, Value,
};
#[test]
fn real_listener_serves_four_core_interactions_batch_and_write() {
fn real_listener_serves_three_core_interactions_batch_and_write() {
let temporary = tempdir().expect("temporary root");
std::fs::write(temporary.path().join("hello.txt"), b"hello world").expect("write file");
std::fs::create_dir(temporary.path().join("nested")).expect("create directory");
@@ -46,32 +46,39 @@ fn real_listener_serves_four_core_interactions_batch_and_write() {
let endpoint = Endpoint::parse(&format!("https://localhost:{}/wip", address.port()))
.expect("client endpoint");
let file_request = FetchRequest {
let file_request = ObserveRequest {
path: "/hello.txt".into(),
depth: 0,
};
let response = exchange(
address,
encode_fetch_request(&endpoint, &file_request, limits).expect("encode fetch"),
encode_observe_request(&endpoint, &file_request, limits).expect("encode observation"),
);
let file =
success(decode_fetch_response(&file_request, &response, limits).expect("decode fetch"));
let file = success(
decode_observe_response(&file_request, &response, limits).expect("decode observation"),
)
.object;
assert_eq!(file.name, "hello.txt");
assert_eq!(file.interfaces.len(), 2);
assert!(file.validator.is_some());
let tree_request = FetchTreeRequest {
let tree_request = ObserveRequest {
path: "/".into(),
depth: 1,
limit: 10,
};
let response = exchange(
address,
encode_fetch_tree_request(&endpoint, &tree_request, limits).expect("encode tree"),
encode_observe_request(&endpoint, &tree_request, limits).expect("encode tree"),
);
let tree =
success(decode_fetch_tree_response(&tree_request, &response, limits).expect("decode tree"));
assert!(!tree.truncated);
assert_eq!(tree.root.children.len(), 2);
success(decode_observe_response(&tree_request, &response, limits).expect("decode tree"));
assert_eq!(
tree.children
.as_ref()
.expect("root children observed")
.len(),
2
);
let read_interface_request = FetchInterfaceRequest {
interface: file.interfaces[0].clone(),
@@ -122,14 +129,16 @@ fn real_listener_serves_four_core_interactions_batch_and_write() {
requests: vec![
BatchRequestItem {
id: "root".into(),
request: BatchRequestKind::Fetch(FetchRequest { path: "/".into() }),
request: BatchRequestKind::Observe(ObserveRequest {
path: "/".into(),
depth: 0,
}),
},
BatchRequestItem {
id: "tree".into(),
request: BatchRequestKind::FetchTree(FetchTreeRequest {
request: BatchRequestKind::Observe(ObserveRequest {
path: "/nested".into(),
depth: 1,
limit: 4,
}),
},
BatchRequestItem {
@@ -230,7 +239,7 @@ fn stalled_request_body_times_out_and_releases_the_only_worker() {
.expect("set stalled read timeout");
write!(
stalled,
"POST /wip/v1/fetch HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nAccept: application/json\r\nContent-Length: 100\r\n\r\n{{"
"POST /wip/v1/observe HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nAccept: application/json\r\nContent-Length: 100\r\n\r\n{{"
)
.expect("write partial request");
stalled.flush().expect("flush partial request");
@@ -238,13 +247,17 @@ fn stalled_request_body_times_out_and_releases_the_only_worker() {
let endpoint = Endpoint::parse(&format!("https://localhost:{}/wip", address.port()))
.expect("client endpoint");
let fetch = FetchRequest { path: "/".into() };
let observe = ObserveRequest {
path: "/".into(),
depth: 0,
};
let started = Instant::now();
let response = exchange(
address,
encode_fetch_request(&endpoint, &fetch, limits).expect("encode fetch"),
encode_observe_request(&endpoint, &observe, limits).expect("encode observation"),
);
let _root = success(decode_fetch_response(&fetch, &response, limits).expect("decode fetch"));
let _root =
success(decode_observe_response(&observe, &response, limits).expect("decode observation"));
assert!(
started.elapsed() < Duration::from_secs(1),
"the valid request remained starved after the intake deadline"
@@ -277,7 +290,7 @@ fn batch_keeps_known_malformed_arguments_item_local() {
let limits = config.limits.wire;
let server = WipFsServer::new(config).expect("build server");
let endpoint = Endpoint::parse("https://localhost/wip").expect("endpoint");
let body = br#"{"requests":[{"id":"ok","method":"fetch","arguments":{"path":"/"}},{"id":"bad","method":"fetch_tree","arguments":{"path":"/","depth":"bad","limit":1}}]}"#;
let body = br#"{"requests":[{"id":"ok","method":"observe","arguments":{"path":"/","depth":0}},{"id":"bad","method":"observe","arguments":{"path":"/","depth":"bad"}}]}"#;
let logical = decode_batch_request(body, limits).expect("decode item-local invalid request");
assert!(matches!(
logical.requests[1].request,
@@ -296,7 +309,7 @@ fn batch_keeps_known_malformed_arguments_item_local() {
);
assert!(matches!(
batch.responses[0].outcome,
Ok(BatchSuccess::Fetch(_))
Ok(BatchSuccess::Observe(_))
));
assert!(batch.responses[1].outcome.is_err());
}
@@ -331,13 +344,15 @@ fn backend_invalid_unicode_path_is_not_found_not_internal() {
let limits = config.limits.wire;
let server = WipFsServer::new(config).expect("build server");
let endpoint = Endpoint::parse("https://localhost/wip").expect("endpoint");
let request = FetchRequest {
let request = ObserveRequest {
path: "/bad\0name".into(),
depth: 0,
};
let response = server
.handle_http(encode_fetch_request(&endpoint, &request, limits).expect("encode fetch"));
let response = server.handle_http(
encode_observe_request(&endpoint, &request, limits).expect("encode observation"),
);
assert_eq!(response.status(), http::StatusCode::NOT_FOUND);
match decode_fetch_response(&request, &response, limits).expect("decode missing response") {
match decode_observe_response(&request, &response, limits).expect("decode missing response") {
DecodedResponse::ProtocolFailure { error, .. } => {
assert_eq!(error.code, ProtocolErrorCode::NotFound);
}
@@ -355,14 +370,17 @@ fn read_response_size_failure_preserves_non_commit_guarantee() {
let server = WipFsServer::new(config).expect("build server");
let endpoint = Endpoint::parse("https://localhost/wip").expect("endpoint");
let fetch_request = FetchRequest {
let observe_request = ObserveRequest {
path: "/large".into(),
depth: 0,
};
let response = server.handle_http(
encode_fetch_request(&endpoint, &fetch_request, limits).expect("encode fetch"),
encode_observe_request(&endpoint, &observe_request, limits).expect("encode observation"),
);
let file =
success(decode_fetch_response(&fetch_request, &response, limits).expect("decode fetch"));
let file = success(
decode_observe_response(&observe_request, &response, limits).expect("decode observation"),
)
.object;
let interface_request = FetchInterfaceRequest {
interface: file.interfaces[0].clone(),
};
@@ -405,11 +423,11 @@ fn assert_batch_successes(batch: &BatchResponse) {
assert_eq!(batch.responses.len(), 3);
assert!(matches!(
batch.responses[0].outcome,
Ok(BatchSuccess::Fetch(_))
Ok(BatchSuccess::Observe(_))
));
assert!(matches!(
batch.responses[1].outcome,
Ok(BatchSuccess::FetchTree(_))
Ok(BatchSuccess::Observe(_))
));
assert!(matches!(
batch.responses[2].outcome,