18
18
from __future__ import annotations
19
19
20
20
import json
21
+ import time
21
22
import warnings
22
23
from collections .abc import Sequence
23
24
from functools import cached_property
28
29
from slack_sdk .errors import SlackApiError
29
30
from typing_extensions import NotRequired
30
31
31
- from airflow .exceptions import AirflowNotFoundException
32
+ from airflow .exceptions import AirflowException , AirflowNotFoundException
32
33
from airflow .hooks .base import BaseHook
33
34
from airflow .providers .slack .utils import ConnectionExtraConfig
34
35
from airflow .utils .helpers import exactly_one
@@ -291,7 +292,7 @@ def get_channel_id(self, channel_name: str) -> str:
291
292
"""
292
293
next_cursor = None
293
294
while not (channel_id := self ._channels_mapping .get (channel_name )):
294
- res = self .client . conversations_list (cursor = next_cursor , types = "public_channel,private_channel" )
295
+ res = self ._call_conversations_list (cursor = next_cursor )
295
296
if TYPE_CHECKING :
296
297
# Slack SDK response type too broad, this should make mypy happy
297
298
assert isinstance (res .data , dict )
@@ -308,6 +309,37 @@ def get_channel_id(self, channel_name: str) -> str:
308
309
raise LookupError (msg )
309
310
return channel_id
310
311
312
+ def _call_conversations_list (self , cursor : str | None = None ):
313
+ """
314
+ Call ``conversations.list`` with automatic 429-retry.
315
+
316
+ .. versionchanged:: 3.0.0
317
+ Automatically retries on 429 responses (up to 5 times, honouring *Retry-After* header).
318
+
319
+ :param cursor: Pagination cursor returned by the previous ``conversations.list`` call.
320
+ Pass ``None`` (default) to start from the first page.
321
+ :raises AirflowException: If the method hits the rate-limit 5 times in a row.
322
+ :raises SlackApiError: Propagated when errors other than 429 occur.
323
+ :return: Slack SDK response for the page requested.
324
+ """
325
+ max_retries = 5
326
+ for attempt in range (max_retries ):
327
+ try :
328
+ return self .client .conversations_list (cursor = cursor , types = "public_channel,private_channel" )
329
+ except SlackApiError as e :
330
+ if e .response .status_code == 429 and attempt < max_retries :
331
+ retry_after = int (e .response .headers .get ("Retry-After" , 30 ))
332
+ self .log .warning (
333
+ "Rate limit hit. Retrying in %s seconds. Attempt %s/%s" ,
334
+ retry_after ,
335
+ attempt + 1 ,
336
+ max_retries ,
337
+ )
338
+ time .sleep (retry_after )
339
+ else :
340
+ raise
341
+ raise AirflowException ("Max retries reached for conversations.list" )
342
+
311
343
def test_connection (self ):
312
344
"""
313
345
Tests the Slack API connection.
0 commit comments