Surfaced + fixed during the F3 attestation:
- executor-core/sdk/queue.rs: drop redundant .map(|()| ()) call sites;
enqueue_blocking discards QueueMessageId via .map(|_id| ()) (intentional)
- executor-core/sdk/retry.rs: hoist use std::collections::hash_map::DefaultHasher
+ use std::hash::Hasher to the top of the file (clippy::items_after_statements);
replace `as i64` casts with i64::try_from + clear comment about jitter
bound (clippy::cast_possible_wrap)
- executor-core/sdk/invoke.rs: move _LIMITS_IS_COPY const before #[cfg(test)]
mod tests (clippy::items_after_test_module)
- manager-core/dispatcher.rs: dispatch_one_queue gains #[allow(too_many_lines)]
(it's the queue tick's whole logic — split makes it less readable than
the lint); .map(...).unwrap_or(default) → .map_or(default, ...)
- picloud/lib.rs: Limits { trigger_depth_max, ..Limits::default() } via
struct-update instead of let-mut-assign (clippy::field_reassign_with_default)
- tests: r#"..."# → r"..." where there are no `"` inside (clippy::needless_raw_string_hashes);
combine InvokeTarget::Name | InvokeTarget::Path arms with identical bodies
in sdk_invoke.rs (clippy::match_same_arms)
cargo fmt --all -- --check 2>&1 | tail -3: no output (exit 0)
cargo clippy --workspace --all-targets --all-features -- -D warnings:
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Workspace lib smoke: 432 passing (306 manager-core + 74 executor-core + 34 shared + 18 orchestrator-core).
DB-gated E2E (against docker compose postgres on localhost:15432):
queue_e2e: 4 ok / invoke_e2e: 4 ok / retry_e2e: 3 ok / migration_queue_messages: 4 ok / schema_snapshot: 1 ok
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
209 lines
6.8 KiB
Rust
209 lines
6.8 KiB
Rust
//! `queue::` SDK bridge integration tests — runs a real Rhai engine
|
|
//! against an in-memory `QueueService` that records the enqueued
|
|
//! `(queue_name, payload, opts)`. Verifies opts handling, blob
|
|
//! base64 encoding, depth/depth_pending pass-through.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use async_trait::async_trait;
|
|
use picloud_executor_core::{Engine, ExecRequest, InvocationType, Limits};
|
|
use picloud_shared::{
|
|
AppId, EnqueueOpts, ExecutionId, NoopDeadLetterService, NoopDocsService, NoopEventEmitter,
|
|
NoopFilesService, NoopHttpService, NoopKvService, NoopModuleSource, NoopPubsubService,
|
|
QueueError, QueueMessageId, QueueService, RequestId, ScriptId, ScriptSandbox, SdkCallCx,
|
|
Services,
|
|
};
|
|
use serde_json::{json, Value};
|
|
|
|
#[derive(Default)]
|
|
struct RecordingQueue {
|
|
enqueues: Mutex<Vec<(String, Value, EnqueueOpts)>>,
|
|
depth: Mutex<u64>,
|
|
depth_pending: Mutex<u64>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl QueueService for RecordingQueue {
|
|
async fn enqueue(
|
|
&self,
|
|
_cx: &SdkCallCx,
|
|
queue_name: &str,
|
|
payload: Value,
|
|
opts: EnqueueOpts,
|
|
) -> Result<QueueMessageId, QueueError> {
|
|
if queue_name.is_empty() {
|
|
return Err(QueueError::EmptyName);
|
|
}
|
|
self.enqueues
|
|
.lock()
|
|
.unwrap()
|
|
.push((queue_name.to_string(), payload, opts));
|
|
Ok(QueueMessageId::new())
|
|
}
|
|
|
|
async fn depth(&self, _cx: &SdkCallCx, _queue_name: &str) -> Result<u64, QueueError> {
|
|
Ok(*self.depth.lock().unwrap())
|
|
}
|
|
|
|
async fn depth_pending(&self, _cx: &SdkCallCx, _queue_name: &str) -> Result<u64, QueueError> {
|
|
Ok(*self.depth_pending.lock().unwrap())
|
|
}
|
|
}
|
|
|
|
fn make_engine(svc: Arc<RecordingQueue>) -> Arc<Engine> {
|
|
let services = Services::new(
|
|
Arc::new(NoopKvService),
|
|
Arc::new(NoopDocsService),
|
|
Arc::new(NoopDeadLetterService),
|
|
Arc::new(NoopEventEmitter),
|
|
Arc::new(NoopModuleSource),
|
|
Arc::new(NoopHttpService),
|
|
Arc::new(NoopFilesService),
|
|
Arc::new(NoopPubsubService),
|
|
Arc::new(picloud_shared::NoopSecretsService),
|
|
Arc::new(picloud_shared::NoopEmailService),
|
|
Arc::new(picloud_shared::NoopUsersService),
|
|
svc,
|
|
Arc::new(picloud_shared::NoopInvokeService),
|
|
);
|
|
Arc::new(Engine::new(Limits::default(), services))
|
|
}
|
|
|
|
fn baseline_request(app_id: AppId) -> ExecRequest {
|
|
let execution_id = ExecutionId::new();
|
|
ExecRequest {
|
|
execution_id,
|
|
request_id: RequestId::new(),
|
|
script_id: ScriptId::new(),
|
|
script_name: "queue-test".into(),
|
|
invocation_type: InvocationType::Http,
|
|
path: "/queue-test".into(),
|
|
headers: BTreeMap::new(),
|
|
body: Value::Null,
|
|
params: BTreeMap::new(),
|
|
query: BTreeMap::new(),
|
|
rest: String::new(),
|
|
sandbox_overrides: ScriptSandbox::default(),
|
|
app_id,
|
|
principal: None,
|
|
trigger_depth: 0,
|
|
root_execution_id: execution_id,
|
|
is_dead_letter_handler: false,
|
|
event: None,
|
|
}
|
|
}
|
|
|
|
async fn run(engine: Arc<Engine>, src: &str, req: ExecRequest) {
|
|
let src = src.to_string();
|
|
tokio::task::spawn_blocking(move || engine.execute(&src, req))
|
|
.await
|
|
.expect("spawn_blocking should not panic")
|
|
.expect("script execution should succeed");
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn enqueue_map_payload() {
|
|
let svc = Arc::new(RecordingQueue::default());
|
|
let engine = make_engine(svc.clone());
|
|
run(
|
|
engine,
|
|
r#"queue::enqueue("jobs", #{ id: 1, name: "x" });"#,
|
|
baseline_request(AppId::new()),
|
|
)
|
|
.await;
|
|
let (qn, msg, opts) = svc.enqueues.lock().unwrap()[0].clone();
|
|
assert_eq!(qn, "jobs");
|
|
assert_eq!(msg, json!({ "id": 1, "name": "x" }));
|
|
assert!(opts.delay_ms.is_none());
|
|
assert!(opts.max_attempts.is_none());
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn enqueue_with_opts_threads_through() {
|
|
let svc = Arc::new(RecordingQueue::default());
|
|
let engine = make_engine(svc.clone());
|
|
run(
|
|
engine,
|
|
r#"queue::enqueue("jobs", #{ x: 1 }, #{ delay_ms: 60000, max_attempts: 5 });"#,
|
|
baseline_request(AppId::new()),
|
|
)
|
|
.await;
|
|
let (qn, _msg, opts) = svc.enqueues.lock().unwrap()[0].clone();
|
|
assert_eq!(qn, "jobs");
|
|
assert_eq!(opts.delay_ms, Some(60_000));
|
|
assert_eq!(opts.max_attempts, Some(5));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn enqueue_blob_base64_encodes() {
|
|
let svc = Arc::new(RecordingQueue::default());
|
|
let engine = make_engine(svc.clone());
|
|
run(
|
|
engine,
|
|
r#"
|
|
let data = base64::decode("aGVsbG8=");
|
|
queue::enqueue("blobs", #{ raw: data });
|
|
"#,
|
|
baseline_request(AppId::new()),
|
|
)
|
|
.await;
|
|
let (_qn, msg, _opts) = svc.enqueues.lock().unwrap()[0].clone();
|
|
assert_eq!(msg, json!({ "raw": "aGVsbG8=" }));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn depth_returns_service_value() {
|
|
let svc = Arc::new(RecordingQueue::default());
|
|
*svc.depth.lock().unwrap() = 99;
|
|
let engine = make_engine(svc.clone());
|
|
// Stash result via a known-shape map; the engine returns the eval value.
|
|
let src = r#"
|
|
let n = queue::depth("any");
|
|
#{ statusCode: 200, body: n }
|
|
"#
|
|
.to_string();
|
|
let req = baseline_request(AppId::new());
|
|
let resp = tokio::task::spawn_blocking({
|
|
let engine = engine.clone();
|
|
move || engine.execute(&src, req)
|
|
})
|
|
.await
|
|
.expect("spawn_blocking should not panic")
|
|
.expect("script execution should succeed");
|
|
assert_eq!(resp.body, json!(99));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn enqueue_rejects_fnptr_in_payload() {
|
|
let svc = Arc::new(RecordingQueue::default());
|
|
let engine = make_engine(svc.clone());
|
|
let src = r#"
|
|
let f = |x| x + 1;
|
|
queue::enqueue("jobs", #{ closure: f });
|
|
"#
|
|
.to_string();
|
|
let req = baseline_request(AppId::new());
|
|
let res = tokio::task::spawn_blocking(move || engine.execute(&src, req))
|
|
.await
|
|
.expect("spawn_blocking should not panic");
|
|
let err = res.unwrap_err();
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("FnPtr") || msg.contains("closure"),
|
|
"expected FnPtr rejection, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn enqueue_empty_name_throws() {
|
|
let svc = Arc::new(RecordingQueue::default());
|
|
let engine = make_engine(svc.clone());
|
|
let src = r#"queue::enqueue("", 1);"#.to_string();
|
|
let req = baseline_request(AppId::new());
|
|
let res = tokio::task::spawn_blocking(move || engine.execute(&src, req))
|
|
.await
|
|
.expect("spawn_blocking should not panic");
|
|
assert!(res.is_err(), "empty queue_name should throw");
|
|
}
|