Skip to content

Commit 5144caa

Browse files
author
Scott Collins
committed
wip failure simulation
1 parent 8f8947e commit 5144caa

6 files changed

Lines changed: 162 additions & 15 deletions

File tree

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ install_requires =
3434
more-itertools>=9.0,<10.8
3535
joblib>=1.3.1,<1.6.0
3636
requests~=2.23
37+
requests-mock~=1.12.1
3738
types-requests~=2.23
3839
PyYAML~=6.0
3940
setuptools~=75.8.1

src/pds/ingress/client/pds_ingress_client.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
import backoff
2222
import pds.ingress.util.log_util as log_util
2323
import requests
24+
import requests_mock
2425
from joblib import delayed
2526
from joblib import Parallel
2627
from more_itertools import chunked as batched
2728
from pds.ingress import __version__
2829
from pds.ingress.util.auth_util import AuthUtil
30+
from pds.ingress.util.backoff_util import simulate_batch_request_failure
31+
from pds.ingress.util.backoff_util import simulate_ingress_failure
2932
from pds.ingress.util.config_util import ConfigUtil
3033
from pds.ingress.util.hash_util import md5_for_path
3134
from pds.ingress.util.log_util import get_log_level
@@ -193,7 +196,8 @@ def _process_batch(batch_index, request_batch, node_id, force_overwrite, api_gat
193196
except Exception as err:
194197
# If here, the HTTP request error was unrecoverable by a backoff/retry
195198
trimmed_path = ingress_response.get("trimmed_path")
196-
update_summary_table(SUMMARY_TABLE, "failed", trimmed_path)
199+
ingress_path = ingress_response.get("ingress_path")
200+
update_summary_table(SUMMARY_TABLE, "failed", ingress_path)
197201

198202
logger.error("Batch %d : Ingress failed for %s, Reason:\n%s", batch_index, trimmed_path, str(err))
199203

@@ -352,10 +356,11 @@ def _prepare_batch_for_ingress(ingress_path_batch, prefix, batch_index, batch_pb
352356
@backoff.on_exception(
353357
backoff.expo,
354358
Exception,
355-
max_time=120,
359+
max_time=10, # TODO: for testing only, revert to 120
356360
logger="request_batch_for_ingress",
357361
)
358-
def request_batch_for_ingress(request_batch, batch_index, node_id, force_overwrite, api_gateway_config):
362+
@requests_mock.Mocker(kw="mock_requests", real_http=True)
363+
def request_batch_for_ingress(request_batch, batch_index, node_id, force_overwrite, api_gateway_config, **kwargs):
359364
"""
360365
Submits a batch of ingress requests to the PDS Ingress App API.
361366
@@ -409,9 +414,13 @@ def request_batch_for_ingress(request_batch, batch_index, node_id, force_overwri
409414
"x-amz-docs-region": api_gateway_region,
410415
}
411416

417+
# Simulate a random failure for the batch request if configured to do so
418+
simulate_batch_request_failure(kwargs["mock_requests"], api_gateway_url.split("?")[0])
419+
412420
response = requests.post(
413421
api_gateway_url, params=params, data=json.dumps(request_batch), headers=headers, timeout=600
414422
)
423+
415424
elapsed_time = time.time() - start_time
416425

417426
# Ingress request successful
@@ -428,10 +437,11 @@ def request_batch_for_ingress(request_batch, batch_index, node_id, force_overwri
428437
@backoff.on_exception(
429438
backoff.expo,
430439
Exception,
431-
max_time=120,
440+
max_time=10, # TODO: for testing only, revert to 120
432441
logger="ingress_file_to_s3",
433442
)
434-
def ingress_file_to_s3(ingress_response, batch_index, batch_pbar):
443+
#@requests_mock.Mocker(kw="mock_requests", real_http=True)
444+
def ingress_file_to_s3(ingress_response, batch_index, batch_pbar, **kwargs):
435445
"""
436446
Copies the local file path using the pre-signed S3 URL returned from the
437447
Ingress Lambda App.
@@ -487,13 +497,17 @@ def ingress_file_to_s3(ingress_response, batch_index, batch_pbar):
487497
batch_pbar, total=os.stat(ingress_path).st_size, filename=os.path.basename(ingress_path)
488498
)
489499

490-
with open(ingress_path, "rb") as infile:
491-
# Wrap file I/O with our upload bar to automatically track file upload progress
492-
wrapped_file = CallbackIOWrapper(upload_pbar.update, infile, "read")
500+
with requests_mock.Mocker(real_http=True) as mock_requests:
501+
# Simulate a random failure for the S3 ingress request if configured to do so
502+
simulate_ingress_failure(mock_requests, s3_ingress_url.split("?")[0])
493503

494-
# Only send the file data if the file is non-empty
495-
response = requests.put(s3_ingress_url, data=wrapped_file if file_length > 0 else b"", headers=headers)
496-
response.raise_for_status()
504+
with open(ingress_path, "rb") as infile:
505+
# Wrap file I/O with our upload bar to automatically track file upload progress
506+
wrapped_file = CallbackIOWrapper(upload_pbar.update, infile, "read")
507+
508+
# Only send the file data if the file is non-empty
509+
response = requests.put(s3_ingress_url, data=wrapped_file if file_length > 0 else b"", headers=headers)
510+
response.raise_for_status()
497511

498512
logger.info("Batch %d : %s Ingest complete", batch_index, trimmed_path)
499513
update_summary_table(SUMMARY_TABLE, "uploaded", ingress_path)
@@ -517,10 +531,11 @@ def ingress_file_to_s3(ingress_response, batch_index, batch_pbar):
517531
@backoff.on_exception(
518532
backoff.expo,
519533
Exception,
520-
max_time=120,
534+
max_time=10, # TODO: for testing only, revert to 120
521535
logger="ingress_multipart_file_to_s3",
522536
)
523-
def ingress_multipart_file_to_s3(ingress_response, batch_index, batch_pbar):
537+
@requests_mock.Mocker(kw="mock_requests", real_http=True)
538+
def ingress_multipart_file_to_s3(ingress_response, batch_index, batch_pbar, **kwargs):
524539
"""
525540
Performs an ingress request for a file that is too large to be uploaded
526541
in a single request. The file is instead uploaded in multiple parts using
@@ -582,6 +597,9 @@ def ingress_multipart_file_to_s3(ingress_response, batch_index, batch_pbar):
582597
upload_pbar, f"{os.path.basename(ingress_path)} (Part {part_number}/{len(s3_ingress_urls)})"
583598
)
584599

600+
# Simulate a random failure for the S3 ingress request if configured to do so
601+
simulate_ingress_failure(kwargs["mock_requests"], s3_ingress_url.split("?")[0])
602+
585603
# Submit a single chunk to AWS
586604
response = requests.put(s3_ingress_url, data=next(chunk_iterator))
587605
response.raise_for_status()
@@ -848,16 +866,22 @@ def main(args):
848866
try:
849867
init_batch_progress_bars(args.num_threads)
850868
perform_ingress(request_batchs, node_id, args.force_overwrite, config["API_GATEWAY"])
869+
finally:
870+
close_batch_progress_bars()
851871

852-
logger.info("All batches processed")
872+
logger.info("All batches processed")
853873

874+
try:
854875
if len(SUMMARY_TABLE["failed"]) > 0:
876+
logger.info("----------------------------------------")
855877
logger.info("Reattempting ingress for failed files...")
878+
856879
failed_ingresses = SUMMARY_TABLE["failed"]
857880
batched_failed_ingresses = list(batched(failed_ingresses, batch_size))
858881
failed_request_batchs = prepare_batches(batched_failed_ingresses, args.prefix)
882+
883+
init_batch_progress_bars(args.num_threads)
859884
perform_ingress(failed_request_batchs, node_id, args.force_overwrite, config["API_GATEWAY"])
860-
logger.info("Reattempted ingress complete")
861885
finally:
862886
close_batch_progress_bars()
863887

src/pds/ingress/conf.default.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ console_format = "%(message)s"
1919
log_group_name = "/pds/nucleus/dum/client-log-group"
2020
log_file_path =
2121
batch_size = 250
22+
23+
[DEBUG]
24+
simulate_batch_request_failures = false
25+
batch_request_failure_rate = 0
26+
simulate_ingress_failures = false
27+
ingress_failure_rate = 0

src/pds/ingress/util/backoff_util.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
automatic backoff/retry of HTTP requests.
88
99
"""
10+
import random
11+
12+
from distutils.util import strtobool
1013
from http import HTTPStatus
1114

15+
from pds.ingress.util.config_util import ConfigUtil
16+
1217
# When leveraging this module with the Lambda service functions, the requests
1318
# module will not be available within the Python runtime.
1419
# It should not be needed by those Lambda's, so use MagicMock to bypass any
@@ -60,3 +65,107 @@ def fatal_code(err: requests.exceptions.RequestException) -> bool:
6065
else:
6166
# No response to interrogate, so default to no retry
6267
return True
68+
69+
70+
def check_failure_chance(percentage: int) -> bool:
71+
"""
72+
Checks if a simulated failure event should occur based on a given percentage
73+
chance.
74+
75+
Parameters
76+
----------
77+
percentage : int
78+
The desired percentage chance (e.g., 70 for 70%).
79+
80+
Returns
81+
-------
82+
bool: True if the failure should occur, False otherwise.
83+
84+
Raises
85+
------
86+
ValueError: If the percentage is not between 0 and 100.
87+
88+
"""
89+
if not (0 <= percentage <= 100):
90+
raise ValueError("Percentage must be between 0 and 100.")
91+
92+
# Generate a random float between 0.0 and 1.0
93+
random_number = random.random()
94+
95+
# Convert the percentage to a decimal for comparison
96+
chance_threshold = percentage / 100.0
97+
98+
return random_number < chance_threshold
99+
100+
101+
def simulate_requests_failure(mock_requests, s3_ingress_url, http_method, enable_key, failure_rate_key):
102+
"""
103+
Simulates a random failure for S3 ingress by registering the provided
104+
ingress URL with the requests mocker to raise an HTTPError exception.
105+
106+
Whether the failure is simulated is determined by the `simulate_ingress_failures`
107+
configuration option and the `ingress_failure_rate` percentage chance within
108+
the optional DEBUG section of the INI config. If this section is not present,
109+
this function should always default to not simulating a failure.
110+
111+
If the failure is not simulated, the mock_requests instance is reset to
112+
ensure no previous failures are registered.
113+
114+
Parameters
115+
----------
116+
mock_requests : requests_mock.Mocker
117+
Mocked requests instance to register the simulated failure.
118+
s3_ingress_url : str
119+
The S3 ingress URL to which the simulated failure will be applied.
120+
http_method : str
121+
HTTP method to register the failure for (e.g., 'POST', 'PUT').
122+
enable_key : str
123+
Name of the INI key to check if failure simulation is enabled.
124+
failure_rate_key : str
125+
Name of the INI key that specifies the percentage chance of failure.
126+
127+
"""
128+
config = ConfigUtil.get_config()
129+
130+
# Check if simulated failures are enabled, and if so, if we should simulate
131+
# a failure via mock_requests based on the configured failure chance
132+
if bool(strtobool(config.get("DEBUG", enable_key, fallback="false"))):
133+
if check_failure_chance(int(config.get("DEBUG", failure_rate_key, fallback="0"))):
134+
mock_requests.register_uri(http_method, s3_ingress_url, exc=requests.exceptions.HTTPError)
135+
else:
136+
# Remove any previously registered URL
137+
mock_requests.reset()
138+
139+
140+
def simulate_batch_request_failure(mock_requests, s3_ingress_url):
141+
"""
142+
Simulates a random failure for an ingress batch request by registering the
143+
provided ingress URL with the requests mocker to raise an HTTPError exception.
144+
145+
Parameters
146+
----------
147+
mock_requests : requests_mock.Mocker
148+
Mocked requests instance to register the simulated failure.
149+
s3_ingress_url : str
150+
The S3 ingress URL to which the simulated failure will be applied.
151+
152+
"""
153+
simulate_requests_failure(
154+
mock_requests, s3_ingress_url, "POST", "simulate_batch_request_failures", "batch_request_failure_rate"
155+
)
156+
157+
158+
def simulate_ingress_failure(mock_requests, s3_ingress_url):
159+
"""
160+
Simulates a random failure for S3 ingress by registering the provided
161+
ingress URL with the requests mocker to raise an HTTPError exception.
162+
163+
Parameters
164+
----------
165+
mock_requests : requests_mock.Mocker
166+
Mocked requests instance to register the simulated failure.
167+
s3_ingress_url : str
168+
The S3 ingress URL to which the simulated failure will be applied.
169+
170+
"""
171+
simulate_requests_failure(mock_requests, s3_ingress_url, "PUT", "simulate_ingress_failures", "ingress_failure_rate")

src/pds/ingress/util/progress_util.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,12 @@ def update_upload_pbar_filename(upload_pbar, filename):
279279

280280
def close_batch_progress_bars():
281281
"""Closes all Batch progress bars and associated Upload sub-bars."""
282+
global BATCH_BARS, TOTAL_INGRESS_BAR # noqa F824
283+
282284
with BATCH_SEMAPHORE:
283285
for batch_pbar in BATCH_BARS:
284286
batch_pbar.upload_pbar.close()
285287
batch_pbar.close()
288+
289+
BATCH_BARS.clear()
290+
TOTAL_INGRESS_BAR = None

src/pds/ingress/util/report_util.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def update_summary_table(summary_table, key, paths):
5050
The key in the summary table to update (e.g., "uploaded", "skipped", "failed").
5151
paths : str or list of str
5252
The path value (or values) to add to the summary table for the specified key.
53+
Note, these paths should be the absolute paths to files that were processed,
54+
not the "trimmed" relative paths.
5355
5456
"""
5557
if key not in ("uploaded", "skipped", "failed", "unprocessed"):

0 commit comments

Comments
 (0)