1010 Callable ,
1111 Dict ,
1212 Generator ,
13+ Iterable ,
1314 List ,
1415 Literal ,
1516 Optional ,
17+ Tuple ,
1618 TypeVar ,
1719)
1820
2729from sqs_workers .exceptions import SQSError
2830from sqs_workers .processors import DEFAULT_CONTEXT_VAR , Processor
2931from sqs_workers .shutdown_policies import NEVER_SHUTDOWN
32+ from sqs_workers .utils import batcher
3033
3134DEFAULT_MESSAGE_GROUP_ID = "default"
3235SEND_BATCH_SIZE = 10
@@ -85,53 +88,84 @@ def process_queue(self, shutdown_policy=NEVER_SHUTDOWN, wait_second=10):
8588 )
8689 break
8790
88- def process_batch (self , wait_seconds = 0 ) -> BatchProcessingResult :
91+ def process_batch (self , wait_seconds : int = 0 ) -> BatchProcessingResult :
8992 """
9093 Process a batch of messages from the queue (10 messages at most), return
9194 the number of successfully processed messages, and exit
9295 """
93- queue = self .get_queue ()
94-
9596 if self .batching_policy .batching_enabled :
96- return self ._process_messages_in_batch (queue , wait_seconds )
97+ messages = self .get_raw_messages (
98+ wait_seconds , self .batching_policy .batch_size
99+ )
100+ success = self .process_messages (messages )
101+ messages_with_success = ((m , success ) for m in messages )
102+ else :
103+ messages = self .get_raw_messages (wait_seconds )
104+ success = [self .process_message (message ) for message in messages ]
105+ messages_with_success = zip (messages , success )
97106
98- return self ._process_messages_individually ( queue , wait_seconds )
107+ return self ._handle_processed ( messages_with_success )
99108
100- def _process_messages_in_batch (self , queue , wait_seconds ):
101- messages = self . get_raw_messages ( wait_seconds , self . batching_policy . batch_size )
102- result = BatchProcessingResult ( self . name )
109+ def _handle_processed (self , messages_with_success : Iterable [ Tuple [ Any , bool ]] ):
110+ """
111+ Handles the results of processing messages.
103112
104- success = self .process_messages (messages )
113+ For successful messages, we delete the message ID from the queue, which is
114+ equivalent to acknowledging it.
105115
106- for message in messages :
107- result .update_with_message (message , success )
108- if success :
109- entry = {
110- "Id" : message .message_id ,
111- "ReceiptHandle" : message .receipt_handle ,
112- }
113- queue .delete_messages (Entries = [entry ])
114- else :
115- timeout = self .backoff_policy .get_visibility_timeout (message )
116- message .change_visibility (VisibilityTimeout = timeout )
117- return result
116+ For failed messages, we change the visibility of the message, in order to
117+ keep it un-consumeable for a little while (a form of backoff).
118+
119+ In each case (delete or change-viz), we batch the API calls to AWS in order
120+ to try to avoid getting throttled, with batches of size 10 (the limit). The
121+ config (see sqs_env.py) should also retry in the event of exceptions.
122+ """
123+ queue = self .get_queue ()
118124
119- def _process_messages_individually (self , queue , wait_seconds ):
120- messages = self .get_raw_messages (wait_seconds )
121125 result = BatchProcessingResult (self .name )
122126
123- for message in messages :
124- success = self .process_message (message )
125- result .update_with_message (message , success )
126- if success :
127- entry = {
128- "Id" : message .message_id ,
129- "ReceiptHandle" : message .receipt_handle ,
130- }
131- queue .delete_messages (Entries = [entry ])
132- else :
133- timeout = self .backoff_policy .get_visibility_timeout (message )
134- message .change_visibility (VisibilityTimeout = timeout )
127+ for subgroup in batcher (messages_with_success , batch_size = 10 ):
128+ entries_to_ack = []
129+ entries_to_change_viz = []
130+
131+ for m , success in subgroup :
132+ result .update_with_message (m , success )
133+ if success :
134+ entries_to_ack .append (
135+ {
136+ "Id" : m .message_id ,
137+ "ReceiptHandle" : m .receipt_handle ,
138+ }
139+ )
140+ else :
141+ entries_to_change_viz .append (
142+ {
143+ "Id" : m .message_id ,
144+ "ReceiptHandle" : m .receipt_handle ,
145+ "VisibilityTimeout" : self .backoff_policy .get_visibility_timeout (
146+ m
147+ ),
148+ }
149+ )
150+
151+ ack_response = queue .delete_messages (Entries = entries_to_ack )
152+
153+ if ack_response .get ("Failed" ):
154+ logger .warning (
155+ "Failed to delete processed messages from queue" ,
156+ extra = {"queue" : self .name , "failures" : ack_response ["Failed" ]},
157+ )
158+
159+ viz_response = queue .change_message_visibility_batch (
160+ Entries = entries_to_change_viz ,
161+ )
162+
163+ if viz_response .get ("Failed" ):
164+ logger .warning (
165+ "Failed to change visibility of messages which failed to process" ,
166+ extra = {"queue" : self .name , "failures" : viz_response ["Failed" ]},
167+ )
168+
135169 return result
136170
137171 def process_message (self , message : Any ) -> bool :
@@ -151,15 +185,16 @@ def process_messages(self, messages: List[Any]) -> bool:
151185 """
152186 raise NotImplementedError ()
153187
154- def get_raw_messages (self , wait_seconds , max_messages = 10 ):
188+ def get_raw_messages (self , wait_seconds : int , max_messages : int = 10 ) -> List [ Any ] :
155189 """Return raw messages from the queue, addressed by its name"""
190+ queue = self .get_queue ()
191+
156192 kwargs = {
157193 "WaitTimeSeconds" : wait_seconds ,
158194 "MaxNumberOfMessages" : max_messages if max_messages <= 10 else 10 ,
159195 "MessageAttributeNames" : ["All" ],
160196 "AttributeNames" : ["All" ],
161197 }
162- queue = self .get_queue ()
163198
164199 if max_messages <= 10 :
165200 return queue .receive_messages (** kwargs )
@@ -180,16 +215,29 @@ def get_raw_messages(self, wait_seconds, max_messages=10):
180215 def drain_queue (self , wait_seconds = 0 ):
181216 """Delete all messages from the queue without calling purge()."""
182217 queue = self .get_queue ()
218+
183219 deleted_count = 0
184220 while True :
185221 messages = self .get_raw_messages (wait_seconds )
186222 if not messages :
187223 break
224+
188225 entries = [
189226 {"Id" : msg .message_id , "ReceiptHandle" : msg .receipt_handle }
190227 for msg in messages
191228 ]
192- queue .delete_messages (Entries = entries )
229+
230+ ack_response = queue .delete_messages (Entries = entries )
231+
232+ if ack_response .get ("Failed" ):
233+ logger .warning (
234+ "Failed to delete processed messages from queue" ,
235+ extra = {
236+ "queue" : self .name ,
237+ "failures" : ack_response ["Failed" ],
238+ },
239+ )
240+
193241 deleted_count += len (messages )
194242 return deleted_count
195243
0 commit comments