Skip to content

Commit 41725a5

Browse files
committed
prodvider/slack: only update a check-in's messages every 5m, serialize req handling
1 parent 436e1d3 commit 41725a5

File tree

1 file changed

+72
-49
lines changed

1 file changed

+72
-49
lines changed

internal/provider/slack/client.go

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"context"
66
"errors"
77
"fmt"
8-
"sort"
98
"strings"
9+
"sync"
1010
"time"
1111

1212
"github.com/adamdecaf/deadcheck/internal/config"
@@ -31,6 +31,7 @@ func NewClient(logger log.Logger, conf *config.Slack, timeService stime.TimeServ
3131
logger: logger,
3232
conf: *conf,
3333
timeService: timeService,
34+
lastMod: make(map[string]latestModification),
3435
}
3536

3637
underlying := slack.New(conf.ApiToken)
@@ -46,13 +47,24 @@ type client struct {
4647
logger log.Logger
4748
conf config.Slack
4849
timeService stime.TimeService
50+
underlying *slack.Client
51+
mu sync.Mutex
4952

50-
underlying *slack.Client
53+
lastMod map[string]latestModification
54+
lastModMu sync.RWMutex
55+
}
56+
57+
type latestModification struct {
58+
modifiedAt time.Time
59+
nextCheckIn time.Time
5160
}
5261

5362
var _ Client = (&client{})
5463

5564
func (c *client) Setup(ctx context.Context, check config.Check) error {
65+
c.mu.Lock()
66+
defer c.mu.Unlock()
67+
5668
err := c.setupScheduledMessage(ctx, check)
5769
if err != nil {
5870
return fmt.Errorf("setup scheduled message: %w", err)
@@ -61,6 +73,10 @@ func (c *client) Setup(ctx context.Context, check config.Check) error {
6173
return nil
6274
}
6375

76+
const (
77+
quiescencePeriod = 5 * time.Minute
78+
)
79+
6480
func (c *client) setupScheduledMessage(ctx context.Context, check config.Check) error {
6581
logger := c.logger.With(log.Fields{
6682
"channel_id": log.String(c.conf.ChannelID),
@@ -92,7 +108,7 @@ func (c *client) setupScheduledMessage(ctx context.Context, check config.Check)
92108
func (c *client) findScheduledMessages(ctx context.Context, logger log.Logger, check config.Check) ([]slack.ScheduledMessage, error) {
93109
params := &slack.GetScheduledMessagesParameters{
94110
Channel: c.conf.ChannelID,
95-
Limit: 20,
111+
Limit: 100,
96112
}
97113
messages, _, err := c.underlying.GetScheduledMessagesContext(ctx, params)
98114
if err != nil {
@@ -109,13 +125,18 @@ func (c *client) findScheduledMessages(ctx context.Context, logger log.Logger, c
109125
"text": log.String(msg.Text),
110126
})
111127

112-
if strings.Contains(msg.Text, check.ID) && strings.Contains(msg.Text, "check-in") {
128+
if strings.Contains(msg.Text, fmt.Sprintf("%s did not check-in", check.ID)) {
113129
logger.Log("found matching scheduled message")
114130
out = append(out, msg)
115131
} else {
116132
logger.Log("found unrelated scheduled message")
117133
}
118134
}
135+
136+
if len(out) > 1 {
137+
logger.Error().Logf("found %d duplicate messages for check %s", len(out), check.ID)
138+
}
139+
119140
return out, nil
120141
}
121142

@@ -154,13 +175,45 @@ func (c *client) createSnoozedMessage(ctx context.Context, logger log.Logger, ch
154175
}
155176

156177
func (c *client) CheckIn(ctx context.Context, check config.Check) (time.Time, error) {
178+
c.mu.Lock()
179+
defer c.mu.Unlock()
180+
181+
c.lastModMu.RLock()
182+
lastMod, exists := c.lastMod[check.ID]
183+
c.lastModMu.RUnlock()
184+
185+
now := c.timeService.Now()
186+
if exists && now.Sub(lastMod.modifiedAt) < quiescencePeriod {
187+
// Skip if we're within the quiescence period - another instance just handled a check-in for this check
188+
return lastMod.nextCheckIn, nil
189+
}
190+
157191
logger := c.logger.With(log.Fields{
158192
"channel_id": log.String(c.conf.ChannelID),
159193
"check": log.String(check.ID),
160194
})
161195

162-
// Calculate schedule timing
163-
now := c.timeService.Now()
196+
// Delete existing messages
197+
messages, err := c.findScheduledMessages(ctx, logger, check)
198+
if err != nil {
199+
return time.Time{}, fmt.Errorf("finding scheduled messages: %w", err)
200+
}
201+
202+
for _, msg := range messages {
203+
logger.Info().With(log.Fields{
204+
"message_id": log.String(msg.ID),
205+
"post_at": log.String(time.Unix(int64(msg.PostAt), 0).Format(time.RFC3339)),
206+
}).Log("deleting scheduled message")
207+
208+
err = c.deleteScheduledMessage(ctx, msg)
209+
if err != nil {
210+
if !strings.Contains(err.Error(), "invalid_scheduled_message_id") {
211+
logger.Error().LogErrorf("failed to delete scheduled message: %v", err)
212+
}
213+
}
214+
}
215+
216+
// Calculate next check-in time
164217
scheduleTime, _, err := snooze.Calculate(now, check.Schedule)
165218
if err != nil {
166219
return time.Time{}, fmt.Errorf("calculating snooze: %w", err)
@@ -172,57 +225,27 @@ func (c *client) CheckIn(ctx context.Context, check config.Check) (time.Time, er
172225
return time.Time{}, logger.Error().LogError(err).Err()
173226
}
174227

175-
// Find existing messages
176-
messages, err := c.findScheduledMessages(ctx, logger, check)
228+
// Calculate next window
229+
_, wait, err := snooze.Calculate(scheduleTime, check.Schedule)
177230
if err != nil {
178-
return time.Time{}, fmt.Errorf("finding scheduled message: %w", err)
231+
return time.Time{}, fmt.Errorf("calculating next window: %w", err)
179232
}
180233

181-
// Calculate next check-in window
182-
_, wait, err := snooze.Calculate(scheduleTime, check.Schedule)
234+
// Create new message
235+
nextCheckin, err := c.createSnoozedMessage(ctx, logger, check, now, wait)
183236
if err != nil {
184-
return time.Time{}, fmt.Errorf("calculating second snooze: %w", err)
237+
return time.Time{}, fmt.Errorf("creating new message: %w", err)
185238
}
186-
nextCheckIn := scheduleTime.Add(wait)
187239

188-
// If we found existing messages, handle rescheduling
189-
if len(messages) > 0 {
190-
// Sort by post time to get the latest
191-
sort.Slice(messages, func(i, j int) bool {
192-
return messages[i].PostAt > messages[j].PostAt
193-
})
194-
195-
currentMsg := messages[0]
196-
currentPostAt := time.Unix(int64(currentMsg.PostAt), 0)
197-
198-
// Only update if we're extending the time further out
199-
if nextCheckIn.After(currentPostAt) {
200-
// Delete existing messages (cleanup any duplicates too)
201-
for _, msg := range messages {
202-
err = c.deleteScheduledMessage(ctx, msg)
203-
if err != nil && !strings.Contains(err.Error(), "invalid_scheduled_message_id") {
204-
return time.Time{}, fmt.Errorf("deleting message: %w", err)
205-
}
206-
}
207-
208-
// Create new message with extended time
209-
_, err = c.createSnoozedMessage(ctx, logger, check, now, nextCheckIn.Sub(now))
210-
if err != nil {
211-
return time.Time{}, fmt.Errorf("creating new message: %w", err)
212-
}
213-
} else {
214-
logger.Info().Logf("keeping existing message scheduled for %v as it's further out", currentPostAt)
215-
nextCheckIn = currentPostAt
216-
}
217-
} else {
218-
// No existing message, create new one
219-
_, err = c.createSnoozedMessage(ctx, logger, check, now, nextCheckIn.Sub(now))
220-
if err != nil {
221-
return time.Time{}, fmt.Errorf("creating initial message: %w", err)
222-
}
240+
// Record the modification time
241+
c.lastModMu.Lock()
242+
c.lastMod[check.ID] = latestModification{
243+
modifiedAt: now,
244+
nextCheckIn: nextCheckin,
223245
}
246+
c.lastModMu.Unlock()
224247

225-
return nextCheckIn, nil
248+
return nextCheckin, nil
226249
}
227250

228251
func (c *client) deleteScheduledMessage(ctx context.Context, msg slack.ScheduledMessage) error {

0 commit comments

Comments
 (0)