use bpm_engine::bpm_engine_adapter_memory::MemoryRepo;
use bpm_engine::bpm_engine_core::ExternalTaskState;
use bpm_engine::bpm_engine_storage::ExternalTaskStore;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
#[tokio::test]
async fn only_one_worker_can_lock_same_task() {
let repo = Arc::new(MemoryRepo::new());
let _task_id = repo
.create("token-1", "instance-1", "payment", 3, 60, HashMap::new())
.await
.unwrap();
let locked = repo
.fetch_and_lock(
"worker-1",
&["payment".to_string()],
10,
Duration::from_secs(30),
)
.await
.unwrap();
assert_eq!(locked.len(), 1);
assert_eq!(locked[0].state, ExternalTaskState::Locked);
assert_eq!(locked[0].lock_owner.as_deref(), Some("worker-1"));
let locked2 = repo
.fetch_and_lock(
"worker-2",
&["payment".to_string()],
10,
Duration::from_secs(30),
)
.await
.unwrap();
assert!(
locked2.is_empty(),
"worker-2 must not see a task already locked by worker-1"
);
}
#[tokio::test]
async fn lease_expiry_enables_reclaim() {
let repo = Arc::new(MemoryRepo::new());
let _task_id = repo
.create("token-1", "instance-1", "job", 1, 60, HashMap::new())
.await
.unwrap();
let tasks = repo
.fetch_and_lock("worker-1", &["job".to_string()], 10, Duration::from_secs(0))
.await
.unwrap();
assert_eq!(tasks.len(), 1);
let reclaimed = repo.reclaim_expired_locks().await.unwrap();
assert!(reclaimed >= 1, "at least one lock should be reclaimed");
let tasks2 = repo
.fetch_and_lock(
"worker-2",
&["job".to_string()],
10,
Duration::from_secs(30),
)
.await
.unwrap();
assert_eq!(tasks2.len(), 1);
assert_eq!(tasks2[0].lock_owner.as_deref(), Some("worker-2"));
}
#[tokio::test]
async fn complete_after_lease_expiry_fails() {
let repo = Arc::new(MemoryRepo::new());
let task_id = repo
.create("token-1", "instance-1", "payment", 3, 60, HashMap::new())
.await
.unwrap();
let _tasks = repo
.fetch_and_lock(
"worker-1",
&["payment".to_string()],
10,
Duration::from_secs(0),
)
.await
.unwrap();
tokio::time::sleep(Duration::from_secs(2)).await;
let _reclaimed = repo.reclaim_expired_locks().await.unwrap();
let err = repo.complete(&task_id, "worker-1", HashMap::new()).await;
assert!(
err.is_err(),
"complete by worker-1 after lease expiry must be rejected"
);
}
#[tokio::test]
async fn worker_can_complete_before_lease_expires() {
let repo = Arc::new(MemoryRepo::new());
let task_id = repo
.create("token-1", "instance-1", "payment", 3, 60, HashMap::new())
.await
.unwrap();
let _tasks = repo
.fetch_and_lock(
"worker-1",
&["payment".to_string()],
10,
Duration::from_secs(60),
)
.await
.unwrap();
repo.complete(&task_id, "worker-1", HashMap::new())
.await
.unwrap();
let task = repo.get(&task_id).await.unwrap().unwrap();
assert_eq!(task.state, ExternalTaskState::Completed);
}
#[tokio::test]
async fn fail_decrements_retries_and_returns_to_ready() {
let repo = Arc::new(MemoryRepo::new());
let task_id = repo
.create("token-1", "instance-1", "notify", 2, 60, HashMap::new())
.await
.unwrap();
let _tasks = repo
.fetch_and_lock(
"worker-1",
&["notify".to_string()],
10,
Duration::from_secs(30),
)
.await
.unwrap();
repo.fail(&task_id, "worker-1", "timeout".to_string(), None)
.await
.unwrap();
let task = repo.get(&task_id).await.unwrap().unwrap();
assert_eq!(task.state, ExternalTaskState::Ready);
assert_eq!(task.retries, 1);
let tasks2 = repo
.fetch_and_lock(
"worker-2",
&["notify".to_string()],
10,
Duration::from_secs(30),
)
.await
.unwrap();
assert_eq!(tasks2.len(), 1);
repo.fail(&task_id, "worker-2", "still failing".to_string(), None)
.await
.unwrap();
let task = repo.get(&task_id).await.unwrap().unwrap();
assert_eq!(task.state, ExternalTaskState::Failed);
assert_eq!(task.retries, 0);
}