1
1
#![ allow( dead_code) ]
2
2
use diesel:: prelude:: * ;
3
3
use std:: panic:: { catch_unwind, UnwindSafe } ;
4
+ use threadpool:: ThreadPool ;
4
5
5
- use super :: storage;
6
+ use db:: { DieselPool , DieselPooledConn } ;
7
+ use super :: { storage, Registry , Job } ;
6
8
use util:: errors:: * ;
7
9
8
- fn get_single_job < F > ( conn : & PgConnection , f : F ) -> CargoResult < ( ) >
9
- where
10
- F : FnOnce ( storage:: BackgroundJob ) -> CargoResult < ( ) > + UnwindSafe ,
11
- {
12
- conn. transaction :: < _ , Box < dyn CargoError > , _ > ( || {
13
- let job = storage:: find_next_unlocked_job ( conn) ?;
14
- let job_id = job. id ;
15
-
16
- let result = catch_unwind ( || f ( job) )
17
- . map_err ( |_| internal ( "job panicked" ) )
18
- . and_then ( |r| r) ;
19
-
20
- if result. is_ok ( ) {
21
- storage:: delete_successful_job ( conn, job_id) ?;
22
- } else {
23
- storage:: update_failed_job ( conn, job_id) ;
10
+ #[ allow( missing_debug_implementations) ]
11
+ pub struct Builder < Env > {
12
+ connection_pool : DieselPool ,
13
+ environment : Env ,
14
+ registry : Registry < Env > ,
15
+ thread_count : Option < usize > ,
16
+ }
17
+
18
+ impl < Env > Builder < Env > {
19
+ pub fn register < T : Job < Environment = Env > > ( mut self ) -> Self {
20
+ self . registry . register :: < T > ( ) ;
21
+ self
22
+ }
23
+
24
+ pub fn thread_count ( mut self , thread_count : usize ) -> Self {
25
+ self . thread_count = Some ( thread_count) ;
26
+ self
27
+ }
28
+
29
+ pub fn build ( self ) -> Runner < Env > {
30
+ Runner {
31
+ connection_pool : self . connection_pool ,
32
+ thread_pool : ThreadPool :: new ( self . thread_count . unwrap_or ( 5 ) ) ,
33
+ environment : self . environment ,
34
+ registry : self . registry ,
24
35
}
25
- Ok ( ( ) )
26
- } )
36
+ }
37
+ }
38
+
39
+ #[ allow( missing_debug_implementations) ]
40
+ pub struct Runner < Env > {
41
+ connection_pool : DieselPool ,
42
+ thread_pool : ThreadPool ,
43
+ environment : Env ,
44
+ registry : Registry < Env > ,
45
+ }
46
+
47
+ impl < Env > Runner < Env > {
48
+ pub fn builder ( connection_pool : DieselPool , environment : Env ) -> Builder < Env > {
49
+ Builder {
50
+ connection_pool,
51
+ environment,
52
+ registry : Registry :: new ( ) ,
53
+ thread_count : None ,
54
+ }
55
+ }
56
+
57
+ fn get_single_job < F > ( & self , f : F )
58
+ where
59
+ F : FnOnce ( storage:: BackgroundJob ) -> CargoResult < ( ) > + Send + UnwindSafe + ' static ,
60
+ {
61
+ let conn = self . connection ( ) . expect ( "Could not acquire connection" ) ;
62
+ self . thread_pool . execute ( move || {
63
+ conn. transaction :: < _ , Box < dyn CargoError > , _ > ( || {
64
+ let job = storage:: find_next_unlocked_job ( & conn) . optional ( ) ?;
65
+ let job = match job {
66
+ Some ( j) => j,
67
+ None => return Ok ( ( ) ) ,
68
+ } ;
69
+ let job_id = job. id ;
70
+
71
+ let result = catch_unwind ( || f ( job) )
72
+ . map_err ( |_| internal ( "job panicked" ) )
73
+ . and_then ( |r| r) ;
74
+
75
+ if result. is_ok ( ) {
76
+ storage:: delete_successful_job ( & conn, job_id) ?;
77
+ } else {
78
+ storage:: update_failed_job ( & conn, job_id) ;
79
+ }
80
+ Ok ( ( ) )
81
+ } ) . expect ( "Could not retrieve or update job" )
82
+ } )
83
+ }
84
+
85
+ fn connection ( & self ) -> CargoResult < DieselPooledConn > {
86
+ self . connection_pool . get ( ) . map_err ( Into :: into)
87
+ }
88
+
89
+ #[ cfg( test) ]
90
+ fn wait_for_jobs ( & self ) {
91
+ self . thread_pool . join ( ) ;
92
+ assert_eq ! ( 0 , self . thread_pool. panic_count( ) ) ;
93
+ }
27
94
}
28
95
29
96
#[ cfg( test) ]
30
97
mod tests {
31
98
use diesel:: prelude:: * ;
99
+ use diesel:: r2d2;
32
100
33
101
use schema:: background_jobs:: dsl:: * ;
34
102
use std:: sync:: { Mutex , MutexGuard , Barrier , Arc } ;
35
103
use std:: panic:: AssertUnwindSafe ;
36
- use std:: thread;
37
104
use super :: * ;
38
105
39
106
#[ test]
40
107
fn jobs_are_locked_when_fetched ( ) {
41
108
let _guard = TestGuard :: lock ( ) ;
42
109
43
- let conn = connection ( ) ;
44
- let first_job_id = create_dummy_job ( & conn ) . id ;
45
- let second_job_id = create_dummy_job ( & conn ) . id ;
110
+ let runner = runner ( ) ;
111
+ let first_job_id = create_dummy_job ( & runner ) . id ;
112
+ let second_job_id = create_dummy_job ( & runner ) . id ;
46
113
let fetch_barrier = Arc :: new ( AssertUnwindSafe ( Barrier :: new ( 2 ) ) ) ;
47
114
let fetch_barrier2 = fetch_barrier. clone ( ) ;
48
115
let return_barrier = Arc :: new ( AssertUnwindSafe ( Barrier :: new ( 2 ) ) ) ;
49
116
let return_barrier2 = return_barrier. clone ( ) ;
50
117
51
- let t1 = thread:: spawn ( move || {
52
- let _ = get_single_job ( & connection ( ) , |job| {
53
- fetch_barrier. 0 . wait ( ) ; // Tell thread 2 it can lock its job
54
- assert_eq ! ( first_job_id, job. id) ;
55
- return_barrier. 0 . wait ( ) ; // Wait for thread 2 to lock its job
56
- Ok ( ( ) )
57
- } ) ;
118
+ runner. get_single_job ( move |job| {
119
+ fetch_barrier. 0 . wait ( ) ; // Tell thread 2 it can lock its job
120
+ assert_eq ! ( first_job_id, job. id) ;
121
+ return_barrier. 0 . wait ( ) ; // Wait for thread 2 to lock its job
122
+ Ok ( ( ) )
58
123
} ) ;
59
124
60
- let t2 = thread:: spawn ( move || {
61
- fetch_barrier2. 0 . wait ( ) ; // Wait until thread 1 locks its job
62
- get_single_job ( & connection ( ) , |job| {
63
- assert_eq ! ( second_job_id, job. id) ;
64
- return_barrier2. 0 . wait ( ) ; // Tell thread 1 it can unlock its job
65
- Ok ( ( ) )
66
- } )
67
- . unwrap ( ) ;
125
+ fetch_barrier2. 0 . wait ( ) ; // Wait until thread 1 locks its job
126
+ runner. get_single_job ( move |job| {
127
+ assert_eq ! ( second_job_id, job. id) ;
128
+ return_barrier2. 0 . wait ( ) ; // Tell thread 1 it can unlock its job
129
+ Ok ( ( ) )
68
130
} ) ;
69
131
70
- t1. join ( ) . unwrap ( ) ;
71
- t2. join ( ) . unwrap ( ) ;
132
+ runner. wait_for_jobs ( ) ;
72
133
}
73
134
74
135
#[ test]
75
136
fn jobs_are_deleted_when_successfully_run ( ) {
76
137
let _guard = TestGuard :: lock ( ) ;
77
138
78
- let conn = connection ( ) ;
79
- create_dummy_job ( & conn ) ;
139
+ let runner = runner ( ) ;
140
+ create_dummy_job ( & runner ) ;
80
141
81
- get_single_job ( & conn , |_| {
142
+ runner . get_single_job ( |_| {
82
143
Ok ( ( ) )
83
- } ) . unwrap ( ) ;
144
+ } ) ;
145
+ runner. wait_for_jobs ( ) ;
84
146
85
147
let remaining_jobs = background_jobs. count ( )
86
- . get_result ( & conn ) ;
148
+ . get_result ( & runner . connection ( ) . unwrap ( ) ) ;
87
149
assert_eq ! ( Ok ( 0 ) , remaining_jobs) ;
88
150
}
89
151
90
152
#[ test]
91
153
fn failed_jobs_do_not_release_lock_before_updating_retry_time ( ) {
92
154
let _guard = TestGuard :: lock ( ) ;
93
- create_dummy_job ( & connection ( ) ) ;
155
+
156
+ let runner = runner ( ) ;
157
+ create_dummy_job ( & runner) ;
94
158
let barrier = Arc :: new ( AssertUnwindSafe ( Barrier :: new ( 2 ) ) ) ;
95
159
let barrier2 = barrier. clone ( ) ;
96
160
97
- let t1 = thread:: spawn ( move || {
98
- let _ = get_single_job ( & connection ( ) , |_| {
99
- barrier. 0 . wait ( ) ;
100
- // error so the job goes back into the queue
101
- Err ( human ( "nope" ) )
102
- } ) ;
161
+ runner. get_single_job ( move |_| {
162
+ barrier. 0 . wait ( ) ;
163
+ // error so the job goes back into the queue
164
+ Err ( human ( "nope" ) )
103
165
} ) ;
104
166
105
- let t2 = thread:: spawn ( move || {
106
- let conn = connection ( ) ;
107
- // Wait for the first thread to acquire the lock
108
- barrier2. 0 . wait ( ) ;
109
- // We are intentionally not using `get_single_job` here.
110
- // `SKIP LOCKED` is intentionally omitted here, so we block until
111
- // the lock on the first job is released.
112
- // If there is any point where the row is unlocked, but the retry
113
- // count is not updated, we will get a row here.
114
- let available_jobs = background_jobs
115
- . select ( id)
116
- . filter ( retries. eq ( 0 ) )
117
- . for_update ( )
118
- . load :: < i64 > ( & conn)
119
- . unwrap ( ) ;
120
- assert_eq ! ( 0 , available_jobs. len( ) ) ;
167
+ let conn = runner. connection ( ) . unwrap ( ) ;
168
+ // Wait for the first thread to acquire the lock
169
+ barrier2. 0 . wait ( ) ;
170
+ // We are intentionally not using `get_single_job` here.
171
+ // `SKIP LOCKED` is intentionally omitted here, so we block until
172
+ // the lock on the first job is released.
173
+ // If there is any point where the row is unlocked, but the retry
174
+ // count is not updated, we will get a row here.
175
+ let available_jobs = background_jobs
176
+ . select ( id)
177
+ . filter ( retries. eq ( 0 ) )
178
+ . for_update ( )
179
+ . load :: < i64 > ( & conn)
180
+ . unwrap ( ) ;
181
+ assert_eq ! ( 0 , available_jobs. len( ) ) ;
121
182
122
- // Sanity check to make sure the job actually is there
123
- let total_jobs_including_failed = background_jobs
124
- . select ( id)
125
- . for_update ( )
126
- . load :: < i64 > ( & conn)
127
- . unwrap ( ) ;
128
- assert_eq ! ( 1 , total_jobs_including_failed. len( ) ) ;
129
- } ) ;
183
+ // Sanity check to make sure the job actually is there
184
+ let total_jobs_including_failed = background_jobs
185
+ . select ( id)
186
+ . for_update ( )
187
+ . load :: < i64 > ( & conn)
188
+ . unwrap ( ) ;
189
+ assert_eq ! ( 1 , total_jobs_including_failed. len( ) ) ;
130
190
131
- t1. join ( ) . unwrap ( ) ;
132
- t2. join ( ) . unwrap ( ) ;
191
+ runner. wait_for_jobs ( ) ;
133
192
}
134
193
135
194
#[ test]
136
195
fn panicking_in_jobs_updates_retry_counter ( ) {
137
196
let _guard = TestGuard :: lock ( ) ;
138
- let conn = connection ( ) ;
139
- let job_id = create_dummy_job ( & conn ) . id ;
197
+ let runner = runner ( ) ;
198
+ let job_id = create_dummy_job ( & runner ) . id ;
140
199
141
- let t1 = thread:: spawn ( move || {
142
- let _ = get_single_job ( & connection ( ) , |_| {
143
- panic ! ( )
144
- } ) ;
200
+ runner. get_single_job ( |_| {
201
+ panic ! ( )
145
202
} ) ;
146
-
147
- let _ = t1. join ( ) ;
203
+ runner. wait_for_jobs ( ) ;
148
204
149
205
let tries = background_jobs
150
206
. find ( job_id)
151
207
. select ( retries)
152
208
. for_update ( )
153
- . first :: < i32 > ( & conn )
209
+ . first :: < i32 > ( & runner . connection ( ) . unwrap ( ) )
154
210
. unwrap ( ) ;
155
211
assert_eq ! ( 1 , tries) ;
156
212
}
157
213
158
-
159
214
lazy_static ! {
160
215
// Since these tests deal with behavior concerning multiple connections
161
216
// running concurrently, they have to run outside of a transaction.
@@ -177,24 +232,32 @@ mod tests {
177
232
impl < ' a > Drop for TestGuard < ' a > {
178
233
fn drop ( & mut self ) {
179
234
:: diesel:: sql_query ( "TRUNCATE TABLE background_jobs" )
180
- . execute ( & connection ( ) )
235
+ . execute ( & runner ( ) . connection ( ) . unwrap ( ) )
181
236
. unwrap ( ) ;
182
237
}
183
238
}
184
239
185
- fn connection ( ) -> PgConnection {
240
+ fn runner ( ) -> Runner < ( ) > {
186
241
use dotenv;
187
242
188
243
let database_url =
189
244
dotenv:: var ( "TEST_DATABASE_URL" ) . expect ( "TEST_DATABASE_URL must be set to run tests" ) ;
190
- PgConnection :: establish ( & database_url) . unwrap ( )
245
+ let manager = r2d2:: ConnectionManager :: new ( database_url) ;
246
+ let pool = r2d2:: Pool :: builder ( )
247
+ . max_size ( 2 )
248
+ . build ( manager)
249
+ . unwrap ( ) ;
250
+
251
+ Runner :: builder ( pool, ( ) )
252
+ . thread_count ( 2 )
253
+ . build ( )
191
254
}
192
255
193
- fn create_dummy_job ( conn : & PgConnection ) -> storage:: BackgroundJob {
256
+ fn create_dummy_job ( runner : & Runner < ( ) > ) -> storage:: BackgroundJob {
194
257
:: diesel:: insert_into ( background_jobs)
195
258
. values ( ( job_type. eq ( "Foo" ) , data. eq ( json ! ( null) ) ) )
196
259
. returning ( ( id, job_type, data) )
197
- . get_result ( conn )
260
+ . get_result ( & runner . connection ( ) . unwrap ( ) )
198
261
. unwrap ( )
199
262
}
200
263
}
0 commit comments