Skip to content

Commit 7e7052d

Browse files
committed
Make complete_chunk and fail_chunk not error when processing previously completed, failed, or paused chunks
1 parent bcd4161 commit 7e7052d

4 files changed

Lines changed: 76 additions & 89 deletions

File tree

‎libs/opsqueue_python/python/opsqueue/exceptions.py‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,6 @@ class TryFromIntError(IncorrectUsageError):
9292
pass
9393

9494

95-
class ChunkNotFoundError(IncorrectUsageError):
96-
"""
97-
Raised when a method is used to look up information about a chunk
98-
but the chunk doesn't exist within the Opsqueue.
99-
"""
100-
101-
pass
102-
103-
10495
class SubmissionNotFoundError(IncorrectUsageError):
10596
"""
10697
Raised when a method is used to look up information about a submission

‎libs/opsqueue_python/src/errors.rs‎

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,21 @@
22
/// so we have nice IDE support for docs-on-hover and for 'go to definition'.
33
use std::error::Error;
44

5-
use opsqueue::common::chunk::ChunkId;
65
use opsqueue::common::errors::{
7-
ChunkNotFound, IncorrectUsage, SubmissionNotCancellable, SubmissionNotFound,
6+
IncorrectUsage, SubmissionNotCancellable, SubmissionNotFound,
87
UnexpectedOpsqueueConsumerServerResponse, E,
98
};
109
use pyo3::exceptions::PyBaseException;
1110
use pyo3::{import_exception, Bound, PyErr, Python};
1211

1312
use crate::common;
14-
use crate::common::{ChunkIndex, SubmissionId};
1513

1614
// Expected errors:
1715
import_exception!(opsqueue.exceptions, SubmissionFailedError);
1816

1917
// Incorrect usage errors:
2018
import_exception!(opsqueue.exceptions, IncorrectUsageError);
2119
import_exception!(opsqueue.exceptions, TryFromIntError);
22-
import_exception!(opsqueue.exceptions, ChunkNotFoundError);
2320
import_exception!(opsqueue.exceptions, SubmissionNotFoundError);
2421
import_exception!(opsqueue.exceptions, SubmissionNotCancellableError);
2522
import_exception!(opsqueue.exceptions, NewObjectStoreClientError);
@@ -166,22 +163,6 @@ impl From<CError<crate::producer::SubmissionNotCompletedYetError>> for PyErr {
166163
}
167164
}
168165

169-
impl From<CError<ChunkNotFound>> for PyErr {
170-
fn from(value: CError<ChunkNotFound>) -> Self {
171-
let ChunkId {
172-
submission_id,
173-
chunk_index,
174-
} = value.0 .0;
175-
ChunkNotFoundError::new_err((
176-
value.0.to_string(),
177-
(
178-
SubmissionId::from(submission_id),
179-
ChunkIndex::from(chunk_index),
180-
),
181-
))
182-
}
183-
}
184-
185166
impl From<CError<opsqueue::object_store::NewObjectStoreClientError>> for PyErr {
186167
fn from(value: CError<opsqueue::object_store::NewObjectStoreClientError>) -> Self {
187168
NewObjectStoreClientError::new_err(value.0.to_string())

‎opsqueue/src/common/chunk.rs‎

Lines changed: 74 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,10 @@ impl Chunk {
218218
#[cfg(feature = "server-logic")]
219219
pub 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
})

‎opsqueue/src/common/errors.rs‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use thiserror::Error;
1212
use crate::consumer::common::SyncServerToClientResponse;
1313

1414
use super::{
15-
chunk::{ChunkFailed, ChunkId},
15+
chunk::ChunkFailed,
1616
submission::{SubmissionCancelled, SubmissionCompleted, SubmissionFailed, SubmissionId},
1717
};
1818

@@ -31,10 +31,6 @@ impl<T> From<DatabaseError> for E<DatabaseError, T> {
3131
}
3232
}
3333

34-
#[derive(Error, Debug)]
35-
#[error("Chunk not found for ID {0:?}")]
36-
pub struct ChunkNotFound(pub ChunkId);
37-
3834
#[derive(Error, Debug, Deserialize, Serialize)]
3935
#[error("Submission not found for ID {0:?}")]
4036
pub struct SubmissionNotFound(pub SubmissionId);

0 commit comments

Comments
 (0)