Skip to content

Commit 71ace4e

Browse files
committed
Make complete_chunk and fail_chunk not error when processing previously completed, failed, or paused chunks
1 parent 2c15037 commit 71ace4e

4 files changed

Lines changed: 78 additions & 90 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: 2 additions & 21 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, E, IncorrectUsage, SubmissionNotCancellable, SubmissionNotFound,
8-
TooManyMatchingSubmissions, UnexpectedOpsqueueConsumerServerResponse,
6+
E, IncorrectUsage, SubmissionNotCancellable, SubmissionNotFound, TooManyMatchingSubmissions,
7+
UnexpectedOpsqueueConsumerServerResponse,
98
};
109
use pyo3::exceptions::PyBaseException;
1110
use pyo3::{Bound, PyErr, Python, import_exception};
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, TooManyMatchingSubmissionsError);
@@ -173,22 +170,6 @@ impl From<CError<crate::producer::SubmissionNotCompletedYetError>> for PyErr {
173170
}
174171
}
175172

176-
impl From<CError<ChunkNotFound>> for PyErr {
177-
fn from(value: CError<ChunkNotFound>) -> Self {
178-
let ChunkId {
179-
submission_id,
180-
chunk_index,
181-
} = value.0.0;
182-
ChunkNotFoundError::new_err((
183-
value.0.to_string(),
184-
(
185-
SubmissionId::from(submission_id),
186-
ChunkIndex::from(chunk_index),
187-
),
188-
))
189-
}
190-
}
191-
192173
impl From<CError<opsqueue::object_store::NewObjectStoreClientError>> for PyErr {
193174
fn from(value: CError<opsqueue::object_store::NewObjectStoreClientError>) -> Self {
194175
NewObjectStoreClientError::new_err(value.0.to_string())

‎opsqueue/src/common/chunk.rs‎

Lines changed: 75 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,11 @@ impl Chunk {
218218
#[cfg(feature = "server-logic")]
219219
pub mod db {
220220
use super::*;
221-
use crate::common::errors::{ChunkNotFound, DatabaseError, E, SubmissionNotFound};
221+
use crate::common::errors::{DatabaseError, E, SubmissionNotFound};
222222
use crate::db::{Connection, True, WriterConnection};
223223
use axum_prometheus::metrics::{counter, gauge};
224224
use sqlx::{QueryBuilder, Sqlite};
225-
use sqlx::{query, query_as};
225+
use sqlx::{query, query_as, query_scalar};
226226

227227
impl<'q> sqlx::Encode<'q, Sqlite> for super::ChunkIndex {
228228
fn encode_by_ref(
@@ -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,29 +323,43 @@ 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
// This does mean we potentially run the same chunk twice, but that is fine because we
344342
// assume chunks to be processed idempotently.
345343
//
346344
// (Not doing that resulted in a hard-to-track-down bug in the past.
347345
// https://github.com/channable/opsqueue/issues/76
348346
// )
349-
sqlx::query_scalar!(
350-
"UPDATE submissions SET chunks_done = chunks_done + 1 WHERE submissions.id = $1 RETURNING submissions.chunk_size;",
351-
chunk_id.submission_id,
352-
)
353-
.fetch_one(tx.get_inner())
354-
.await
355-
.map(|opt| opt.map(ChunkSize))
347+
if chunk_moved {
348+
sqlx::query_scalar!(
349+
"UPDATE submissions SET chunks_done = chunks_done + 1 WHERE submissions.id = $1 RETURNING submissions.chunk_size;",
350+
chunk_id.submission_id,
351+
)
352+
.fetch_one(tx.get_inner())
353+
.await?;
354+
} else {
355+
tracing::warn!(
356+
"Could not complete chunk {:?} because it was either: \
357+
completed, failed, cancelled, or paused before. Ignoring.",
358+
chunk_id
359+
);
360+
}
361+
362+
Ok(())
356363
}
357364

358365
#[tracing::instrument(skip(conn))]
@@ -369,7 +376,7 @@ pub mod db {
369376
submission_id,
370377
chunk_index,
371378
} = chunk_id;
372-
let fields = query!(
379+
let retries = query_scalar!(
373380
"
374381
UPDATE chunks SET retries = retries + 1
375382
WHERE submission_id = $1 AND chunk_index = $2
@@ -378,24 +385,37 @@ pub mod db {
378385
submission_id,
379386
chunk_index
380387
)
381-
.fetch_one(tx.get_inner())
388+
.fetch_optional(tx.get_inner())
382389
.await?;
383-
tracing::trace!("Retries: {}", fields.retries);
384-
if fields.retries >= max_retries.into() {
385-
crate::common::submission::db::fail_submission_notx(
386-
submission_id,
387-
chunk_index,
388-
failure,
389-
&mut tx,
390-
)
391-
.await?;
392-
393-
Ok::<_, sqlx::Error>(true)
394-
} else {
395-
counter!(crate::prometheus::CHUNKS_RETRIED_COUNTER).increment(1);
396-
// When retrying, the chunk re-enters ('stays') in the backlog,
397-
// so we *don't* decrement the backlog gauge here.
398-
Ok::<_, sqlx::Error>(false)
390+
match retries {
391+
Some(retries) => {
392+
tracing::trace!("Retries: {}", retries);
393+
if retries >= max_retries.into() {
394+
crate::common::submission::db::fail_submission_notx(
395+
submission_id,
396+
chunk_index,
397+
failure,
398+
&mut tx,
399+
)
400+
.await?;
401+
402+
Ok::<_, sqlx::Error>(true)
403+
} else {
404+
counter!(crate::prometheus::CHUNKS_RETRIED_COUNTER).increment(1);
405+
// When retrying, the chunk re-enters ('stays') in the backlog,
406+
// so we *don't* decrement the backlog gauge here.
407+
Ok::<_, sqlx::Error>(false)
408+
}
409+
}
410+
None => {
411+
tracing::warn!(
412+
"Could not fail chunk {:?} because it was either: \
413+
completed, failed, cancelled, or paused before. Ignoring.",
414+
chunk_id
415+
);
416+
417+
Ok::<_, sqlx::Error>(false)
418+
}
399419
}
400420
})
401421
})

‎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)