1+ use claims:: { assert_none, assert_some} ;
12use crates_io_test_db:: TestDatabase ;
23use crates_io_worker:: schema:: background_jobs;
34use crates_io_worker:: { BackgroundJob , Runner } ;
45use diesel:: prelude:: * ;
56use diesel_async:: pooled_connection:: deadpool:: Pool ;
67use diesel_async:: pooled_connection:: AsyncDieselConnectionManager ;
78use diesel_async:: AsyncPgConnection ;
9+ use insta:: assert_compact_json_snapshot;
810use serde:: { Deserialize , Serialize } ;
11+ use serde_json:: Value ;
12+ use std:: sync:: atomic:: { AtomicU8 , Ordering } ;
913use std:: sync:: Arc ;
1014use tokio:: sync:: Barrier ;
1115
16+ fn all_jobs ( conn : & mut PgConnection ) -> Vec < ( String , Value ) > {
17+ background_jobs:: table
18+ . select ( ( background_jobs:: job_type, background_jobs:: data) )
19+ . get_results ( conn)
20+ . unwrap ( )
21+ }
22+
1223fn job_exists ( id : i64 , conn : & mut PgConnection ) -> bool {
1324 background_jobs:: table
1425 . find ( id)
@@ -63,7 +74,7 @@ async fn jobs_are_locked_when_fetched() {
6374 let runner = runner ( test_database. url ( ) , test_context. clone ( ) ) . register_job_type :: < TestJob > ( ) ;
6475
6576 let mut conn = test_database. connect ( ) ;
66- let job_id = TestJob . enqueue ( & mut conn) . unwrap ( ) ;
77+ let job_id = TestJob . enqueue ( & mut conn) . unwrap ( ) . unwrap ( ) ;
6778
6879 assert ! ( job_exists( job_id, & mut conn) ) ;
6980 assert ! ( !job_is_locked( job_id, & mut conn) ) ;
@@ -193,7 +204,7 @@ async fn panicking_in_jobs_updates_retry_counter() {
193204
194205 let mut conn = test_database. connect ( ) ;
195206
196- let job_id = TestJob . enqueue ( & mut conn) . unwrap ( ) ;
207+ let job_id = TestJob . enqueue ( & mut conn) . unwrap ( ) . unwrap ( ) ;
197208
198209 let runner = runner. start ( ) ;
199210 runner. wait_for_shutdown ( ) . await ;
@@ -207,6 +218,85 @@ async fn panicking_in_jobs_updates_retry_counter() {
207218 assert_eq ! ( tries, 1 ) ;
208219}
209220
221+ #[ tokio:: test( flavor = "multi_thread" ) ]
222+ async fn jobs_can_be_deduplicated ( ) {
223+ #[ derive( Clone ) ]
224+ struct TestContext {
225+ runs : Arc < AtomicU8 > ,
226+ job_started_barrier : Arc < Barrier > ,
227+ assertions_finished_barrier : Arc < Barrier > ,
228+ }
229+
230+ #[ derive( Serialize , Deserialize ) ]
231+ struct TestJob {
232+ value : String ,
233+ }
234+
235+ impl TestJob {
236+ fn new ( value : impl Into < String > ) -> Self {
237+ let value = value. into ( ) ;
238+ Self { value }
239+ }
240+ }
241+
242+ impl BackgroundJob for TestJob {
243+ const JOB_NAME : & ' static str = "test" ;
244+ const DEDUPLICATED : bool = true ;
245+ type Context = TestContext ;
246+
247+ async fn run ( & self , ctx : Self :: Context ) -> anyhow:: Result < ( ) > {
248+ let runs = ctx. runs . fetch_add ( 1 , Ordering :: SeqCst ) ;
249+ if runs == 0 {
250+ ctx. job_started_barrier . wait ( ) . await ;
251+ ctx. assertions_finished_barrier . wait ( ) . await ;
252+ }
253+ Ok ( ( ) )
254+ }
255+ }
256+
257+ let test_database = TestDatabase :: new ( ) ;
258+
259+ let test_context = TestContext {
260+ runs : Arc :: new ( AtomicU8 :: new ( 0 ) ) ,
261+ job_started_barrier : Arc :: new ( Barrier :: new ( 2 ) ) ,
262+ assertions_finished_barrier : Arc :: new ( Barrier :: new ( 2 ) ) ,
263+ } ;
264+
265+ let runner = runner ( test_database. url ( ) , test_context. clone ( ) ) . register_job_type :: < TestJob > ( ) ;
266+
267+ let mut conn = test_database. connect ( ) ;
268+
269+ // Enqueue first job
270+ assert_some ! ( TestJob :: new( "foo" ) . enqueue( & mut conn) . unwrap( ) ) ;
271+ assert_compact_json_snapshot ! ( all_jobs( & mut conn) , @r#"[["test", {"value": "foo"}]]"# ) ;
272+
273+ // Try to enqueue the same job again, which should be deduplicated
274+ assert_none ! ( TestJob :: new( "foo" ) . enqueue( & mut conn) . unwrap( ) ) ;
275+ assert_compact_json_snapshot ! ( all_jobs( & mut conn) , @r#"[["test", {"value": "foo"}]]"# ) ;
276+
277+ // Start processing the first job
278+ let runner = runner. start ( ) ;
279+ test_context. job_started_barrier . wait ( ) . await ;
280+
281+ // Enqueue the same job again, which should NOT be deduplicated,
282+ // since the first job already still running
283+ assert_some ! ( TestJob :: new( "foo" ) . enqueue( & mut conn) . unwrap( ) ) ;
284+ assert_compact_json_snapshot ! ( all_jobs( & mut conn) , @r#"[["test", {"value": "foo"}], ["test", {"value": "foo"}]]"# ) ;
285+
286+ // Try to enqueue the same job again, which should be deduplicated again
287+ assert_none ! ( TestJob :: new( "foo" ) . enqueue( & mut conn) . unwrap( ) ) ;
288+ assert_compact_json_snapshot ! ( all_jobs( & mut conn) , @r#"[["test", {"value": "foo"}], ["test", {"value": "foo"}]]"# ) ;
289+
290+ // Enqueue the same job but with different data, which should
291+ // NOT be deduplicated
292+ assert_some ! ( TestJob :: new( "bar" ) . enqueue( & mut conn) . unwrap( ) ) ;
293+ assert_compact_json_snapshot ! ( all_jobs( & mut conn) , @r#"[["test", {"value": "foo"}], ["test", {"value": "foo"}], ["test", {"value": "bar"}]]"# ) ;
294+
295+ // Resolve the final barrier to finish the test
296+ test_context. assertions_finished_barrier . wait ( ) . await ;
297+ runner. wait_for_shutdown ( ) . await ;
298+ }
299+
210300fn runner < Context : Clone + Send + Sync + ' static > (
211301 database_url : & str ,
212302 context : Context ,
0 commit comments