@@ -185,27 +185,16 @@ pub(super) fn get_next_pending_migration_tasks_batch(
185185 limit : usize ,
186186) -> Result < Vec < MigrationTask > , Error > {
187187 let txn = conn. transaction_with_behavior ( rusqlite:: TransactionBehavior :: Immediate ) ?;
188- let tasks = txn
189- . prepare (
190- "SELECT task_id, target_namespace, status, job_id
191- FROM pending_tasks
188+ let tasks = {
189+ let mut stmt = txn . prepare (
190+ "SELECT task_id, target_namespace, status, job_id
191+ FROM pending_tasks
192192 WHERE job_id = ? AND status = ? AND task_id NOT IN (select * from enqueued_tasks)
193193 LIMIT ?" ,
194- ) ?
195- . query_map ( ( job_id, status as u64 , limit) , |row| {
196- let task_id = row. get :: < _ , i64 > ( 0 ) ?;
197- let namespace = NamespaceName :: from_string ( row. get :: < _ , String > ( 1 ) ?) . unwrap ( ) ;
198- let status = MigrationTaskStatus :: from_int ( row. get :: < _ , u64 > ( 2 ) ?) ;
199- let job_id = row. get :: < _ , i64 > ( 3 ) ?;
200- Ok ( MigrationTask {
201- namespace,
202- status,
203- job_id,
204- task_id,
205- } )
206- } ) ?
207- . map ( |r| r. map_err ( Into :: into) )
208- . collect :: < Result < Vec < _ > , Error > > ( ) ?;
194+ ) ?;
195+ let mut rows = stmt. query ( ( job_id, status as u64 , limit) ) ?;
196+ read_migration_tasks ( & mut rows) ?
197+ } ;
209198
210199 for task in tasks. iter ( ) {
211200 txn. execute ( "INSERT INTO enqueued_tasks VALUES (?)" , [ task. task_id ] ) ?;
@@ -222,27 +211,16 @@ pub(super) fn get_unfinished_task_batch(
222211 limit : usize ,
223212) -> Result < Vec < MigrationTask > , Error > {
224213 let txn = conn. transaction_with_behavior ( rusqlite:: TransactionBehavior :: Immediate ) ?;
225- let tasks = txn
226- . prepare (
227- "SELECT task_id, target_namespace, status, job_id
228- FROM pending_tasks
214+ let tasks = {
215+ let mut stmt = txn . prepare (
216+ "SELECT task_id, target_namespace, status, job_id
217+ FROM pending_tasks
229218 WHERE job_id = ? AND finished = false AND task_id NOT IN (select * from enqueued_tasks)
230219 LIMIT ?" ,
231- ) ?
232- . query_map ( ( job_id, limit) , |row| {
233- let task_id = row. get :: < _ , i64 > ( 0 ) ?;
234- let namespace = NamespaceName :: from_string ( row. get :: < _ , String > ( 1 ) ?) . unwrap ( ) ;
235- let status = MigrationTaskStatus :: from_int ( row. get :: < _ , u64 > ( 2 ) ?) ;
236- let job_id = row. get :: < _ , i64 > ( 3 ) ?;
237- Ok ( MigrationTask {
238- namespace,
239- status,
240- job_id,
241- task_id,
242- } )
243- } ) ?
244- . map ( |r| r. map_err ( Into :: into) )
245- . collect :: < Result < Vec < _ > , Error > > ( ) ?;
220+ ) ?;
221+ let mut rows = stmt. query ( ( job_id, limit) ) ?;
222+ read_migration_tasks ( & mut rows) ?
223+ } ;
246224
247225 for task in tasks. iter ( ) {
248226 txn. execute ( "INSERT INTO enqueued_tasks VALUES (?)" , [ task. task_id ] ) ?;
@@ -252,6 +230,30 @@ pub(super) fn get_unfinished_task_batch(
252230 Ok ( tasks)
253231}
254232
233+ // Reject corrupt persisted names before enqueuing any tasks from the batch. An error
234+ // rolls back the transaction, so the scheduler cannot silently lose unfinished work.
235+ fn read_migration_tasks ( rows : & mut rusqlite:: Rows < ' _ > ) -> Result < Vec < MigrationTask > , Error > {
236+ let mut tasks = Vec :: new ( ) ;
237+ while let Some ( row) = rows. next ( ) ? {
238+ let task_id = row. get :: < _ , i64 > ( 0 ) ?;
239+ let name = row. get :: < _ , String > ( 1 ) ?;
240+ let namespace = NamespaceName :: from_string ( name. clone ( ) ) . map_err ( |_| {
241+ Error :: InvalidPersistedNamespace {
242+ kind : "task" ,
243+ id : task_id,
244+ name,
245+ }
246+ } ) ?;
247+ tasks. push ( MigrationTask {
248+ namespace,
249+ status : MigrationTaskStatus :: from_int ( row. get :: < _ , u64 > ( 2 ) ?) ,
250+ job_id : row. get :: < _ , i64 > ( 3 ) ?,
251+ task_id,
252+ } ) ;
253+ }
254+ Ok ( tasks)
255+ }
256+
255257pub ( super ) fn update_meta_task_status (
256258 conn : & mut rusqlite:: Connection ,
257259 task : & MigrationTask ,
@@ -317,7 +319,7 @@ pub(super) fn get_next_pending_migration_job(
317319 conn : & mut rusqlite:: Connection ,
318320) -> Result < Option < MigrationJob > , Error > {
319321 let txn = conn. transaction ( ) ?;
320- let mut job = txn
322+ let row = txn
321323 . query_row (
322324 "SELECT job_id, status, migration, schema
323325 FROM jobs
@@ -328,23 +330,37 @@ pub(super) fn get_next_pending_migration_job(
328330 MigrationJobStatus :: RunFailure as u64 ,
329331 ) ,
330332 |row| {
331- let job_id = row. get :: < _ , i64 > ( 0 ) ?;
332- let status = MigrationJobStatus :: from_int ( row. get :: < _ , u64 > ( 1 ) ?) ;
333- let mut migration = serde_json:: from_str ( row. get_ref ( 2 ) ?. as_str ( ) ?) . unwrap ( ) ;
334- let schema = NamespaceName :: from_string ( row. get :: < _ , String > ( 3 ) ?) . unwrap ( ) ;
335- let disable_foreign_key = validate_migration ( & mut migration) . unwrap ( ) ;
336- Ok ( MigrationJob {
337- schema,
338- job_id,
339- status,
340- progress : Default :: default ( ) ,
341- task_error : None ,
342- disable_foreign_key,
343- migration : migration. into ( ) ,
344- } )
333+ Ok ( (
334+ row. get :: < _ , i64 > ( 0 ) ?,
335+ row. get :: < _ , u64 > ( 1 ) ?,
336+ row. get :: < _ , String > ( 2 ) ?,
337+ row. get :: < _ , String > ( 3 ) ?,
338+ ) )
345339 } ,
346340 )
347341 . optional ( ) ?;
342+ let mut job = if let Some ( ( job_id, status, migration, name) ) = row {
343+ let schema = NamespaceName :: from_string ( name. clone ( ) ) . map_err ( |_| {
344+ Error :: InvalidPersistedNamespace {
345+ kind : "job" ,
346+ id : job_id,
347+ name,
348+ }
349+ } ) ?;
350+ let mut migration = serde_json:: from_str ( & migration) . unwrap ( ) ;
351+ let disable_foreign_key = validate_migration ( & mut migration) . unwrap ( ) ;
352+ Some ( MigrationJob {
353+ schema,
354+ job_id,
355+ status : MigrationJobStatus :: from_int ( status) ,
356+ progress : Default :: default ( ) ,
357+ task_error : None ,
358+ disable_foreign_key,
359+ migration : migration. into ( ) ,
360+ } )
361+ } else {
362+ None
363+ } ;
348364
349365 if let Some ( ref mut job) = job {
350366 txn. prepare (
@@ -483,6 +499,106 @@ mod test {
483499
484500 use super :: * ;
485501
502+ #[ test]
503+ fn invalid_persisted_job_schema_can_be_repaired ( ) {
504+ let mut conn = rusqlite:: Connection :: open_in_memory ( ) . unwrap ( ) ;
505+ setup_schema ( & mut conn) . unwrap ( ) ;
506+ let migration = serde_json:: to_string ( & Program :: seq ( & [ "select 1" ] ) ) . unwrap ( ) ;
507+ conn. execute (
508+ "INSERT INTO jobs (schema, migration, status) VALUES (?1, ?2, ?3)" ,
509+ (
510+ "../schema" ,
511+ migration,
512+ MigrationJobStatus :: WaitingDryRun as u64 ,
513+ ) ,
514+ )
515+ . unwrap ( ) ;
516+ let job_id = conn. last_insert_rowid ( ) ;
517+
518+ assert ! ( matches!(
519+ get_next_pending_migration_job( & mut conn) ,
520+ Err ( Error :: InvalidPersistedNamespace { kind: "job" , id, .. } ) if id == job_id
521+ ) ) ;
522+ conn. execute (
523+ "UPDATE jobs SET schema = 'schema' WHERE job_id = ?" ,
524+ [ job_id] ,
525+ )
526+ . unwrap ( ) ;
527+ assert_eq ! (
528+ get_next_pending_migration_job( & mut conn)
529+ . unwrap( )
530+ . unwrap( )
531+ . job_id( ) ,
532+ job_id
533+ ) ;
534+ }
535+
536+ #[ test]
537+ fn invalid_persisted_task_namespace_does_not_enqueue_partial_batch ( ) {
538+ let mut conn = rusqlite:: Connection :: open_in_memory ( ) . unwrap ( ) ;
539+ setup_schema ( & mut conn) . unwrap ( ) ;
540+ let migration = serde_json:: to_string ( & Program :: seq ( & [ "select 1" ] ) ) . unwrap ( ) ;
541+ conn. execute (
542+ "INSERT INTO jobs (schema, migration, status) VALUES (?1, ?2, ?3)" ,
543+ (
544+ "schema" ,
545+ migration,
546+ MigrationJobStatus :: WaitingDryRun as u64 ,
547+ ) ,
548+ )
549+ . unwrap ( ) ;
550+ let job_id = conn. last_insert_rowid ( ) ;
551+ conn. execute (
552+ "INSERT INTO pending_tasks (job_id, target_namespace, status) VALUES (?1, 'valid', ?2)" ,
553+ ( job_id, MigrationTaskStatus :: Enqueued as u64 ) ,
554+ )
555+ . unwrap ( ) ;
556+ conn. execute (
557+ "INSERT INTO pending_tasks (job_id, target_namespace, status) VALUES (?1, '../escape', ?2)" ,
558+ ( job_id, MigrationTaskStatus :: Enqueued as u64 ) ,
559+ )
560+ . unwrap ( ) ;
561+ let bad_task_id = conn. last_insert_rowid ( ) ;
562+
563+ for fetch in [ false , true ] {
564+ let result = if fetch {
565+ get_unfinished_task_batch ( & mut conn, job_id, 10 )
566+ } else {
567+ get_next_pending_migration_tasks_batch (
568+ & mut conn,
569+ job_id,
570+ MigrationTaskStatus :: Enqueued ,
571+ 10 ,
572+ )
573+ } ;
574+ assert ! ( matches!(
575+ result,
576+ Err ( Error :: InvalidPersistedNamespace { kind: "task" , id, .. } ) if id == bad_task_id
577+ ) ) ;
578+ let queued: i64 = conn
579+ . query_row ( "SELECT count(*) FROM enqueued_tasks" , [ ] , |row| row. get ( 0 ) )
580+ . unwrap ( ) ;
581+ assert_eq ! ( queued, 0 ) ;
582+ }
583+
584+ conn. execute (
585+ "UPDATE pending_tasks SET target_namespace = 'repaired' WHERE task_id = ?" ,
586+ [ bad_task_id] ,
587+ )
588+ . unwrap ( ) ;
589+ assert_eq ! (
590+ get_next_pending_migration_tasks_batch(
591+ & mut conn,
592+ job_id,
593+ MigrationTaskStatus :: Enqueued ,
594+ 10 ,
595+ )
596+ . unwrap( )
597+ . len( ) ,
598+ 2
599+ ) ;
600+ }
601+
486602 async fn register_schema ( meta_store : & MetaStore , schema : & ' static str ) {
487603 meta_store
488604 . handle ( schema. into ( ) )
0 commit comments