index.go (6978B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package timeline 19 20 import ( 21 "container/list" 22 "context" 23 "errors" 24 25 "codeberg.org/gruf/go-kv" 26 "github.com/superseriousbusiness/gotosocial/internal/db" 27 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 28 "github.com/superseriousbusiness/gotosocial/internal/log" 29 ) 30 31 func (t *timeline) indexXBetweenIDs(ctx context.Context, amount int, behindID string, beforeID string, frontToBack bool) error { 32 l := log. 33 WithContext(ctx). 34 WithFields(kv.Fields{ 35 {"amount", amount}, 36 {"behindID", behindID}, 37 {"beforeID", beforeID}, 38 {"frontToBack", frontToBack}, 39 }...) 40 l.Trace("entering indexXBetweenIDs") 41 42 if beforeID >= behindID { 43 // This is an impossible situation, we 44 // can't index anything between these. 45 return nil 46 } 47 48 t.Lock() 49 defer t.Unlock() 50 51 // Lazily init indexed items. 52 if t.items.data == nil { 53 t.items.data = &list.List{} 54 t.items.data.Init() 55 } 56 57 // Start by mapping out the list so we know what 58 // we have to do. Depending on the current state 59 // of the list we might not have to do *anything*. 60 var ( 61 position int 62 listLen = t.items.data.Len() 63 behindIDPosition int 64 beforeIDPosition int 65 ) 66 67 for e := t.items.data.Front(); e != nil; e = e.Next() { 68 entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert 69 70 position++ 71 72 if entry.itemID > behindID { 73 l.Trace("item is too new, continuing") 74 continue 75 } 76 77 if behindIDPosition == 0 { 78 // Gone far enough through the list 79 // and found our behindID mark. 80 // We only need to set this once. 81 l.Tracef("found behindID mark %s at position %d", entry.itemID, position) 82 behindIDPosition = position 83 } 84 85 if entry.itemID >= beforeID { 86 // Push the beforeID mark back 87 // one place every iteration. 88 l.Tracef("setting beforeID mark %s at position %d", entry.itemID, position) 89 beforeIDPosition = position 90 } 91 92 if entry.itemID <= beforeID { 93 // We've gone beyond the bounds of 94 // items we're interested in; stop. 95 l.Trace("reached older items, breaking") 96 break 97 } 98 } 99 100 // We can now figure out if we need to make db calls. 101 var grabMore bool 102 switch { 103 case listLen < amount: 104 // The whole list is shorter than the 105 // amount we're being asked to return, 106 // make up the difference. 107 grabMore = true 108 amount -= listLen 109 case beforeIDPosition-behindIDPosition < amount: 110 // Not enough items between behindID and 111 // beforeID to return amount required, 112 // try to get more. 113 grabMore = true 114 } 115 116 if !grabMore { 117 // We're good! 118 return nil 119 } 120 121 // Fetch additional items. 122 items, err := t.grab(ctx, amount, behindID, beforeID, frontToBack) 123 if err != nil { 124 return err 125 } 126 127 // Index all the items we got. We already have 128 // a lock on the timeline, so don't call IndexOne 129 // here, since that will also try to get a lock! 130 for _, item := range items { 131 entry := &indexedItemsEntry{ 132 itemID: item.GetID(), 133 boostOfID: item.GetBoostOfID(), 134 accountID: item.GetAccountID(), 135 boostOfAccountID: item.GetBoostOfAccountID(), 136 } 137 138 if _, err := t.items.insertIndexed(ctx, entry); err != nil { 139 return gtserror.Newf("error inserting entry with itemID %s into index: %w", entry.itemID, err) 140 } 141 } 142 143 return nil 144 } 145 146 // grab wraps the timeline's grabFunction in paging + filtering logic. 147 func (t *timeline) grab(ctx context.Context, amount int, behindID string, beforeID string, frontToBack bool) ([]Timelineable, error) { 148 var ( 149 sinceID string 150 minID string 151 grabbed int 152 maxID = behindID 153 filtered = make([]Timelineable, 0, amount) 154 ) 155 156 if frontToBack { 157 sinceID = beforeID 158 } else { 159 minID = beforeID 160 } 161 162 for attempts := 0; attempts < 5; attempts++ { 163 if grabbed >= amount { 164 // We got everything we needed. 165 break 166 } 167 168 items, stop, err := t.grabFunction( 169 ctx, 170 t.timelineID, 171 maxID, 172 sinceID, 173 minID, 174 // Don't grab more than we need to. 175 amount-grabbed, 176 ) 177 178 if err != nil { 179 // Grab function already checks for 180 // db.ErrNoEntries, so if an error 181 // is returned then it's a real one. 182 return nil, err 183 } 184 185 if stop || len(items) == 0 { 186 // No items left. 187 break 188 } 189 190 // Set next query parameters. 191 if frontToBack { 192 // Page down. 193 maxID = items[len(items)-1].GetID() 194 if maxID <= beforeID { 195 // Can't go any further. 196 break 197 } 198 } else { 199 // Page up. 200 minID = items[0].GetID() 201 if minID >= behindID { 202 // Can't go any further. 203 break 204 } 205 } 206 207 for _, item := range items { 208 ok, err := t.filterFunction(ctx, t.timelineID, item) 209 if err != nil { 210 if !errors.Is(err, db.ErrNoEntries) { 211 // Real error here. 212 return nil, err 213 } 214 log.Warnf(ctx, "errNoEntries while filtering item %s: %s", item.GetID(), err) 215 continue 216 } 217 218 if ok { 219 filtered = append(filtered, item) 220 grabbed++ // count this as grabbed 221 } 222 } 223 } 224 225 return filtered, nil 226 } 227 228 func (t *timeline) IndexAndPrepareOne(ctx context.Context, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) { 229 t.Lock() 230 defer t.Unlock() 231 232 postIndexEntry := &indexedItemsEntry{ 233 itemID: statusID, 234 boostOfID: boostOfID, 235 accountID: accountID, 236 boostOfAccountID: boostOfAccountID, 237 } 238 239 if inserted, err := t.items.insertIndexed(ctx, postIndexEntry); err != nil { 240 return false, gtserror.Newf("error inserting indexed: %w", err) 241 } else if !inserted { 242 // Entry wasn't inserted, so 243 // don't bother preparing it. 244 return false, nil 245 } 246 247 preparable, err := t.prepareFunction(ctx, t.timelineID, statusID) 248 if err != nil { 249 return true, gtserror.Newf("error preparing: %w", err) 250 } 251 postIndexEntry.prepared = preparable 252 253 return true, nil 254 } 255 256 func (t *timeline) Len() int { 257 t.Lock() 258 defer t.Unlock() 259 260 if t.items == nil || t.items.data == nil { 261 // indexedItems hasnt been initialized yet. 262 return 0 263 } 264 265 return t.items.data.Len() 266 } 267 268 func (t *timeline) OldestIndexedItemID() string { 269 t.Lock() 270 defer t.Unlock() 271 272 if t.items == nil || t.items.data == nil { 273 // indexedItems hasnt been initialized yet. 274 return "" 275 } 276 277 e := t.items.data.Back() 278 if e == nil { 279 // List was empty. 280 return "" 281 } 282 283 return e.Value.(*indexedItemsEntry).itemID //nolint:forcetypeassert 284 }