@@ -218,10 +218,10 @@ impl Chunk {
218218#[ cfg( feature = "server-logic" ) ]
219219pub mod db {
220220 use super :: * ;
221- use crate :: common:: errors:: { ChunkNotFound , DatabaseError , SubmissionNotFound , E } ;
221+ use crate :: common:: errors:: { DatabaseError , SubmissionNotFound , E } ;
222222 use crate :: db:: { Connection , True , WriterConnection } ;
223223 use axum_prometheus:: metrics:: { counter, gauge} ;
224- use sqlx:: { query, query_as} ;
224+ use sqlx:: { query, query_as, query_scalar } ;
225225 use sqlx:: { QueryBuilder , Sqlite } ;
226226
227227 impl < ' q > sqlx:: Encode < ' q , Sqlite > for super :: ChunkIndex {
@@ -281,25 +281,18 @@ pub mod db {
281281 chunk_id : ChunkId ,
282282 output_content : Option < Vec < u8 > > ,
283283 mut conn : impl WriterConnection ,
284- ) -> Result < ( ) , E < DatabaseError , E < SubmissionNotFound , ChunkNotFound > > > {
285- let _chunk_size: Result < ChunkSize , E < DatabaseError , E < SubmissionNotFound , ChunkNotFound > > > =
286- conn. transaction ( move |mut tx| {
287- Box :: pin ( async move {
288- let completed_work =
289- complete_chunk_raw ( chunk_id, output_content, & mut tx) . await ?;
290- crate :: common:: submission:: db:: maybe_complete_submission (
291- chunk_id. submission_id ,
292- & mut tx,
293- )
294- . await
295- . map_err ( |e| match e {
296- E :: L ( e) => E :: L ( e) ,
297- E :: R ( e) => E :: R ( E :: L ( e) ) ,
298- } ) ?;
299- Ok ( completed_work. unwrap_or_default ( ) )
300- } )
284+ ) -> Result < ( ) , E < DatabaseError , SubmissionNotFound > > {
285+ conn. transaction ( move |mut tx| {
286+ Box :: pin ( async move {
287+ complete_chunk_raw ( chunk_id, output_content, & mut tx) . await ?;
288+ crate :: common:: submission:: db:: maybe_complete_submission (
289+ chunk_id. submission_id ,
290+ & mut tx,
291+ )
292+ . await
301293 } )
302- . await ;
294+ } )
295+ . await ?;
303296
304297 counter ! ( crate :: prometheus:: CHUNKS_COMPLETED_COUNTER ) . increment ( 1 ) ;
305298 Ok ( ( ) )
@@ -311,9 +304,9 @@ pub mod db {
311304 chunk_id : ChunkId ,
312305 output_content : Option < Vec < u8 > > ,
313306 mut tx : impl WriterConnection < Transaction = True > ,
314- ) -> sqlx:: Result < Option < ChunkSize > > {
307+ ) -> sqlx:: Result < ( ) > {
315308 let now = chrono:: prelude:: Utc :: now ( ) ;
316- query ! (
309+ let chunk_moved = query ! (
317310 "
318311 INSERT INTO chunks_completed
319312 (submission_id, chunk_index, output_content, completed_at)
@@ -330,26 +323,40 @@ pub mod db {
330323 chunk_id. submission_id,
331324 chunk_id. chunk_index,
332325 )
333- . fetch_one ( tx. get_inner ( ) )
334- . await ?;
335- // Defense in depth: Above query should never be called twice on the same chunk.
336- // If it _does_ happen, it means that either a consumer is attempting a chunk they didn't reserve,
337- // or we gave out the same reservation twice.
326+ . fetch_optional ( tx. get_inner ( ) )
327+ . await ?
328+ . is_some ( ) ;
329+ // Defense in depth: Above query could be called twice on the same chunk. For instance,
330+ // when the server was restarted and the reservations are forgotten, and the same chunk
331+ // was reserved again.
332+ //
333+ // In addition, cancelling or pausing a submission while a chunk is reserved also results
334+ // in the chunk not being in the `chunks` table. Which is fine, because cancelled
335+ // submissions count as failed, and for paused submissions we will retry the chunk when it
336+ // becomes unpaused.
338337 //
339- // By returning early if the chunk was not found,
340- // we ensure that even in these situations
341- // we never mess up the submission's `chunks_done` counter.
338+ // By only updating `chunks_done` when we actually moved a chunk, we ensure that we never
339+ // mess up the submission's `chunks_done` counter.
342340 //
343341 // (Not doing that resulted in a hard-to-track-down bug in the past.
344342 // https://github.com/channable/opsqueue/issues/76
345343 // )
346- sqlx:: query_scalar!(
347- "UPDATE submissions SET chunks_done = chunks_done + 1 WHERE submissions.id = $1 RETURNING submissions.chunk_size;" ,
348- chunk_id. submission_id,
349- )
350- . fetch_one ( tx. get_inner ( ) )
351- . await
352- . map ( |opt| opt. map ( ChunkSize ) )
344+ if chunk_moved {
345+ sqlx:: query_scalar!(
346+ "UPDATE submissions SET chunks_done = chunks_done + 1 WHERE submissions.id = $1 RETURNING submissions.chunk_size;" ,
347+ chunk_id. submission_id,
348+ )
349+ . fetch_one ( tx. get_inner ( ) )
350+ . await ?;
351+ } else {
352+ tracing:: warn!(
353+ "Could not complete chunk {:?} because it was either: \
354+ completed, failed, cancelled, or paused before. Ignoring.",
355+ chunk_id
356+ ) ;
357+ }
358+
359+ Ok ( ( ) )
353360 }
354361
355362 #[ tracing:: instrument( skip( conn) ) ]
@@ -366,7 +373,7 @@ pub mod db {
366373 submission_id,
367374 chunk_index,
368375 } = chunk_id;
369- let fields = query ! (
376+ let retries = query_scalar ! (
370377 "
371378 UPDATE chunks SET retries = retries + 1
372379 WHERE submission_id = $1 AND chunk_index = $2
@@ -375,24 +382,36 @@ pub mod db {
375382 submission_id,
376383 chunk_index
377384 )
378- . fetch_one ( tx. get_inner ( ) )
385+ . fetch_optional ( tx. get_inner ( ) )
379386 . await ?;
380- tracing:: trace!( "Retries: {}" , fields. retries) ;
381- if fields. retries >= max_retries. into ( ) {
382- crate :: common:: submission:: db:: fail_submission_notx (
383- submission_id,
384- chunk_index,
385- failure,
386- & mut tx,
387- )
388- . await ?;
389-
390- Ok :: < _ , sqlx:: Error > ( true )
391- } else {
392- counter ! ( crate :: prometheus:: CHUNKS_RETRIED_COUNTER ) . increment ( 1 ) ;
393- // When retrying, the chunk re-enters ('stays') in the backlog,
394- // so we *don't* decrement the backlog gauge here.
395- Ok :: < _ , sqlx:: Error > ( false )
387+ tracing:: trace!( "Retries: {:?}" , retries) ;
388+ match retries {
389+ Some ( retries) if retries >= max_retries. into ( ) => {
390+ crate :: common:: submission:: db:: fail_submission_notx (
391+ submission_id,
392+ chunk_index,
393+ failure,
394+ & mut tx,
395+ )
396+ . await ?;
397+
398+ Ok :: < _ , sqlx:: Error > ( true )
399+ }
400+ Some ( _) => {
401+ counter ! ( crate :: prometheus:: CHUNKS_RETRIED_COUNTER ) . increment ( 1 ) ;
402+ // When retrying, the chunk re-enters ('stays') in the backlog,
403+ // so we *don't* decrement the backlog gauge here.
404+ Ok :: < _ , sqlx:: Error > ( false )
405+ }
406+ None => {
407+ tracing:: warn!(
408+ "Could not fail chunk {:?} because it was either: \
409+ completed, failed, cancelled, or paused before. Ignoring.",
410+ chunk_id
411+ ) ;
412+
413+ Ok :: < _ , sqlx:: Error > ( false )
414+ }
396415 }
397416 } )
398417 } )
0 commit comments