44import datetime
55import itertools
66import logging
7- from typing import Dict , List , Optional , Union
7+ import time
8+ from typing import Any , Dict , List , Optional , Union
9+ from urllib .parse import unquote_plus as _unquote_plus
810
911import boto3 # type: ignore
1012
@@ -26,17 +28,24 @@ def _split_paths_by_bucket(paths: List[str]) -> Dict[str, List[str]]:
2628 return buckets
2729
2830
29- def _delete_objects (bucket : str , keys : List [str ], client_s3 : boto3 .client ) -> None :
31+ def _delete_objects (bucket : str , keys : List [str ], client_s3 : boto3 .client , attempt : int = 1 ) -> None :
3032 _logger .debug ("len(keys): %s" , len (keys ))
3133 batch : List [Dict [str , str ]] = [{"Key" : key } for key in keys ]
3234 res = client_s3 .delete_objects (Bucket = bucket , Delete = {"Objects" : batch })
33- deleted = res .get ("Deleted" )
34- if deleted is not None :
35- for i in deleted :
36- _logger .debug ("s3://%s/%s has been deleted." , bucket , i .get ("Key" ))
37- errors = res .get ("Errors" )
38- if errors is not None : # pragma: no cover
39- raise exceptions .ServiceApiError (errors )
35+ deleted : List [Dict [str , Any ]] = res .get ("Deleted" , [])
36+ for obj in deleted :
37+ _logger .debug ("s3://%s/%s has been deleted." , bucket , obj .get ("Key" ))
38+ errors : List [Dict [str , Any ]] = res .get ("Errors" , [])
39+ internal_errors : List [str ] = []
40+ for error in errors :
41+ if error ["Code" ] != "InternalError" :
42+ raise exceptions .ServiceApiError (errors )
43+ internal_errors .append (_unquote_plus (error ["Key" ]))
44+ if len (internal_errors ) > 0 :
45+ if attempt > 5 : # Maximum of 5 attempts
46+ raise exceptions .ServiceApiError (errors )
47+ time .sleep (attempt ) # Incremental delay (linear)
48+ _delete_objects (bucket = bucket , keys = internal_errors , client_s3 = client_s3 , attempt = (attempt + 1 ))
4049
4150
4251def delete_objects (
0 commit comments