use super::http::Server; use super::*; use std::time::{Duration, Instant}; #[cfg(feature = "postgres")] #[path = "job/heartbeat.rs"] mod heartbeat; #[test] fn dever_jobs_persist_execute_and_rollback_with_bounded_retries() { let directory = Directory::new(); verify_queue( &directory, serde_json::json!({"type":"sqlite","path":"data/jobs.db","max_connections":4}), ); } #[cfg(feature = "postgres")] #[test] #[ignore = "requires explicit isolated config/setting.json database.postgres_test"] fn postgres_dever_jobs_persist_execute_and_rollback_with_bounded_retries() { let directory = Directory::new(); let schema = super::database::postgres::Schema::new( &directory.0, "dever_native_jobs", "CREATE SCHEMA dever_native_jobs", "DROP SCHEMA dever_native_jobs CASCADE", ); let mut connection = schema.connection.clone(); connection["max_connections"] = 4.into(); verify_queue(&directory, connection); } // 两种驱动运行同一 Dever 源码,包含 worker 重启、失败回滚和活跃键幂等。 fn verify_queue(directory: &Directory, connection: serde_json::Value) { let project = Project::load(&fixture().with_file_name("job")).unwrap(); let program = project.check().unwrap(); assert_eq!(program.jobs.len(), 1); let binary = build::build( &program, &project.sources, "main", &directory.0.join("jobs"), ) .unwrap(); std::fs::create_dir(directory.0.join("config")).unwrap(); std::fs::write(directory.0.join("config/setting.json"), serde_json::to_vec(&serde_json::json!({"database":{"default":connection},"job":{"workers":2,"poll_ms":10,"lease_ms":2000,"retry_base_ms":10,"retry_max_ms":100},"runtime":{"shutdown_ms":1000}})).unwrap()).unwrap(); let run = |name: &str, input: &str| { let result = execute(&binary, &format!("demo.queue.{name}"), input); assert!( result.status.success(), "{}", String::from_utf8_lossy(&result.stderr) ); serde_json::from_slice::(&result.stdout).unwrap()["data"].clone() }; let first = run("submit", r#"{"name":"first","key":"one"}"#); assert_eq!(first, run("submit", r#"{"name":"ignored","key":"one"}"#)); run("submit", r#"{"name":"fail","key":"bad"}"#); assert!( !execute( &binary, "demo.queue.rollback", r#"{"name":"rolled back","key":"rollback"}"# ) .status .success() ); assert_eq!(run("count", "{}"), 0); let mut worker = Server(Command::new(&binary).spawn().unwrap()); let deadline = Instant::now() + Duration::from_secs(10); loop { assert!(worker.0.try_wait().unwrap().is_none(), "worker exited"); if run("count", "{}") == 1 { break; } assert!( Instant::now() < deadline, "worker did not execute queued Dever code" ); std::thread::sleep(Duration::from_millis(20)); } std::thread::sleep(Duration::from_millis(150)); assert_eq!(run("count", "{}"), 1); assert!( Command::new("sh") .args(["-c", "kill -TERM \"$1\"", "--", &worker.0.id().to_string()]) .status() .unwrap() .success() ); assert!(worker.0.wait().unwrap().success()); let next = run("submit", r#"{"name":"second","key":"one"}"#); assert_ne!(first, next); let mut worker = Server(Command::new(&binary).spawn().unwrap()); let deadline = Instant::now() + Duration::from_secs(10); while run("count", "{}") != 2 { assert!( worker.0.try_wait().unwrap().is_none(), "restarted worker exited" ); assert!(Instant::now() < deadline); std::thread::sleep(Duration::from_millis(20)); } }