From 3a29a59e55dc8aa8a9bf31395f0f942eea700919 Mon Sep 17 00:00:00 2001 From: kim Date: Sat, 17 May 2025 11:52:49 +0000 Subject: [bugfix] fix case of failed timeline preload causing lockups (#4182) - moves preloader Done() function calling to be handled entirely by the preloader, not the caller - adds tests for multiple preload success / failure / clear states Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4182 Co-authored-by: kim Co-committed-by: kim --- internal/cache/timeline/status_test.go | 88 +++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) (limited to 'internal/cache/timeline/status_test.go') diff --git a/internal/cache/timeline/status_test.go b/internal/cache/timeline/status_test.go index fc7e43da8..6d513032a 100644 --- a/internal/cache/timeline/status_test.go +++ b/internal/cache/timeline/status_test.go @@ -19,9 +19,12 @@ package timeline import ( "context" + "errors" "fmt" "slices" + "sync/atomic" "testing" + "time" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -65,6 +68,87 @@ var testStatusMeta = []*StatusMeta{ }, } +func TestStatusTimelinePreloader(t *testing.T) { + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + var tt StatusTimeline + tt.Init(1000) + + // Start goroutine to add some + // concurrent usage to preloader. + var started atomic.Int32 + go func() { + for { + select { + case <-ctx.Done(): + return + default: + } + tt.preloader.Check() + started.Add(1) + } + }() + + // Wait until goroutine running. + for started.Load() == 0 { + time.Sleep(time.Millisecond) + } + + // Variable to check whether + // our hook funcs are called. + var called bool + reset := func() { called = false } + + // "no error" preloader hook. + preloadNoErr := func() error { + called = true + return nil + } + + // "error return" preloader hook. + preloadErr := func() error { + called = true + return errors.New("oh no") + } + + // Check that on fail does not mark as preloaded. + err := tt.preloader.CheckPreload(preloadErr) + assert.Error(t, err) + assert.False(t, tt.preloader.Check()) + assert.True(t, called) + reset() + + // Check that on success marks itself as preloaded. + err = tt.preloader.CheckPreload(preloadNoErr) + assert.NoError(t, err) + assert.True(t, tt.preloader.Check()) + assert.True(t, called) + reset() + + // Check that preload func not called again + // if it's already in the 'preloaded' state. + err = tt.preloader.CheckPreload(preloadErr) + assert.NoError(t, err) + assert.True(t, tt.preloader.Check()) + assert.False(t, called) + reset() + + // Ensure that a clear operation + // successfully unsets preloader. + tt.preloader.Clear() + assert.False(t, tt.preloader.Check()) + assert.False(t, called) + reset() + + // Ensure that it can be marked as preloaded again. + err = tt.preloader.CheckPreload(preloadNoErr) + assert.NoError(t, err) + assert.True(t, tt.preloader.Check()) + assert.True(t, called) + reset() +} + func TestStatusTimelineLoadLimit(t *testing.T) { var tt StatusTimeline tt.Init(1000) @@ -80,7 +164,7 @@ func TestStatusTimelineLoadLimit(t *testing.T) { _ = tt.cache.Insert(data...) // Manually mark timeline as 'preloaded'. - tt.preloader.CheckPreload(tt.preloader.Done) + tt.preloader.CheckPreload(func() error { return nil }) // Craft a new page for selection, // setting placeholder min / max values @@ -251,7 +335,7 @@ func TestStatusTimelineInserts(t *testing.T) { assert.Equal(t, maxID, maxStatus(&tt).ID) // Manually mark timeline as 'preloaded'. - tt.preloader.CheckPreload(tt.preloader.Done) + tt.preloader.CheckPreload(func() error { return nil }) // Specifically craft a boost of latest (i.e. max) status in timeline. boost := >smodel.Status{ID: "06B1A00PQWDZZH9WK9P5VND35C", BoostOfID: maxID} -- cgit v1.2.3