@@ -3,6 +3,7 @@ package slack
33import (
44 "errors"
55 "fmt"
6+ "time"
67
78 "github.com/elliotchance/pie/v2"
89 "github.com/rs/zerolog/log"
@@ -14,7 +15,14 @@ type IService interface {
1415}
1516
1617type service struct {
17- client iclient
18+ client iclient
19+ maxAttempts int
20+ initialBackoff time.Duration
21+ }
22+
23+ type conversationsResult struct {
24+ Channels []slack.Channel
25+ NextCursor string
1826}
1927
2028// New creates a new Slack service
@@ -24,19 +32,26 @@ func New(token string, debug bool) (IService, error) {
2432 return nil , errors .New ("failed to create slack client" )
2533 }
2634
27- s := service {& client {client : slackClient }}
35+ s := service {
36+ client : & client {client : slackClient },
37+ maxAttempts : 5 ,
38+ initialBackoff : 2 * time .Second ,
39+ }
2840
2941 return & s , nil
3042}
3143
3244// PostMessage posts a message to the given slack channel
3345func (s * service ) PostMessage (channelName string , options ... slack.MsgOption ) (ts string , err error ) {
34- channel , err := s .findSlackChannel (channelName )
46+ channel , err := runWithRetries ( func () ( * slack. Channel , error ) { return s .findSlackChannel (channelName ) }, s . maxAttempts , s . initialBackoff )
3547 if err != nil {
3648 return
3749 }
3850
39- _ , ts , err = s .client .PostMessage (channel .ID , options ... )
51+ ts , err = runWithRetries (func () (string , error ) {
52+ _ , msgTs , err := s .client .PostMessage (channel .ID , options ... )
53+ return msgTs , err
54+ }, s .maxAttempts , s .initialBackoff )
4055 if err != nil {
4156 return ts , errors .Join (errors .New ("failed to post slack message" ), err )
4257 }
@@ -54,15 +69,25 @@ func (s *service) findSlackChannel(channelName string) (channel *slack.Channel,
5469 var channelTypes = []string {"private_channel" , "public_channel" }
5570
5671 for {
57- if channels , nextCursor , err = s .client .GetConversations (& slack.GetConversationsParameters {
58- ExcludeArchived : true ,
59- Cursor : nextCursor ,
60- Types : channelTypes ,
61- Limit : 1000 ,
62- }); err != nil {
63- return nil , errors .Join (errors .New ("failed to get slack channel list" ), err )
72+ result , opErr := runWithRetries (func () (conversationsResult , error ) {
73+ convChannels , convCursor , convErr := s .client .GetConversations (& slack.GetConversationsParameters {
74+ ExcludeArchived : true ,
75+ Cursor : nextCursor ,
76+ Types : channelTypes ,
77+ Limit : 1000 ,
78+ })
79+ if convErr != nil {
80+ return conversationsResult {}, convErr
81+ }
82+ return conversationsResult {Channels : convChannels , NextCursor : convCursor }, nil
83+ }, s .maxAttempts , s .initialBackoff )
84+ if opErr != nil {
85+ return nil , errors .Join (errors .New ("failed to get slack channel list" ), opErr )
6486 }
6587
88+ channels = result .Channels
89+ nextCursor = result .NextCursor
90+
6691 idx := pie .FindFirstUsing (channels , func (c slack.Channel ) bool { return c .Name == channelName })
6792 if idx > - 1 {
6893 log .Info ().Str ("channel" , channelName ).Msg ("Found slack channel" )
@@ -75,3 +100,46 @@ func (s *service) findSlackChannel(channelName string) (channel *slack.Channel,
75100 log .Debug ().Str ("channel" , channelName ).Str ("nextPage" , nextCursor ).Msg ("Channel not found in current page, fetching next page" )
76101 }
77102}
103+
104+ func runWithRetries [T any ](operation func () (T , error ), maxAttempts int , backoff time.Duration ) (result T , err error ) {
105+ if maxAttempts <= 0 {
106+ maxAttempts = 1
107+ }
108+
109+ for attempt := 1 ; attempt <= maxAttempts ; attempt ++ {
110+ result , err = operation ()
111+ if err == nil {
112+ return result , nil
113+ }
114+
115+ if attempt == maxAttempts {
116+ break
117+ }
118+
119+ var sleepDuration time.Duration
120+ var rateLimitErr * slack.RateLimitedError
121+
122+ if errors .As (err , & rateLimitErr ) {
123+ // Override the standard backoff with Slack's requested wait time
124+ if rateLimitErr .RetryAfter > 0 {
125+ sleepDuration = rateLimitErr .RetryAfter
126+ } else {
127+ // Use exponential backoff: backoff * 2^(attempt-1)
128+ sleepDuration = backoff * time .Duration (1 << (attempt - 1 ))
129+ }
130+
131+ log .Warn ().
132+ Err (err ).
133+ Int ("attempt" , attempt ).
134+ Dur ("retry_after" , sleepDuration ).
135+ Msg ("Hit Slack rate limit, backing off dynamically" )
136+ } else {
137+ sleepDuration = backoff * time .Duration (1 << (attempt - 1 ))
138+ log .Warn ().Err (err ).Int ("attempt" , attempt ).Dur ("backoff" , sleepDuration ).Msg ("Operation failed, retrying with exponential backoff" )
139+ }
140+
141+ time .Sleep (sleepDuration )
142+ }
143+
144+ return result , fmt .Errorf ("operation failed after %d attempts: %w" , maxAttempts , err )
145+ }
0 commit comments