// GoToSocial // Copyright (C) GoToSocial Authors admin@gotosocial.org // SPDX-License-Identifier: AGPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package timeline import ( "container/list" "context" "errors" "fmt" "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/log" ) func (t *timeline) indexXBetweenIDs(ctx context.Context, amount int, behindID string, beforeID string, frontToBack bool) error { l := log. WithContext(ctx). WithFields(kv.Fields{ {"amount", amount}, {"behindID", behindID}, {"beforeID", beforeID}, {"frontToBack", frontToBack}, }...) l.Trace("entering indexXBetweenIDs") if beforeID >= behindID { // This is an impossible situation, we // can't index anything between these. return nil } t.Lock() defer t.Unlock() // Lazily init indexed items. if t.items.data == nil { t.items.data = &list.List{} t.items.data.Init() } // Start by mapping out the list so we know what // we have to do. Depending on the current state // of the list we might not have to do *anything*. var ( position int listLen = t.items.data.Len() behindIDPosition int beforeIDPosition int ) for e := t.items.data.Front(); e != nil; e = e.Next() { entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert position++ if entry.itemID > behindID { l.Trace("item is too new, continuing") continue } if behindIDPosition == 0 { // Gone far enough through the list // and found our behindID mark. // We only need to set this once. l.Tracef("found behindID mark %s at position %d", entry.itemID, position) behindIDPosition = position } if entry.itemID >= beforeID { // Push the beforeID mark back // one place every iteration. l.Tracef("setting beforeID mark %s at position %d", entry.itemID, position) beforeIDPosition = position } if entry.itemID <= beforeID { // We've gone beyond the bounds of // items we're interested in; stop. l.Trace("reached older items, breaking") break } } // We can now figure out if we need to make db calls. var grabMore bool switch { case listLen < amount: // The whole list is shorter than the // amount we're being asked to return, // make up the difference. grabMore = true amount -= listLen case beforeIDPosition-behindIDPosition < amount: // Not enough items between behindID and // beforeID to return amount required, // try to get more. grabMore = true } if !grabMore { // We're good! return nil } // Fetch additional items. items, err := t.grab(ctx, amount, behindID, beforeID, frontToBack) if err != nil { return err } // Index all the items we got. We already have // a lock on the timeline, so don't call IndexOne // here, since that will also try to get a lock! for _, item := range items { entry := &indexedItemsEntry{ itemID: item.GetID(), boostOfID: item.GetBoostOfID(), accountID: item.GetAccountID(), boostOfAccountID: item.GetBoostOfAccountID(), } if _, err := t.items.insertIndexed(ctx, entry); err != nil { return fmt.Errorf("error inserting entry with itemID %s into index: %w", entry.itemID, err) } } return nil } // grab wraps the timeline's grabFunction in paging + filtering logic. func (t *timeline) grab(ctx context.Context, amount int, behindID string, beforeID string, frontToBack bool) ([]Timelineable, error) { var ( sinceID string minID string grabbed int maxID = behindID filtered = make([]Timelineable, 0, amount) ) if frontToBack { sinceID = beforeID } else { minID = beforeID } for attempts := 0; attempts < 5; attempts++ { if grabbed >= amount { // We got everything we needed. break } items, stop, err := t.grabFunction( ctx, t.timelineID, maxID, sinceID, minID, // Don't grab more than we need to. amount-grabbed, ) if err != nil { // Grab function already checks for // db.ErrNoEntries, so if an error // is returned then it's a real one. return nil, err } if stop || len(items) == 0 { // No items left. break } // Set next query parameters. if frontToBack { // Page down. maxID = items[len(items)-1].GetID() if maxID <= beforeID { // Can't go any further. break } } else { // Page up. minID = items[0].GetID() if minID >= behindID { // Can't go any further. break } } for _, item := range items { ok, err := t.filterFunction(ctx, t.timelineID, item) if err != nil { if !errors.Is(err, db.ErrNoEntries) { // Real error here. return nil, err } log.Warnf(ctx, "errNoEntries while filtering item %s: %s", item.GetID(), err) continue } if ok { filtered = append(filtered, item) grabbed++ // count this as grabbed } } } return filtered, nil } func (t *timeline) IndexAndPrepareOne(ctx context.Context, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) { t.Lock() defer t.Unlock() postIndexEntry := &indexedItemsEntry{ itemID: statusID, boostOfID: boostOfID, accountID: accountID, boostOfAccountID: boostOfAccountID, } if inserted, err := t.items.insertIndexed(ctx, postIndexEntry); err != nil { return false, fmt.Errorf("IndexAndPrepareOne: error inserting indexed: %w", err) } else if !inserted { // Entry wasn't inserted, so // don't bother preparing it. return false, nil } preparable, err := t.prepareFunction(ctx, t.timelineID, statusID) if err != nil { return true, fmt.Errorf("IndexAndPrepareOne: error preparing: %w", err) } postIndexEntry.prepared = preparable return true, nil } func (t *timeline) Len() int { t.Lock() defer t.Unlock() if t.items == nil || t.items.data == nil { // indexedItems hasnt been initialized yet. return 0 } return t.items.data.Len() } func (t *timeline) OldestIndexedItemID() string { t.Lock() defer t.Unlock() if t.items == nil || t.items.data == nil { // indexedItems hasnt been initialized yet. return "" } e := t.items.data.Back() if e == nil { // List was empty. return "" } return e.Value.(*indexedItemsEntry).itemID //nolint:forcetypeassert }