|
| 1 | +import contextlib |
1 | 2 | import itertools |
2 | 3 | import json |
3 | 4 | import logging |
|
6 | 7 | from typing import Optional |
7 | 8 | from unittest import mock |
8 | 9 |
|
| 10 | +import dirty_equals |
| 11 | +import httpretty |
9 | 12 | import pytest |
10 | 13 | import requests |
11 | 14 |
|
12 | 15 | import openeo |
13 | 16 | import openeo.rest.job |
14 | | -from openeo.rest import JobFailedException, OpenEoApiPlainError, OpenEoClientException |
| 17 | +from openeo.rest import ( |
| 18 | + DEFAULT_JOB_STATUS_POLL_CONNECTION_RETRY_INTERVAL, |
| 19 | + JobFailedException, |
| 20 | + OpenEoApiPlainError, |
| 21 | + OpenEoClientException, |
| 22 | +) |
15 | 23 | from openeo.rest.job import BatchJob, ResultAsset |
16 | 24 | from openeo.rest.models.general import Link |
17 | 25 | from openeo.rest.models.logs import LogEntry |
@@ -311,6 +319,90 @@ def test_execute_batch_with_excessive_soft_errors(con100, requests_mock, tmpdir, |
311 | 319 | ] |
312 | 320 |
|
313 | 321 |
|
| 322 | +@httpretty.activate(allow_net_connect=False) |
| 323 | +@pytest.mark.parametrize( |
| 324 | + ["retry", "expectation_context", "expected_sleeps"], |
| 325 | + [ |
| 326 | + ( # Default retry settings |
| 327 | + None, |
| 328 | + contextlib.nullcontext(), |
| 329 | + [0.1, 23, 34], |
| 330 | + ), |
| 331 | + ( |
| 332 | + # Only retry on 429 (and fail on 500) |
| 333 | + {"status_forcelist": [429]}, |
| 334 | + pytest.raises(OpenEoApiPlainError, match=re.escape("[500] Internal Server Error")), |
| 335 | + [0.1, 23, DEFAULT_JOB_STATUS_POLL_CONNECTION_RETRY_INTERVAL], |
| 336 | + ), |
| 337 | + ( |
| 338 | + # No retry setup |
| 339 | + False, |
| 340 | + pytest.raises(OpenEoApiPlainError, match=re.escape("[429] Too Many Requests")), |
| 341 | + [0.1], |
| 342 | + ), |
| 343 | + ], |
| 344 | +) |
| 345 | +def test_execute_batch_retry_after_429_too_many_requests(tmpdir, retry, expectation_context, expected_sleeps): |
| 346 | + httpretty.register_uri( |
| 347 | + httpretty.GET, |
| 348 | + uri=API_URL + "/", |
| 349 | + body=json.dumps({"api_version": "1.0.0", "endpoints": [{"path": "/credentials/basic", "methods": ["GET"]}]}), |
| 350 | + ) |
| 351 | + httpretty.register_uri( |
| 352 | + httpretty.GET, |
| 353 | + uri=API_URL + "/file_formats", |
| 354 | + body=json.dumps({"output": {"GTiff": {"gis_data_types": ["raster"]}}}), |
| 355 | + ) |
| 356 | + httpretty.register_uri( |
| 357 | + httpretty.GET, |
| 358 | + uri=API_URL + "/collections/SENTINEL2", |
| 359 | + body=json.dumps({"foo": "bar"}), |
| 360 | + ) |
| 361 | + httpretty.register_uri( |
| 362 | + httpretty.POST, uri=API_URL + "/jobs", status=201, adding_headers={"OpenEO-Identifier": "f00ba5"}, body="" |
| 363 | + ) |
| 364 | + httpretty.register_uri(httpretty.POST, uri=API_URL + "/jobs/f00ba5/results", status=202) |
| 365 | + httpretty.register_uri( |
| 366 | + httpretty.GET, |
| 367 | + uri=API_URL + "/jobs/f00ba5", |
| 368 | + responses=[ |
| 369 | + httpretty.Response(body=json.dumps({"status": "queued"})), |
| 370 | + httpretty.Response(status=429, body="Too Many Requests", adding_headers={"Retry-After": "23"}), |
| 371 | + httpretty.Response(body=json.dumps({"status": "running", "progress": 80})), |
| 372 | + httpretty.Response(status=502, body="Bad Gateway"), |
| 373 | + httpretty.Response(status=500, body="Internal Server Error"), |
| 374 | + httpretty.Response(body=json.dumps({"status": "running", "progress": 80})), |
| 375 | + httpretty.Response(status=429, body="Too Many Requests", adding_headers={"Retry-After": "34"}), |
| 376 | + httpretty.Response(body=json.dumps({"status": "finished", "progress": 100})), |
| 377 | + ], |
| 378 | + ) |
| 379 | + httpretty.register_uri( |
| 380 | + httpretty.GET, |
| 381 | + uri=API_URL + "/jobs/f00ba5/results", |
| 382 | + body=json.dumps( |
| 383 | + { |
| 384 | + "assets": { |
| 385 | + "output.tiff": { |
| 386 | + "href": API_URL + "/jobs/f00ba5/files/output.tiff", |
| 387 | + "type": "image/tiff; application=geotiff", |
| 388 | + }, |
| 389 | + } |
| 390 | + } |
| 391 | + ), |
| 392 | + ) |
| 393 | + httpretty.register_uri(httpretty.GET, uri=API_URL + "/jobs/f00ba5/files/output.tiff", body="tiffdata") |
| 394 | + httpretty.register_uri(httpretty.GET, uri=API_URL + "/jobs/f00ba5/logs", body=json.dumps({"logs": []})) |
| 395 | + |
| 396 | + con = openeo.connect(API_URL, retry=retry) |
| 397 | + |
| 398 | + with mock.patch("time.sleep") as sleep_mock: |
| 399 | + job = con.load_collection("SENTINEL2").create_job() |
| 400 | + with expectation_context: |
| 401 | + job.start_and_wait(max_poll_interval=0.1) |
| 402 | + |
| 403 | + assert sleep_mock.call_args_list == dirty_equals.Contains(*(mock.call(s) for s in expected_sleeps)) |
| 404 | + |
| 405 | + |
314 | 406 | class LogGenerator: |
315 | 407 | """Helper to generate log entry (dicts) with auto-generated ids, messages, etc.""" |
316 | 408 |
|
|
0 commit comments