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
5362var _ Client = (& client {})
5463
5564func (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+
6480func (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)
92108func (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
156177func (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
228251func (c * client ) deleteScheduledMessage (ctx context.Context , msg slack.ScheduledMessage ) error {
0 commit comments