Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,37 @@
|
||||
//! 固定 payload 指纹来自本基准的公开记录合同;不把生成函数作为 Rust 参考实现。
|
||||
|
||||
use dever::compiler::model::Program;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn run(core: &Path) {
|
||||
crate::compare(
|
||||
core,
|
||||
"job-performance",
|
||||
"jobs",
|
||||
&[
|
||||
include_str!("../auth/shared.rs"),
|
||||
include_str!("reference.rs"),
|
||||
include_str!("measure.rs"),
|
||||
include_str!("checks.rs"),
|
||||
]
|
||||
.concat(),
|
||||
&[("GENERATED_SUBMIT", "bench.queue.app.submit")],
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn metadata(program: &Program, reference: &str) -> String {
|
||||
assert_eq!(program.jobs.len(), 1);
|
||||
assert_eq!(program.models.len(), 1);
|
||||
assert!(!program.has_roles());
|
||||
assert_eq!(program.jobs[0].name, "bench.queue.job.publish");
|
||||
let schema = format!(
|
||||
"{:x}",
|
||||
Sha256::digest(b"bench.queue.Payload{name:Text,amount:Int}")
|
||||
);
|
||||
reference
|
||||
.replace("JOB_BINDING", &program.models.len().to_string())
|
||||
.replace("RESULT_MODEL", &program.models[0].definition.to_string())
|
||||
.replace("JOB_SCHEMA", &format!("{schema:?}"))
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// 独立 SQL 校验持久队列和业务结果,不能只用函数返回值或随机 ID 长度认定正确。
|
||||
async fn clear(scope: &Scope<'_>, registry: &Arc<Registry>) {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
executor
|
||||
.execute(scope, "DELETE FROM _dever_job", vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(scope, "DELETE FROM bench_queue_model", vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn check_rows(scope: &Scope<'_>, registry: &Arc<Registry>, inputs: &[JobInput], handled: bool) {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
let rows = executor.query(scope,
|
||||
"SELECT id,target,payload,schema,execution,task_key,state,attempt,retries,timeout_ms,token,lease_until,available_at,created_at FROM _dever_job ORDER BY task_key",
|
||||
vec![], inputs.len() + 1, |row| Ok((
|
||||
row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?, row.get::<_, String>(4)?, row.get::<_, String>(5)?,
|
||||
row.get::<_, String>(6)?, row.get::<_, i64>(7)?, row.get::<_, i64>(8)?, row.get::<_, i64>(9)?,
|
||||
row.get::<_, Option<String>>(10)?, row.get::<_, Option<i64>>(11)?, row.get::<_, i64>(12)?, row.get::<_, i64>(13)?,
|
||||
))).await.unwrap();
|
||||
assert_eq!(rows.len(), inputs.len());
|
||||
let mut unique = std::collections::BTreeSet::new();
|
||||
for (row, input) in rows.iter().zip(inputs) {
|
||||
assert!(unique.insert(&row.0));
|
||||
assert_eq!(row.0.len(), 32);
|
||||
assert_eq!(row.1, TARGET);
|
||||
let payload: RustPayload = dever_runtime::wire::decode(&row.2).unwrap();
|
||||
assert_eq!(payload.name, input.name);
|
||||
assert_eq!(payload.amount, input.amount);
|
||||
assert_eq!(row.3, SCHEMA);
|
||||
assert!(matches!(
|
||||
dever_runtime::wire::decode::<Execution>(&row.4).unwrap(),
|
||||
Execution::System { tenant: None }
|
||||
));
|
||||
assert_eq!(row.5, input.key);
|
||||
assert_eq!(row.6, if handled { "completed" } else { "pending" });
|
||||
assert_eq!((row.7, row.8, row.9), (i64::from(handled), 2, 1000));
|
||||
assert!(row.10.is_none() && row.11.is_none());
|
||||
assert!(row.12 >= row.13 && row.13 > 0);
|
||||
}
|
||||
let results = executor
|
||||
.query(
|
||||
scope,
|
||||
"SELECT name,amount FROM bench_queue_model ORDER BY name",
|
||||
vec![],
|
||||
inputs.len() + 1,
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected = if handled {
|
||||
inputs
|
||||
.iter()
|
||||
.map(|input| (input.name.clone(), input.amount * 3 + 7))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
assert_eq!(results, expected);
|
||||
}
|
||||
|
||||
fn check_jobs(runtime: &Runtime, registry: &Arc<Registry>) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
for generated in [true, false] {
|
||||
clear(scope, registry).await;
|
||||
let inputs = inputs(2);
|
||||
for input in &inputs {
|
||||
let id = submit(generated, input, scope, registry).await;
|
||||
assert_eq!(submit(generated, input, scope, registry).await, id);
|
||||
}
|
||||
check_rows(scope, registry, &inputs, false).await;
|
||||
for claim in claims(scope, registry, 2).await {
|
||||
execute(generated, claim, scope, registry).await.unwrap();
|
||||
}
|
||||
check_rows(scope, registry, &inputs, true).await;
|
||||
clear(scope, registry).await;
|
||||
let input = JobInput {
|
||||
name: "rollback".into(),
|
||||
amount: -4,
|
||||
key: "rollback".into(),
|
||||
};
|
||||
submit(generated, &input, scope, registry).await;
|
||||
let claim = claims(scope, registry, 1).await.pop().unwrap();
|
||||
assert!(matches!(
|
||||
execute(generated, claim, scope, registry).await,
|
||||
Err(JobFailure::Handler)
|
||||
));
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
assert_eq!(
|
||||
executor
|
||||
.query(
|
||||
scope,
|
||||
"SELECT COUNT(*) FROM bench_queue_model",
|
||||
vec![],
|
||||
1,
|
||||
|row| Ok(row.get::<_, i64>(0)?)
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
[0]
|
||||
);
|
||||
assert_eq!(
|
||||
executor
|
||||
.query(scope, "SELECT state FROM _dever_job", vec![], 1, |row| Ok(
|
||||
row.get::<_, String>(0)?
|
||||
))
|
||||
.await
|
||||
.unwrap(),
|
||||
["running"]
|
||||
);
|
||||
drop(database);
|
||||
clear(scope, registry).await;
|
||||
submit(generated, &inputs[0], scope, registry).await;
|
||||
let claim = claims(scope, registry, 1).await.pop().unwrap();
|
||||
let mut claim = Arc::try_unwrap(claim).unwrap();
|
||||
claim.payload = r#"["array-is-not-a-record",1]"#.into();
|
||||
assert!(matches!(
|
||||
execute(generated, Arc::new(claim), scope, registry).await,
|
||||
Err(JobFailure::Blocked)
|
||||
));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// 入队计时含具体编码和外层提交;执行计时含真实生成分派、任务作用域、解码、业务写入和确认。
|
||||
// 单阶段对照把 claim 放在计时外;完整循环另计入入队、claim、执行和确认。
|
||||
struct JobInput {
|
||||
name: String,
|
||||
amount: i64,
|
||||
key: String,
|
||||
}
|
||||
|
||||
fn inputs(count: usize) -> Vec<JobInput> {
|
||||
(0..count)
|
||||
.map(|index| JobInput {
|
||||
name: format!("job-{index:04} 中文\""),
|
||||
amount: index as i64 % 997,
|
||||
key: format!("key-{index:04}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn submit(
|
||||
generated: bool,
|
||||
input: &JobInput,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> String {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let owner = database.enter();
|
||||
let execution = Execution::System { tenant: None };
|
||||
let id = if generated {
|
||||
GENERATED_SUBMIT(
|
||||
&input.name,
|
||||
input.amount,
|
||||
&input.key,
|
||||
scope,
|
||||
&mut database,
|
||||
&execution,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
} else {
|
||||
rust_submit(
|
||||
&input.name,
|
||||
input.amount,
|
||||
&input.key,
|
||||
scope,
|
||||
&mut database,
|
||||
&execution,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
database.finish(scope, owner, true).await.unwrap();
|
||||
id.into_text()
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
generated: bool,
|
||||
claim: Arc<Claim>,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> Result<(), JobFailure> {
|
||||
let registry = registry.clone();
|
||||
let timeout = claim.policy.timeout_ms;
|
||||
let outcome = if generated {
|
||||
scope
|
||||
.spawn(move |child| job_execute(JOB_BINDING, None, claim, child, registry))
|
||||
.unwrap()
|
||||
.timeout(timeout)
|
||||
.await
|
||||
.unwrap()
|
||||
} else {
|
||||
scope
|
||||
.spawn(move |child| rust_execute(JOB_BINDING, claim, child, registry))
|
||||
.unwrap()
|
||||
.timeout(timeout)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
match outcome {
|
||||
dever_runtime::task::Timed::Ready(value) => value,
|
||||
_ => panic!("benchmark handler timed out"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn claims(scope: &Scope<'_>, registry: &Arc<Registry>, count: usize) -> Vec<Arc<Claim>> {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
let mut claims = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let claim = job::claim(
|
||||
executor,
|
||||
scope,
|
||||
dever_runtime::database::now().unwrap(),
|
||||
60000,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(claim.schema, SCHEMA);
|
||||
assert_eq!(claim.target, TARGET);
|
||||
assert_eq!(claim.attempt, 1);
|
||||
claims.push(Arc::new(claim));
|
||||
}
|
||||
assert!(
|
||||
job::claim(
|
||||
executor,
|
||||
scope,
|
||||
dever_runtime::database::now().unwrap(),
|
||||
60000
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
claims
|
||||
}
|
||||
|
||||
fn measure_jobs(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
generated: bool,
|
||||
handler: bool,
|
||||
inputs: Vec<JobInput>,
|
||||
) -> (u128, u64) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
clear(scope, registry).await;
|
||||
let claims = if handler {
|
||||
for input in &inputs {
|
||||
black_box(submit(false, input, scope, registry).await);
|
||||
}
|
||||
claims(scope, registry, inputs.len()).await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut checksum = 0u64;
|
||||
let start = Instant::now();
|
||||
if handler {
|
||||
for claim in claims {
|
||||
execute(generated, black_box(claim), scope, registry)
|
||||
.await
|
||||
.unwrap();
|
||||
checksum += black_box(1);
|
||||
}
|
||||
} else {
|
||||
for input in &inputs {
|
||||
let id = black_box(submit(generated, black_box(input), scope, registry).await);
|
||||
checksum += id.len() as u64 + input.name.len() as u64 + input.amount as u64;
|
||||
}
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
check_rows(scope, registry, &inputs, handler).await;
|
||||
(elapsed, checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (directory, path) = directory("jobs", r#""runtime":{"mode":"worker"}"#);
|
||||
let runtime = runtime();
|
||||
let registry = runtime
|
||||
.run(async |scope| database_registry(&path, scope).await)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
check_jobs(&runtime, ®istry);
|
||||
header();
|
||||
for (name, handler) in [("job_enqueue_commit", false), ("job_handler_commit", true)] {
|
||||
samples(name, |generated, count| {
|
||||
measure_jobs(&runtime, ®istry, generated, handler, inputs(count))
|
||||
});
|
||||
}
|
||||
samples("job_enqueue_large_payload_commit", |generated, count| {
|
||||
let mut inputs = inputs(count.div_ceil(64));
|
||||
for input in &mut inputs {
|
||||
input.name.push_str(&"a".repeat(64 * 1024));
|
||||
}
|
||||
measure_jobs(&runtime, ®istry, generated, false, inputs)
|
||||
});
|
||||
samples("job_submit_claim_complete", |generated, count| {
|
||||
// 完整循环的每个样本包含 250 项实际任务;初始化及独立 SQL 核验不计时。
|
||||
measure_cycle(&runtime, ®istry, generated, count.div_ceil(8), false)
|
||||
});
|
||||
samples("job_backlog_claim_complete", |generated, count| {
|
||||
measure_cycle(&runtime, ®istry, generated, count.div_ceil(8), true)
|
||||
});
|
||||
samples("job_enqueue_duplicate_commit", |generated, count| {
|
||||
measure_duplicate(&runtime, ®istry, generated, inputs(count))
|
||||
});
|
||||
drop(registry);
|
||||
drop(runtime);
|
||||
drop(directory);
|
||||
}
|
||||
|
||||
fn measure_duplicate(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
generated: bool,
|
||||
inputs: Vec<JobInput>,
|
||||
) -> (u128, u64) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
clear(scope, registry).await;
|
||||
let mut expected = Vec::with_capacity(inputs.len());
|
||||
for input in &inputs {
|
||||
expected.push(submit(false, input, scope, registry).await);
|
||||
}
|
||||
let mut actual = Vec::with_capacity(inputs.len());
|
||||
let mut checksum = 0u64;
|
||||
let start = Instant::now();
|
||||
for input in &inputs {
|
||||
let id = submit(generated, black_box(input), scope, registry).await;
|
||||
checksum += black_box(id.len() as u64);
|
||||
actual.push(id);
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
assert_eq!(actual, expected);
|
||||
check_rows(scope, registry, &inputs, false).await;
|
||||
(elapsed, checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn measure_cycle(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
generated: bool,
|
||||
count: usize,
|
||||
backlog: bool,
|
||||
) -> (u128, u64) {
|
||||
let inputs = inputs(count);
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
clear(scope, registry).await;
|
||||
if backlog {
|
||||
for input in &inputs {
|
||||
submit(generated, input, scope, registry).await;
|
||||
}
|
||||
}
|
||||
let start = Instant::now();
|
||||
let mut checksum = 0u64;
|
||||
for input in &inputs {
|
||||
if !backlog {
|
||||
submit(generated, black_box(input), scope, registry).await;
|
||||
}
|
||||
let claim = {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(JOB_BINDING, scope).await.unwrap();
|
||||
job::claim(
|
||||
executor,
|
||||
scope,
|
||||
dever_runtime::database::now().unwrap(),
|
||||
60000,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
};
|
||||
execute(generated, Arc::new(claim), scope, registry)
|
||||
.await
|
||||
.unwrap();
|
||||
checksum += black_box(1);
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
check_rows(scope, registry, &inputs, true).await;
|
||||
(elapsed, checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// 手写 Rust 重新实现 payload 编码、业务计算、行解码和事务边界;仅复用同一队列和数据库设施。
|
||||
use dever_runtime::{
|
||||
job::{self, Claim, Execution, Failure as JobFailure},
|
||||
wire::serde::{Deserialize, Serialize},
|
||||
};
|
||||
|
||||
const SCHEMA: &str = JOB_SCHEMA;
|
||||
const TARGET: &str = "bench.queue.job.publish";
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct RustPayload {
|
||||
name: String,
|
||||
amount: i64,
|
||||
}
|
||||
|
||||
impl<'input> Deserialize<'input> for RustPayload {
|
||||
fn deserialize<Decoder: dever_runtime::wire::serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct Fields {
|
||||
name: String,
|
||||
amount: i64,
|
||||
}
|
||||
// 与 Dever record 一样只接受对象,不能用 serde 默认的数组兼容路径降低校验成本。
|
||||
let fields = dever_runtime::wire::object::<Fields, Decoder>(decoder)?;
|
||||
Ok(Self {
|
||||
name: fields.name,
|
||||
amount: fields.amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct RustResult {
|
||||
_id: dever_runtime::database::Id<RESULT_MODEL>,
|
||||
_created_at: dever_runtime::time::calendar::DateTime,
|
||||
_name: String,
|
||||
amount: i64,
|
||||
}
|
||||
|
||||
async fn rust_submit(
|
||||
name: &str,
|
||||
amount: i64,
|
||||
key: &str,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
execution: &Execution,
|
||||
) -> Result<dever_runtime::value::id::Id, dever_runtime::database::Error> {
|
||||
let payload = dever_runtime::wire::json::Json::encode(&RustPayload {
|
||||
name: name.to_owned(),
|
||||
amount,
|
||||
})
|
||||
.map_err(|_| {
|
||||
dever_runtime::database::Error::new(
|
||||
dever_runtime::database::Kind::InvalidData,
|
||||
"invalid Job payload",
|
||||
)
|
||||
})?;
|
||||
let now = dever_runtime::database::now()?;
|
||||
let executor = database.on(JOB_BINDING, scope).await?;
|
||||
job::enqueue(
|
||||
executor,
|
||||
scope,
|
||||
job::Submission {
|
||||
target: TARGET,
|
||||
payload: Some(&payload),
|
||||
schema: SCHEMA,
|
||||
execution,
|
||||
key,
|
||||
policy: job::Policy {
|
||||
retries: 2,
|
||||
timeout_ms: 1000,
|
||||
},
|
||||
at: now,
|
||||
},
|
||||
now,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn rust_apply(
|
||||
payload: RustPayload,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
) -> Result<(), JobFailure> {
|
||||
let amount = payload
|
||||
.amount
|
||||
.checked_mul(3)
|
||||
.and_then(|value| value.checked_add(7))
|
||||
.ok_or(JobFailure::Handler)?;
|
||||
let parameters = vec![
|
||||
Parameter::Text(payload.name),
|
||||
Parameter::Integer(amount),
|
||||
Parameter::Integer(dever_runtime::database::now().map_err(|_| JobFailure::Handler)?),
|
||||
];
|
||||
let executor = database
|
||||
.on(0, scope)
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)?;
|
||||
let saved = executor.query(scope,
|
||||
"INSERT INTO \"bench_queue_model\" (\"name\",\"amount\",\"created_at\") VALUES (?1,?2,?3) RETURNING \"id\",\"created_at\",\"name\",\"amount\"",
|
||||
parameters, 1, |row| Ok(RustResult {
|
||||
_id: dever_runtime::database::Id(row.get(0)?),
|
||||
_created_at: dever_runtime::time::calendar::DateTime::from_millis(row.get::<_, i64>(1)?).map_err(|_| dever_runtime::database::Error::new(dever_runtime::database::Kind::InvalidData, "invalid stored DateTime"))?,
|
||||
_name: row.get(2)?, amount: row.get(3)?,
|
||||
})
|
||||
).await.map_err(|_| JobFailure::Handler)?.pop().ok_or(JobFailure::Handler)?;
|
||||
if saved.amount < 0 {
|
||||
return Err(JobFailure::Handler);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rust_execute(
|
||||
binding: usize,
|
||||
claim: Arc<Claim>,
|
||||
scope: Scope<'static>,
|
||||
registry: Arc<Registry>,
|
||||
) -> Result<(), JobFailure> {
|
||||
let execution: Execution =
|
||||
dever_runtime::wire::decode(&claim.execution).map_err(|_| JobFailure::Blocked)?;
|
||||
if claim.target != TARGET || claim.schema != SCHEMA || !registry.same_pool(binding, JOB_BINDING)
|
||||
{
|
||||
return Err(JobFailure::Blocked);
|
||||
}
|
||||
if !matches!(execution, Execution::System { tenant: None }) || claim.execution_tenant != 0 {
|
||||
return Err(JobFailure::Blocked);
|
||||
}
|
||||
let payload: RustPayload =
|
||||
dever_runtime::wire::decode(&claim.payload).map_err(|_| JobFailure::Blocked)?;
|
||||
let mut database = DbContext::new(registry);
|
||||
let owner = database.enter();
|
||||
let outcome = async {
|
||||
rust_apply(payload, &scope, &mut database).await?;
|
||||
let executor = database
|
||||
.on(JOB_BINDING, &scope)
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)?;
|
||||
job::complete(
|
||||
executor,
|
||||
&scope,
|
||||
&claim,
|
||||
dever_runtime::database::now().map_err(|_| JobFailure::Handler)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)
|
||||
}
|
||||
.await;
|
||||
database
|
||||
.finish(&scope, owner, outcome.is_ok())
|
||||
.await
|
||||
.map_err(|_| JobFailure::Handler)?;
|
||||
outcome
|
||||
}
|
||||
Reference in new issue
Block a user