diff options
| author | 2025-10-17 19:41:06 +0200 | |
|---|---|---|
| committer | 2025-11-17 14:11:23 +0100 | |
| commit | 14bf8e62f81d7eac637f2097a88b4c3c32a8a7b5 (patch) | |
| tree | 56b675dadea6b525336ec9c109d932c224df9df5 /internal/db/bundb/relationship_follow.go | |
| parent | [chore] update dependencies (#4507) (diff) | |
| download | gotosocial-14bf8e62f81d7eac637f2097a88b4c3c32a8a7b5.tar.xz | |
[performance] reduce account stats database calls (#4496)
Reduces both code complexity and the number of separate database transactions we need to make by moving account statistics operations into the database as side-effects of the operations that effect them. In contrast to currently, where we manually update account statistics at the application layer.
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4496
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
Diffstat (limited to 'internal/db/bundb/relationship_follow.go')
| -rw-r--r-- | internal/db/bundb/relationship_follow.go | 284 |
1 files changed, 173 insertions, 111 deletions
diff --git a/internal/db/bundb/relationship_follow.go b/internal/db/bundb/relationship_follow.go index f37efa94b..ca0ac2bf8 100644 --- a/internal/db/bundb/relationship_follow.go +++ b/internal/db/bundb/relationship_follow.go @@ -189,7 +189,7 @@ func (r *relationshipDB) getFollow(ctx context.Context, lookup string, dbQuery f func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Follow) error { var ( err error - errs = gtserror.NewMultiError(2) + errs gtserror.MultiError ) if follow.Account == nil { @@ -218,8 +218,10 @@ func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Fo } func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error { - return r.state.Caches.DB.Follow.Store(follow, func() error { - _, err := r.db.NewInsert().Model(follow).Exec(ctx) + return r.insertFollow(ctx, follow, func(tx bun.Tx) error { + _, err := tx.NewInsert(). + Model(follow). + Exec(ctx) return err }) } @@ -239,7 +241,6 @@ func (r *relationshipDB) UpdateFollow(ctx context.Context, follow *gtsmodel.Foll Exec(ctx); err != nil { return err } - return nil }) } @@ -249,104 +250,63 @@ func (r *relationshipDB) DeleteFollow( sourceAccountID string, targetAccountID string, ) error { + return r.deleteFollow(ctx, func(tx bun.Tx) (*gtsmodel.Follow, error) { + var deleted gtsmodel.Follow + deleted.AccountID = sourceAccountID + deleted.TargetAccountID = targetAccountID + + if _, err := tx.NewDelete(). + Model(&deleted). + Where("? = ?", bun.Ident("account_id"), sourceAccountID). + Where("? = ?", bun.Ident("target_account_id"), targetAccountID). + Returning("?", bun.Ident("id")). + Exec(ctx); err != nil { + return nil, err + } - // Gather necessary fields from - // deleted for cache invaliation. - var deleted gtsmodel.Follow - deleted.AccountID = sourceAccountID - deleted.TargetAccountID = targetAccountID - - // Delete follow from origin - // account, to targeting account, - // returning the deleted models. - if _, err := r.db.NewDelete(). - Model(&deleted). - Where("? = ?", bun.Ident("account_id"), sourceAccountID). - Where("? = ?", bun.Ident("target_account_id"), targetAccountID). - Returning("?", bun.Ident("id")). - Exec(ctx); err != nil && - !errors.Is(err, db.ErrNoEntries) { - return err - } - - // Invalidate cached follow with source / target account IDs, - // manually calling invalidate hook in case it isn't cached. - r.state.Caches.DB.Follow.Invalidate("AccountID,TargetAccountID", - sourceAccountID, targetAccountID) - r.state.Caches.OnInvalidateFollow(&deleted) - - // Delete every list entry that was created targetting this follow ID. - if err := r.state.DB.DeleteAllListEntriesByFollows(ctx, deleted.ID); err != nil { - return gtserror.Newf("error deleting list entries: %w", err) - } - - return nil + return &deleted, nil + }) } func (r *relationshipDB) DeleteFollowByID(ctx context.Context, id string) error { - // Gather necessary fields from - // deleted for cache invaliation. - var deleted gtsmodel.Follow - deleted.ID = id - - // Delete follow with given ID, - // returning the deleted models. - if _, err := r.db.NewDelete(). - Model(&deleted). - Where("? = ?", bun.Ident("id"), id). - Returning("?, ?", - bun.Ident("account_id"), - bun.Ident("target_account_id"), - ). - Exec(ctx); err != nil && - !errors.Is(err, db.ErrNoEntries) { - return err - } - - // Invalidate cached follow with ID, manually - // call invalidate hook in case not cached. - r.state.Caches.DB.Follow.Invalidate("ID", id) - r.state.Caches.OnInvalidateFollow(&deleted) - - // Delete every list entry that was created targetting this follow ID. - if err := r.state.DB.DeleteAllListEntriesByFollows(ctx, id); err != nil { - return gtserror.Newf("error deleting list entries: %w", err) - } + return r.deleteFollow(ctx, func(tx bun.Tx) (*gtsmodel.Follow, error) { + var deleted gtsmodel.Follow + deleted.ID = id + + if _, err := tx.NewDelete(). + Model(&deleted). + Where("? = ?", bun.Ident("id"), id). + Returning("?, ?", + bun.Ident("account_id"), + bun.Ident("target_account_id"), + ). + Exec(ctx); err != nil { + return nil, err + } - return nil + return &deleted, nil + }) } func (r *relationshipDB) DeleteFollowByURI(ctx context.Context, uri string) error { - // Gather necessary fields from - // deleted for cache invaliation. - var deleted gtsmodel.Follow - - // Delete follow with given URI, - // returning the deleted models. - if _, err := r.db.NewDelete(). - Model(&deleted). - Where("? = ?", bun.Ident("uri"), uri). - Returning("?, ?, ?", - bun.Ident("id"), - bun.Ident("account_id"), - bun.Ident("target_account_id"), - ). - Exec(ctx); err != nil && - !errors.Is(err, db.ErrNoEntries) { - return err - } - - // Invalidate cached follow with URI, manually - // call invalidate hook in case not cached. - r.state.Caches.DB.Follow.Invalidate("URI", uri) - r.state.Caches.OnInvalidateFollow(&deleted) - - // Delete every list entry that was created targetting this follow ID. - if err := r.state.DB.DeleteAllListEntriesByFollows(ctx, deleted.ID); err != nil { - return gtserror.Newf("error deleting list entries: %w", err) - } + return r.deleteFollow(ctx, func(tx bun.Tx) (*gtsmodel.Follow, error) { + var deleted gtsmodel.Follow + deleted.URI = uri + + if _, err := tx.NewDelete(). + Model(&deleted). + Where("? = ?", bun.Ident("uri"), uri). + Returning("?, ?, ?", + bun.Ident("id"), + bun.Ident("account_id"), + bun.Ident("target_account_id"), + ). + Exec(ctx); err != nil { + return nil, err + } - return nil + return &deleted, nil + }) } func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID string) error { @@ -354,24 +314,52 @@ func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID str // deleted for cache invaliation. var deleted []*gtsmodel.Follow - // Delete all follows either from - // account, or targeting account, - // returning the deleted models. - if _, err := r.db.NewDelete(). - Model(&deleted). - WhereOr("? = ? OR ? = ?", - bun.Ident("account_id"), - accountID, - bun.Ident("target_account_id"), - accountID, - ). - Returning("?, ?, ?", - bun.Ident("id"), - bun.Ident("account_id"), - bun.Ident("target_account_id"), - ). - Exec(ctx); err != nil && - !errors.Is(err, db.ErrNoEntries) { + if err := r.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + // Delete all follows either from + // account, or targeting account, + // returning the deleted models. + if _, err := tx.NewDelete(). + Model(&deleted). + WhereOr("? = ? OR ? = ?", + bun.Ident("account_id"), + accountID, + bun.Ident("target_account_id"), + accountID, + ). + Returning("?, ?, ?", + bun.Ident("id"), + bun.Ident("account_id"), + bun.Ident("target_account_id"), + ). + Exec(ctx); err != nil { + + // the RETURNING here will cause an ErrNoRows + // to be returned on DELETE, which is caught + // outside this RunInTx() func, and ensures we + // return early here to *not* update statistics. + return err + } + + for _, follow := range deleted { + // Decrement origin following statistic. + if err := decrementAccountStats(ctx, tx, + "following_count", + follow.AccountID, + ); err != nil { + return err + } + + // Decrement target followers statistic. + if err := decrementAccountStats(ctx, tx, + "followers_count", + follow.TargetAccountID, + ); err != nil { + return err + } + } + + return nil + }); err != nil && !errors.Is(err, db.ErrNoEntries) { return err } @@ -397,3 +385,77 @@ func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID str return nil } + +func (r *relationshipDB) insertFollow(ctx context.Context, follow *gtsmodel.Follow, insert func(bun.Tx) error) error { + return r.state.Caches.DB.Follow.Store(follow, func() error { + return r.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + // Perform the insert operation. + if err := insert(tx); err != nil { + return gtserror.Newf("error inserting follow: %w", err) + } + + // Increment origin following statistic. + if err := incrementAccountStats(ctx, tx, + "following_count", + follow.AccountID, + ); err != nil { + return err + } + + // Increment target followers statistic. + return incrementAccountStats(ctx, tx, + "followers_count", + follow.TargetAccountID, + ) + }) + }) +} + +func (r *relationshipDB) deleteFollow(ctx context.Context, delete func(bun.Tx) (*gtsmodel.Follow, error)) error { + var follow *gtsmodel.Follow + + if err := r.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) (err error) { + // Perform delete operation. + follow, err = delete(tx) + if err != nil { + + // the RETURNING here will cause an ErrNoRows + // to be returned on DELETE, which is caught + // outside this RunInTx() func, and ensures we + // return early here to *not* update statistics. + return err + } + + // Decrement origin following statistic. + if err := decrementAccountStats(ctx, tx, + "following_count", + follow.AccountID, + ); err != nil { + return err + } + + // Decrement target followers statistic. + return decrementAccountStats(ctx, tx, + "followers_count", + follow.TargetAccountID, + ) + }); err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } + + if follow == nil { + return nil + } + + // Invalidate cached follow with ID, manually + // call invalidate hook in case not cached. + r.state.Caches.DB.Follow.Invalidate("ID", follow.ID) + r.state.Caches.OnInvalidateFollow(follow) + + // Delete every list entry that was created targetting this follow ID. + if err := r.state.DB.DeleteAllListEntriesByFollows(ctx, follow.ID); err != nil { + return gtserror.Newf("error deleting list entries: %w", err) + } + + return nil +} |
