Skip to content

Commit 9ee94b3

Browse files
committed
Make tests timeout properly, in preparation for showing failure logs properly
1 parent ed52344 commit 9ee94b3

4 files changed

Lines changed: 117 additions & 66 deletions

File tree

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def run_submission(
9898
serialization_format: SerializationFormat = DEFAULT_SERIALIZATION_FORMAT,
9999
metadata: None | bytes = None,
100100
strategic_metadata: None | dict[str, int] = None,
101+
timeout: float | None = None,
101102
) -> Iterator[Any]:
102103
"""
103104
Inserts a submission into the queue, and blocks until it is completed.
@@ -118,6 +119,7 @@ def run_submission(
118119
metadata=metadata,
119120
strategic_metadata=strategic_metadata,
120121
chunk_size=chunk_size,
122+
timeout=timeout,
121123
)
122124
return _unchunk_iterator(results_iter, serialization_format)
123125

@@ -171,6 +173,7 @@ def blocking_stream_completed_submission(
171173
submission_id: SubmissionId,
172174
*,
173175
serialization_format: SerializationFormat = DEFAULT_SERIALIZATION_FORMAT,
176+
timeout: float | None = None,
174177
) -> Iterator[Any]:
175178
"""
176179
Blocks until the submission is completed.
@@ -183,7 +186,7 @@ def blocking_stream_completed_submission(
183186
(after retrying a consumer kept failing on one of the chunks)
184187
"""
185188
return _unchunk_iterator(
186-
self.blocking_stream_completed_submission_chunks(submission_id),
189+
self.blocking_stream_completed_submission_chunks(submission_id, timeout),
187190
serialization_format,
188191
)
189192

@@ -213,6 +216,7 @@ def run_submission_chunks(
213216
metadata: None | bytes = None,
214217
strategic_metadata: None | dict[str, int] = None,
215218
chunk_size: None | int = None,
219+
timeout: float | None = None,
216220
) -> Iterator[bytes]:
217221
"""
218222
Inserts an already-chunked submission into the queue, and blocks until it is completed.
@@ -231,7 +235,7 @@ def run_submission_chunks(
231235
strategic_metadata=strategic_metadata,
232236
chunk_size=chunk_size,
233237
)
234-
return self.blocking_stream_completed_submission_chunks(submission_id)
238+
return self.blocking_stream_completed_submission_chunks(submission_id, timeout)
235239

236240
async def async_run_submission_chunks(
237241
self,
@@ -280,7 +284,9 @@ def insert_submission_chunks(
280284
)
281285

282286
def blocking_stream_completed_submission_chunks(
283-
self, submission_id: SubmissionId
287+
self,
288+
submission_id: SubmissionId,
289+
timeout: float | None = None,
284290
) -> Iterator[bytes]:
285291
"""
286292
Blocks until the submission is completed, and returns an iterator that lazily
@@ -291,7 +297,7 @@ def blocking_stream_completed_submission_chunks(
291297
- `SubmissionFailedError` if the submission failed permanently
292298
(after retrying a consumer kept failing on one of the chunks)
293299
"""
294-
return self.inner.blocking_stream_completed_submission_chunks(submission_id) # type: ignore[no-any-return]
300+
return self.inner.blocking_stream_completed_submission_chunks(submission_id, timeout) # type: ignore[no-any-return]
295301

296302
async def async_stream_completed_submission_chunks(
297303
self, submission_id: SubmissionId

‎libs/opsqueue_python/src/errors.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use opsqueue::common::errors::{
66
E, IncorrectUsage, SubmissionNotCancellable, SubmissionNotFound, TooManyMatchingSubmissions,
77
UnexpectedOpsqueueConsumerServerResponse,
88
};
9-
use pyo3::exceptions::PyBaseException;
9+
use pyo3::exceptions::{PyBaseException, PyTimeoutError};
1010
use pyo3::{Bound, PyErr, Python, import_exception};
1111

1212
use crate::common;
@@ -182,6 +182,12 @@ impl From<CError<UnexpectedOpsqueueConsumerServerResponse>> for PyErr {
182182
}
183183
}
184184

185+
impl From<CError<tokio::time::error::Elapsed>> for PyErr {
186+
fn from(_value: CError<tokio::time::error::Elapsed>) -> Self {
187+
PyTimeoutError::new_err("timeout was reached")
188+
}
189+
}
190+
185191
impl<T> From<PyErr> for CError<E<FatalPythonException, T>> {
186192
fn from(value: PyErr) -> Self {
187193
CError(E::L(FatalPythonException(value)))

‎libs/opsqueue_python/src/producer.rs‎

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use opsqueue::{
1818
producer::client::{Client as ActualClient, InternalProducerClientError},
1919
tracing::CarrierMap,
2020
};
21+
use tokio::time::error::Elapsed;
2122
use ux::u63;
2223

2324
use crate::{
@@ -390,52 +391,6 @@ impl ProducerClient {
390391
})
391392
}
392393

393-
#[pyo3(signature = (chunk_contents, metadata=None, strategic_metadata=None, chunk_size=None, otel_trace_carrier=CarrierMap::default()))]
394-
#[allow(clippy::result_large_err, clippy::type_complexity)]
395-
pub fn run_submission_chunks(
396-
&self,
397-
py: Python<'_>,
398-
chunk_contents: Py<PyIterator>,
399-
metadata: Option<submission::Metadata>,
400-
strategic_metadata: Option<StrategicMetadataMap>,
401-
chunk_size: Option<i64>,
402-
otel_trace_carrier: CarrierMap,
403-
) -> CPyResult<
404-
PyChunksIter,
405-
E![
406-
FatalPythonException,
407-
errors::SubmissionFailed,
408-
ChunksStorageError,
409-
InternalProducerClientError,
410-
],
411-
> {
412-
let submission_id = self
413-
.insert_submission_chunks(
414-
py,
415-
chunk_contents,
416-
metadata,
417-
strategic_metadata,
418-
chunk_size,
419-
otel_trace_carrier,
420-
)
421-
.map_err(|CError(e)| {
422-
CError(match e {
423-
L(e) => L(e),
424-
R(e) => R(R(e)),
425-
})
426-
})?;
427-
let res = self
428-
.blocking_stream_completed_submission_chunks(py, submission_id)
429-
.map_err(|CError(e)| {
430-
CError(match e {
431-
L(e) => L(e),
432-
R(L(e)) => R(L(e)),
433-
R(R(e)) => R(R(R(e))),
434-
})
435-
})?;
436-
Ok(res)
437-
}
438-
439394
/// Blocks (and short-polls) until the submission is completed.
440395
///
441396
/// We start with a small short-polling interval
@@ -447,17 +402,34 @@ impl ProducerClient {
447402
&self,
448403
py: Python<'_>,
449404
submission_id: SubmissionId,
405+
timeout: Option<f64>,
450406
) -> CPyResult<
451407
PyChunksIter,
452408
E![
453409
FatalPythonException,
410+
Elapsed,
454411
errors::SubmissionFailed,
455412
InternalProducerClientError
456413
],
457414
> {
458415
py.detach(|| {
459416
self.block_unless_interrupted(async move {
460-
self.stream_completed_submission_chunks(submission_id).await
417+
let fut = self.stream_completed_submission_chunks(submission_id);
418+
match timeout {
419+
Some(duration) => tokio::time::timeout(Duration::from_secs_f64(duration), fut)
420+
.await
421+
.map_err(|err| CError(R(L(err))))
422+
.and_then(|err| {
423+
err.map_err(|err| match err.0 {
424+
L(err) => CError(L(err)),
425+
R(err) => CError(R(R(err))),
426+
})
427+
}),
428+
None => fut.await.map_err(|err| match err.0 {
429+
L(err) => CError(L(err)),
430+
R(err) => CError(R(R(err))),
431+
}),
432+
}
461433
})
462434
})
463435
}

‎libs/opsqueue_python/tests/test_roundtrip.py‎

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
)
3030
import logging
3131
import multiprocess # type: ignore[import-untyped]
32-
32+
import time
3333
import pytest
3434

35+
SUBMISSION_COMPLETED_TIMEOUT = 10.0
36+
3537

3638
def increment(data: int) -> int:
3739
return data + 1
@@ -57,7 +59,10 @@ def run_consumer() -> None:
5759
input_iter = range(0, 100)
5860

5961
output_iter: Iterator[int] = producer_client.run_submission(
60-
input_iter, chunk_size=20, strategic_metadata={"id": 42}
62+
input_iter,
63+
chunk_size=20,
64+
strategic_metadata={"id": 42},
65+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
6166
)
6267
res = sum(output_iter)
6368

@@ -127,6 +132,7 @@ def run_consumer(_consumer_id: int) -> None:
127132
input_iter,
128133
chunk_size=chunk_size,
129134
strategic_metadata={"id": 42, "second_id": 69},
135+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
130136
)
131137
res = sum(output_iter)
132138

@@ -144,7 +150,11 @@ def test_empty_submission(opsqueue: OpsqueueProcess) -> None:
144150
)
145151

146152
input_iter: list[int] = []
147-
output_iter: Iterator[int] = producer_client.run_submission(input_iter, chunk_size=20)
153+
output_iter: Iterator[int] = producer_client.run_submission(
154+
input_iter,
155+
chunk_size=20,
156+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
157+
)
148158
res = sum(output_iter)
149159
assert res == 0
150160

@@ -179,7 +189,10 @@ def run_consumer() -> None:
179189
input_iter = range(0, 100)
180190

181191
output_iter: Iterator[int] = producer_client.run_submission(
182-
input_iter, chunk_size=20, serialization_format=serialization_format
192+
input_iter,
193+
chunk_size=20,
194+
serialization_format=serialization_format,
195+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
183196
)
184197
res = sum(output_iter)
185198

@@ -222,7 +235,11 @@ def broken_increment(input: int) -> float:
222235
input_iter = range(0, 100)
223236

224237
with pytest.raises(SubmissionFailedError) as exc_info:
225-
producer_client.run_submission(input_iter, chunk_size=20)
238+
producer_client.run_submission(
239+
input_iter,
240+
chunk_size=20,
241+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
242+
)
226243

227244
# We expect the intended attributes to be there:
228245
assert isinstance(exc_info.value.failure, str)
@@ -262,7 +279,10 @@ def increment_list(ints: Sequence[int], _chunk: Chunk) -> Sequence[int]:
262279
input_iter = map(lambda i: cbor2.dumps([i, i, i]), range(0, 10))
263280
output_iter: Iterator[list[int]] = map(
264281
lambda c: cbor2.loads(c),
265-
producer_client.run_submission_chunks(input_iter),
282+
producer_client.run_submission_chunks(
283+
input_iter,
284+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
285+
),
266286
)
267287
import itertools
268288

@@ -300,7 +320,11 @@ def run_consumer(consumer_id: int) -> None:
300320
n_consumers = 16
301321
with multiple_background_processes(run_consumer, n_consumers) as _consumers:
302322
input_iter = range(0, 1000)
303-
output_iter: Iterator[int] = producer_client.run_submission(input_iter, chunk_size=100)
323+
output_iter: Iterator[int] = producer_client.run_submission(
324+
input_iter,
325+
chunk_size=100,
326+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
327+
)
304328
res = sum(output_iter)
305329

306330
assert res == sum(range(1, 1001))
@@ -372,7 +396,10 @@ def run_consumer() -> None:
372396

373397
with background_process(run_consumer):
374398
# Wait for the submission to complete.
375-
producer_client.blocking_stream_completed_submission(submission_id)
399+
producer_client.blocking_stream_completed_submission(
400+
submission_id,
401+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
402+
)
376403
submission = producer_client.get_submission_status(submission_id)
377404
assert submission is not None
378405
assert isinstance(submission.submission, SubmissionCompleted)
@@ -416,7 +443,10 @@ def assert_submission_failed_has_metadata(x: SubmissionFailed) -> None:
416443

417444
with pytest.raises(SubmissionFailedError) as exc_info:
418445
# Wait for the submission to fail.
419-
producer_client.blocking_stream_completed_submission(submission_id)
446+
producer_client.blocking_stream_completed_submission(
447+
submission_id,
448+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
449+
)
420450
assert_submission_failed_has_metadata(exc_info.value.submission)
421451

422452
submission = producer_client.get_submission_status(submission_id)
@@ -504,7 +534,10 @@ def run_consumer() -> None:
504534

505535
with background_process(run_consumer):
506536
# Wait for the submission to complete.
507-
producer_client.blocking_stream_completed_submission(submission_id)
537+
producer_client.blocking_stream_completed_submission(
538+
submission_id,
539+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
540+
)
508541
submission = producer_client.get_submission_status(submission_id)
509542
assert submission is not None
510543
assert isinstance(submission.submission, SubmissionCompleted)
@@ -537,7 +570,10 @@ def consume(x: int) -> None:
537570

538571
with background_process(run_consumer):
539572
with pytest.raises(SubmissionFailedError):
540-
producer_client.blocking_stream_completed_submission(submission_id)
573+
producer_client.blocking_stream_completed_submission(
574+
submission_id,
575+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
576+
)
541577
# Cancelling the failed submission should fail.
542578
with pytest.raises(SubmissionNotCancellableError) as exc_info:
543579
producer_client.cancel_submission(submission_id)
@@ -567,7 +603,10 @@ def consume(x: int) -> int | None:
567603

568604
with background_process(run_consumer):
569605
with pytest.raises(SubmissionFailedError) as exc_info:
570-
producer_client.blocking_stream_completed_submission(submission_id)
606+
producer_client.blocking_stream_completed_submission(
607+
submission_id,
608+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
609+
)
571610
assert exc_info.value.submission.chunks_done == len(chunks) - 1
572611

573612

@@ -695,7 +734,10 @@ def run_consumer() -> None:
695734
consumer_client.run_each_op(lambda x: x)
696735

697736
with background_process(run_consumer):
698-
producer_client.blocking_stream_completed_submission(submission_id)
737+
producer_client.blocking_stream_completed_submission(
738+
submission_id,
739+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
740+
)
699741
assert isinstance(
700742
producer_client.get_submission_status(submission_id),
701743
SubmissionStatus.Completed,
@@ -744,7 +786,10 @@ def process_op(x: int) -> int:
744786
# Release the consumer.
745787
can_continue.set()
746788

747-
producer_client.blocking_stream_completed_submission(submission_id)
789+
producer_client.blocking_stream_completed_submission(
790+
submission_id,
791+
timeout=SUBMISSION_COMPLETED_TIMEOUT,
792+
)
748793
assert isinstance(
749794
producer_client.get_submission_status(submission_id),
750795
SubmissionStatus.Completed,
@@ -778,3 +823,25 @@ def test_cancel_paused(opsqueue: OpsqueueProcess) -> None:
778823
assert isinstance(
779824
producer_client.get_submission_status(submission_id), SubmissionStatus.Cancelled
780825
)
826+
827+
828+
def test_run_submission_timeout(opsqueue: OpsqueueProcess) -> None:
829+
url = "file:///tmp/opsqueue/test_run_submission_timeout"
830+
producer_client = ProducerClient(f"localhost:{opsqueue.port}", url)
831+
832+
def run_consumer() -> None:
833+
consumer_client = ConsumerClient(f"localhost:{opsqueue.port}", url)
834+
835+
def process_op(x: int) -> int:
836+
time.sleep(2.0)
837+
return x
838+
839+
consumer_client.run_each_op(process_op)
840+
841+
with background_process(run_consumer) as _consumer:
842+
with pytest.raises(TimeoutError):
843+
producer_client.run_submission(
844+
[1],
845+
chunk_size=1,
846+
timeout=0.1,
847+
)

0 commit comments

Comments
 (0)