diff --git a/internal/slack/slack.go b/internal/slack/slack.go index 86fbbb6..55aaf49 100644 --- a/internal/slack/slack.go +++ b/internal/slack/slack.go @@ -3,6 +3,7 @@ package slack import ( "errors" "fmt" + "sync" "time" "github.com/elliotchance/pie/v2" @@ -18,6 +19,8 @@ type service struct { client iclient maxAttempts int initialBackoff time.Duration + channelCache map[string]*slack.Channel + cacheMutex sync.RWMutex } type conversationsResult struct { @@ -36,6 +39,7 @@ func New(token string, debug bool) (IService, error) { client: &client{client: slackClient}, maxAttempts: 5, initialBackoff: 2 * time.Second, + channelCache: make(map[string]*slack.Channel), } return &s, nil @@ -68,6 +72,12 @@ func (s *service) findSlackChannel(channelName string) (channel *slack.Channel, var channels []slack.Channel var channelTypes = []string{"private_channel", "public_channel"} + cachedChannel := s.getCachedChannel(channelName) + if cachedChannel != nil { + log.Debug().Str("channel", channelName).Msg("Found slack channel in cache") + return cachedChannel, nil + } + for { result, opErr := runWithRetries(func() (conversationsResult, error) { convChannels, convCursor, convErr := s.client.GetConversations(&slack.GetConversationsParameters{ @@ -92,6 +102,7 @@ func (s *service) findSlackChannel(channelName string) (channel *slack.Channel, if idx > -1 { log.Info().Str("channel", channelName).Msg("Found slack channel") channel = &channels[idx] + s.saveChannelToCache(channelName, channel) return } else if nextCursor == "" { return nil, fmt.Errorf("channel %v not found", channelName) @@ -101,6 +112,24 @@ func (s *service) findSlackChannel(channelName string) (channel *slack.Channel, } } +// getCachedChannel retrieves a channel from the cache if it exists +func (s *service) getCachedChannel(channelName string) (channel *slack.Channel) { + s.cacheMutex.RLock() + defer s.cacheMutex.RUnlock() + ch := s.channelCache[channelName] + return ch +} + +// saveChannelToCache saves a channel to the cache +func (s *service) saveChannelToCache(channelName string, channel *slack.Channel) { + s.cacheMutex.Lock() + defer s.cacheMutex.Unlock() + if s.channelCache == nil { + s.channelCache = make(map[string]*slack.Channel) + } + s.channelCache[channelName] = channel +} + func runWithRetries[T any](operation func() (T, error), maxAttempts int, backoff time.Duration) (result T, err error) { if maxAttempts <= 0 { maxAttempts = 1 diff --git a/internal/slack/slack_test.go b/internal/slack/slack_test.go index 2bc96c7..58563ff 100644 --- a/internal/slack/slack_test.go +++ b/internal/slack/slack_test.go @@ -185,3 +185,46 @@ func TestPostMessageWithDynamicRateLimitRetry(t *testing.T) { elapsed := time.Since(start) assert.GreaterOrEqual(t, elapsed, expectedWait, "should have used Slack's dynamic RetryAfter backoff") } + +func TestChannelIsCached(t *testing.T) { + channelID := "1234" + channelName := "random channel" + + mockClient := mockClient{} + mockClient.On("GetConversations", &slack.GetConversationsParameters{ + ExcludeArchived: true, + Cursor: "", + Types: []string{"private_channel", "public_channel"}, + Limit: 1000, + }).Return( + []slack.Channel{ + { + GroupConversation: slack.GroupConversation{ + Conversation: slack.Conversation{ID: channelID}, + Name: channelName, + }, + }, + }, + "", + nil, + ).Once() // Expect only one call to GetConversations + + svc := service{ + client: &mockClient, + maxAttempts: 3, + initialBackoff: 2 * time.Second, + } + + channel, err := svc.findSlackChannel(channelName) + assert.Nil(t, err) + assert.NotNil(t, channel) + assert.Equal(t, channelID, channel.ID) + + // Call again to verify it uses the cache + cachedChannel, err := svc.findSlackChannel(channelName) + assert.Nil(t, err) + assert.NotNil(t, cachedChannel) + assert.Equal(t, channelID, cachedChannel.ID) + + mockClient.AssertExpectations(t) +}