8
8
from moto .dynamodb2 .responses import dynamo_json_dump
9
9
10
10
from testifycompat import assert_equal
11
+ from testifycompat .assertions import assert_in
11
12
from tron .serialize .runstate .dynamodb_state_store import DynamoDBStateStore
13
+ from tron .serialize .runstate .dynamodb_state_store import MAX_UNPROCESSED_KEYS_RETRIES
12
14
13
15
14
16
def mock_transact_write_items (self ):
@@ -294,7 +296,8 @@ def test_delete_item_with_json_partitions(self, store, small_object, large_objec
294
296
vals = store .restore ([key ])
295
297
assert key not in vals
296
298
297
- def test_retry_saving (self , store , small_object , large_object ):
299
+ @mock .patch ("time.sleep" , return_value = None )
300
+ def test_retry_saving (self , mock_sleep , store , small_object , large_object ):
298
301
with mock .patch (
299
302
"moto.dynamodb2.responses.DynamoHandler.transact_write_items" ,
300
303
side_effect = KeyError ("foo" ),
@@ -307,45 +310,56 @@ def test_retry_saving(self, store, small_object, large_object):
307
310
except Exception :
308
311
assert_equal (mock_failed_write .call_count , 3 )
309
312
310
- def test_retry_reading (self , store , small_object , large_object ):
313
+ @mock .patch ("time.sleep" )
314
+ @mock .patch ("random.uniform" )
315
+ def test_retry_reading (self , mock_random_uniform , mock_sleep , store , small_object , large_object ):
311
316
unprocessed_value = {
312
- "Responses" : {
313
- store .name : [
314
- {
315
- "index" : {"N" : "0" },
316
- "key" : {"S" : "job_state 0" },
317
- },
318
- ],
319
- },
317
+ "Responses" : {},
320
318
"UnprocessedKeys" : {
321
319
store .name : {
322
- "ConsistentRead" : True ,
323
320
"Keys" : [
324
321
{
325
- "index" : {"N" : "0" },
326
322
"key" : {"S" : "job_state 0" },
323
+ "index" : {"N" : "0" },
327
324
}
328
325
],
329
- },
326
+ "ConsistentRead" : True ,
327
+ }
330
328
},
331
- "ResponseMetadata" : {},
332
329
}
333
330
keys = [store .build_key ("job_state" , i ) for i in range (1 )]
334
331
value = small_object
335
- pairs = zip (keys , ( value for i in range ( len (keys )) ))
332
+ pairs = zip (keys , [ value ] * len (keys ))
336
333
store .save (pairs )
334
+ store ._consume_save_queue ()
335
+
336
+ # Mock random.uniform to return the upper limit of the range so that we are simulating max jitter
337
+ def side_effect_random_uniform (a , b ):
338
+ return b
339
+
340
+ mock_random_uniform .side_effect = side_effect_random_uniform
341
+
337
342
with mock .patch .object (
338
343
store .client ,
339
344
"batch_get_item" ,
340
345
return_value = unprocessed_value ,
341
346
) as mock_failed_read :
342
- try :
343
- with mock .patch ("tron.config.static_config.load_yaml_file" , autospec = True ), mock .patch (
344
- "tron.config.static_config.build_configuration_watcher" , autospec = True
345
- ):
346
- store .restore (keys )
347
- except Exception :
348
- assert_equal (mock_failed_read .call_count , 10 )
347
+ with pytest .raises (Exception ) as exec_info , mock .patch (
348
+ "tron.config.static_config.load_yaml_file" , autospec = True
349
+ ), mock .patch ("tron.config.static_config.build_configuration_watcher" , autospec = True ):
350
+ store .restore (keys )
351
+ assert_in ("failed to retrieve items with keys" , str (exec_info .value ))
352
+ assert_equal (mock_failed_read .call_count , MAX_UNPROCESSED_KEYS_RETRIES )
353
+
354
+ # We also need to verify that sleep was called with expected delays
355
+ expected_delays = []
356
+ base_delay_seconds = 0.5
357
+ max_delay_seconds = 10
358
+ for attempt in range (1 , MAX_UNPROCESSED_KEYS_RETRIES + 1 ):
359
+ expected_delay = min (base_delay_seconds * (2 ** (attempt - 1 )), max_delay_seconds )
360
+ expected_delays .append (expected_delay )
361
+ actual_delays = [call .args [0 ] for call in mock_sleep .call_args_list ]
362
+ assert_equal (actual_delays , expected_delays )
349
363
350
364
def test_restore_exception_propagation (self , store , small_object ):
351
365
# This test is to ensure that restore propagates exceptions upwards: see DAR-2328
0 commit comments