Skip to content

Commit 0d7e1c6

Browse files
authored
Merge pull request #694 from rusq/review-679
[codex] fix skip complete threads resume
2 parents c72eb79 + e47d52b commit 0d7e1c6

5 files changed

Lines changed: 97 additions & 12 deletions

File tree

cmd/slackdump/internal/resume/resume.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,6 @@ type ResumeParams struct {
7474
// SkipCompleteThreads skips threads where the database already holds all
7575
// replies (DB count == API reply_count + 1). Faster, but won't detect
7676
// edited or deleted messages. Use only when threads are append-only.
77-
// Note: threads with parent messages older than the lookback window will
78-
// not be checked for updates, as only channel messages within the lookback
79-
// window are scanned to discover threads.
8077
SkipCompleteThreads bool
8178
// Dedupe runs duplicate entity cleanup after a successful resume.
8279
Dedupe bool
@@ -92,7 +89,7 @@ func init() {
9289
CmdResume.Flag.BoolVar(&resumeFlags.IncludeThreads, "threads", false, "include threads (slow, and flaky business)")
9390
CmdResume.Flag.BoolVar(&resumeFlags.RecordOnlyNewUsers, "only-new-users", true, "record only new or updated users")
9491
CmdResume.Flag.Var(resumeFlags.Lookback, "lookback", "lookback window `duration`")
95-
CmdResume.Flag.BoolVar(&resumeFlags.SkipCompleteThreads, "skip-complete-threads", false, "skip threads where DB already has all replies (faster, but won't detect edits/deletes or new replies to messages older than lookback window)")
92+
CmdResume.Flag.BoolVar(&resumeFlags.SkipCompleteThreads, "skip-complete-threads", false, "skip threads where DB already has all replies (faster, but won't detect edits/deletes)")
9693
CmdResume.Flag.BoolVar(&resumeFlags.Dedupe, "dedupe", false, "run dedupe cleanup after successful resume finish")
9794
}
9895

@@ -136,9 +133,6 @@ func runResume(ctx context.Context, cmd *base.Command, args []string) error {
136133
return fmt.Errorf("source type %q does not support resume, use 'slackdump convert -f database' to convert it", src.Type())
137134
}
138135

139-
if resumeFlags.IncludeThreads && resumeFlags.SkipCompleteThreads {
140-
slog.WarnContext(ctx, "threads whose parent message is older than the lookback window will not be checked for new replies")
141-
}
142136
latest, err := latest(ctx, src, resumeFlags.IncludeThreads, resumeFlags.SkipCompleteThreads, time.Duration((*duration.Duration)(resumeFlags.Lookback).ToTimeDuration()), list)
143137
if err != nil {
144138
base.SetExitStatus(base.SApplicationError)
@@ -267,7 +261,7 @@ func latest(ctx context.Context, src source.Resumer, includeThreads bool, skipCo
267261

268262
ei := make([]structures.EntityItem, 0, len(latest))
269263
for sl, ts := range latest {
270-
if sl.IsThread() && (!includeThreads || skipCompleteThreads) {
264+
if sl.IsThread() && !includeThreads {
271265
continue
272266
}
273267
item := structures.EntityItem{

cmd/slackdump/internal/resume/resume_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import (
3030
"go.uber.org/mock/gomock"
3131

3232
"github.com/rusq/slackdump/v4"
33-
dedupecmd "github.com/rusq/slackdump/v4/cmd/slackdump/internal/diag/dedupe"
3433
"github.com/rusq/slackdump/v4/cmd/slackdump/internal/cfg"
34+
dedupecmd "github.com/rusq/slackdump/v4/cmd/slackdump/internal/diag/dedupe"
3535
"github.com/rusq/slackdump/v4/internal/fixtures"
3636
"github.com/rusq/slackdump/v4/internal/structures"
3737
"github.com/rusq/slackdump/v4/source"
@@ -476,6 +476,7 @@ func Test_latest(t *testing.T) {
476476
},
477477
want: structures.NewEntityListFromItems(
478478
structures.EntityItem{Id: "C123", Oldest: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), Latest: time.Time(cfg.Latest), Include: true},
479+
structures.EntityItem{Id: "C456:123.456", Oldest: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC), Latest: time.Time(cfg.Latest), Include: true},
479480
),
480481
wantErr: false,
481482
},

stream/conversation.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ func (cs *Stream) thread(ctx context.Context, req request, callback func(mm []sl
244244
lg.DebugContext(ctx, "- getting thread")
245245

246246
var cursor string
247+
firstPage := true
247248
for {
248249
var (
249250
msgs []slack.Message
@@ -281,6 +282,15 @@ func (cs *Stream) thread(ctx context.Context, req request, callback func(mm []sl
281282
return err
282283
}
283284

285+
if firstPage && req.threadOnly && cs.skipThread != nil {
286+
replyCount := msgs[0].ReplyCount
287+
if cs.skipThread(ctx, req.sl.Channel, req.sl.ThreadTS, replyCount) {
288+
lg.DebugContext(ctx, "skipping complete thread", "channel_id", req.sl.Channel, "thread_ts", req.sl.ThreadTS, "reply_count", replyCount)
289+
return nil
290+
}
291+
}
292+
firstPage = false
293+
284294
r := trace.StartRegion(ctx, "thread_callback")
285295
err := callback(msgs, !hasmore)
286296
r.End()

stream/stream.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,10 @@ func OptFailOnNonCritError(b bool) Option {
180180
}
181181

182182
// OptSkipThreadFunc sets a predicate that is called for each thread head message
183-
// during channel processing. If it returns true, the thread is skipped (no API
184-
// call is made). Returns false on any error (safe default: re-fetch the thread).
185-
// Intended for resume operations to skip threads already complete in the database.
183+
// during channel processing and after the first replies page for direct thread
184+
// requests. If it returns true, the thread is skipped. Returns false on any
185+
// error (safe default: re-fetch the thread). Intended for resume operations to
186+
// skip threads already complete in the database.
186187
func OptSkipThreadFunc(fn func(ctx context.Context, channelID, threadTS string, replyCount int) bool) Option {
187188
return func(cs *Stream) {
188189
cs.skipThread = fn

stream/stream_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,85 @@ func TestStream_thread(t *testing.T) {
203203
assert.Equal(t, 1, calls)
204204
})
205205
}
206+
207+
t.Run("skips complete direct thread after first page", func(t *testing.T) {
208+
ctrl := gomock.NewController(t)
209+
ms := mock_client.NewMockSlack(ctrl)
210+
cs := New(ms, network.NoLimits, OptSkipThreadFunc(func(ctx context.Context, channelID, threadTS string, replyCount int) bool {
211+
assert.Equal(t, "CTM1", channelID)
212+
assert.Equal(t, "1610000000.000000", threadTS)
213+
assert.Equal(t, 2, replyCount)
214+
return true
215+
}))
216+
msgs := []slack.Message{
217+
{Msg: slack.Msg{
218+
Channel: "CTM1",
219+
Timestamp: "1610000000.000000",
220+
ThreadTimestamp: "1610000000.000000",
221+
ReplyCount: 2,
222+
}},
223+
}
224+
ms.EXPECT().
225+
GetConversationRepliesContext(gomock.Any(), gomock.Any()).
226+
Return(msgs, true, "next-cursor", nil).
227+
Times(1)
228+
229+
var calls int
230+
err := cs.thread(t.Context(), request{
231+
sl: &structures.SlackLink{Channel: "CTM1", ThreadTS: "1610000000.000000"},
232+
threadOnly: true,
233+
}, func(mm []slack.Message, isLast bool) error {
234+
calls++
235+
return nil
236+
})
237+
if err != nil {
238+
t.Fatalf("thread() error = %v", err)
239+
}
240+
assert.Equal(t, 0, calls)
241+
})
242+
243+
t.Run("processes incomplete direct thread after first page", func(t *testing.T) {
244+
ctrl := gomock.NewController(t)
245+
ms := mock_client.NewMockSlack(ctrl)
246+
cs := New(ms, network.NoLimits, OptSkipThreadFunc(func(ctx context.Context, channelID, threadTS string, replyCount int) bool {
247+
assert.Equal(t, "CTM1", channelID)
248+
assert.Equal(t, "1610000000.000000", threadTS)
249+
assert.Equal(t, 3, replyCount)
250+
return false
251+
}))
252+
msgs := []slack.Message{
253+
{Msg: slack.Msg{
254+
Channel: "CTM1",
255+
Timestamp: "1610000000.000000",
256+
ThreadTimestamp: "1610000000.000000",
257+
ReplyCount: 3,
258+
}},
259+
{Msg: slack.Msg{
260+
Channel: "CTM1",
261+
Timestamp: "1610000001.000000",
262+
ThreadTimestamp: "1610000000.000000",
263+
}},
264+
}
265+
ms.EXPECT().
266+
GetConversationRepliesContext(gomock.Any(), gomock.Any()).
267+
Return(msgs, false, "", nil).
268+
Times(1)
269+
270+
var calls int
271+
err := cs.thread(t.Context(), request{
272+
sl: &structures.SlackLink{Channel: "CTM1", ThreadTS: "1610000000.000000"},
273+
threadOnly: true,
274+
}, func(mm []slack.Message, isLast bool) error {
275+
calls++
276+
assert.True(t, isLast)
277+
assert.Equal(t, msgs, mm)
278+
return nil
279+
})
280+
if err != nil {
281+
t.Fatalf("thread() error = %v", err)
282+
}
283+
assert.Equal(t, 1, calls)
284+
})
206285
}
207286

208287
func Test_processLink(t *testing.T) {

0 commit comments

Comments
 (0)