[feature] status refetch support (#1690)

* revamp http client to not limit requests, instead use sender worker

Signed-off-by: kim <grufwub@gmail.com>

* remove separate sender worker pool, spawn 2*GOMAXPROCS batch senders each time, no need for transport cache sweeping

Signed-off-by: kim <grufwub@gmail.com>

* improve batch senders to keep popping recipients until remote URL found

Signed-off-by: kim <grufwub@gmail.com>

* fix recipient looping issue

Signed-off-by: kim <grufwub@gmail.com>

* move request id ctx key to gtscontext, finish filling out more code comments, add basic support for not logging client IP

Signed-off-by: kim <grufwub@gmail.com>

* first draft of status refetching logic

Signed-off-by: kim <grufwub@gmail.com>

* fix testrig to use new federation alloc func signature

Signed-off-by: kim <grufwub@gmail.com>

* fix log format directive

Signed-off-by: kim <grufwub@gmail.com>

* add status fetched_at migration

Signed-off-by: kim <grufwub@gmail.com>

* remove unused / unchecked for error types

Signed-off-by: kim <grufwub@gmail.com>

* add back the used type...

Signed-off-by: kim <grufwub@gmail.com>

* add separate internal getStatus() function for derefThread() that doesn't recurse

Signed-off-by: kim <grufwub@gmail.com>

* improved mention and media attachment error handling

Signed-off-by: kim <grufwub@gmail.com>

* fix log and error format directives

Signed-off-by: kim <grufwub@gmail.com>

* update account deref to match status deref changes

Signed-off-by: kim <grufwub@gmail.com>

* very small code formatting change to make things clearer

Signed-off-by: kim <grufwub@gmail.com>

* add more code comments

Signed-off-by: kim <grufwub@gmail.com>

* improved code commenting

Signed-off-by: kim <grufwub@gmail.com>

* only check for required further derefs if needed

Signed-off-by: kim <grufwub@gmail.com>

* improved cache invalidation

Signed-off-by: kim <grufwub@gmail.com>

* tweak cache restarting to use a (very small) backoff

Signed-off-by: kim <grufwub@gmail.com>

* small readability changes and fixes

Signed-off-by: kim <grufwub@gmail.com>

* fix account sync issues

Signed-off-by: kim <grufwub@gmail.com>

* fix merge conflicts + update account enrichment to accept already-passed accountable

Signed-off-by: kim <grufwub@gmail.com>

* remove secondary function declaration

Signed-off-by: kim <grufwub@gmail.com>

* normalise dereferencer get status / account behaviour, fix remaining tests

Signed-off-by: kim <grufwub@gmail.com>

* fix remaining rebase conflicts, finish commenting code

Signed-off-by: kim <grufwub@gmail.com>

* appease the linter

Signed-off-by: kim <grufwub@gmail.com>

* add source file header

Signed-off-by: kim <grufwub@gmail.com>

* update to use TIMESTAMPTZ column type instead of just TIMESTAMP

Signed-off-by: kim <grufwub@gmail.com>

* don't pass in 'updated_at' to UpdateEmoji()

Signed-off-by: kim <grufwub@gmail.com>

* use new ap.Resolve{Account,Status}able() functions

Signed-off-by: kim <grufwub@gmail.com>

* remove the somewhat confusing rescoping of the same variable names

Signed-off-by: kim <grufwub@gmail.com>

* update migration file name, improved database delete error returns

Signed-off-by: kim <grufwub@gmail.com>

* formatting

Signed-off-by: kim <grufwub@gmail.com>

* improved multi-delete database functions to minimise DB calls

Signed-off-by: kim <grufwub@gmail.com>

* remove unused type

Signed-off-by: kim <grufwub@gmail.com>

* fix delete statements

Signed-off-by: kim <grufwub@gmail.com>

---------

Signed-off-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2023-05-12 10:15:54 +01:00 committed by GitHub
commit 6c9d8e78eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1552 additions and 1118 deletions

View file

@ -302,7 +302,7 @@ func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account
columns = append(columns, "updated_at")
}
err := a.state.Caches.GTS.Account().Store(account, func() error {
return a.state.Caches.GTS.Account().Store(account, func() error {
// It is safe to run this database transaction within cache.Store
// as the cache does not attempt a mutex lock until AFTER hook.
//
@ -338,15 +338,23 @@ func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account
return err
})
})
if err != nil {
return err
}
return nil
}
func (a *accountDB) DeleteAccount(ctx context.Context, id string) db.Error {
if err := a.conn.RunInTx(ctx, func(tx bun.Tx) error {
defer a.state.Caches.GTS.Account().Invalidate("ID", id)
// Load account into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := a.GetAccountByID(gtscontext.SetBarebones(ctx), id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// NOTE: even if db.ErrNoEntries is returned, we
// still run the below transaction to ensure related
// objects are appropriately deleted.
return err
}
return a.conn.RunInTx(ctx, func(tx bun.Tx) error {
// clear out any emoji links
if _, err := tx.
NewDelete().
@ -363,14 +371,7 @@ func (a *accountDB) DeleteAccount(ctx context.Context, id string) db.Error {
Where("? = ?", bun.Ident("account.id"), id).
Exec(ctx)
return err
}); err != nil {
return err
}
// Invalidate account from database lookups.
a.state.Caches.GTS.Account().Invalidate("ID", id)
return nil
})
}
func (a *accountDB) GetAccountLastPosted(ctx context.Context, accountID string, webOnly bool) (time.Time, db.Error) {

View file

@ -66,9 +66,9 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
}
func (suite *BunDBStandardTestSuite) SetupTest() {
suite.state.Caches.Init()
testrig.InitTestConfig()
testrig.InitTestLog()
suite.state.Caches.Init()
suite.db = testrig.NewTestDB(&suite.state)
testrig.StandardDBSetup(suite.db, suite.testAccounts)
}

View file

@ -19,10 +19,12 @@ package bundb
import (
"context"
"errors"
"strings"
"time"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
@ -56,24 +58,46 @@ func (e *emojiDB) PutEmoji(ctx context.Context, emoji *gtsmodel.Emoji) db.Error
}
func (e *emojiDB) UpdateEmoji(ctx context.Context, emoji *gtsmodel.Emoji, columns ...string) (*gtsmodel.Emoji, db.Error) {
// Update the emoji's last-updated
emoji.UpdatedAt = time.Now()
if _, err := e.conn.
NewUpdate().
Model(emoji).
Where("? = ?", bun.Ident("emoji.id"), emoji.ID).
Column(columns...).
Exec(ctx); err != nil {
return nil, e.conn.ProcessError(err)
if len(columns) > 0 {
// If we're updating by column, ensure "updated_at" is included.
columns = append(columns, "updated_at")
}
err := e.state.Caches.GTS.Emoji().Store(emoji, func() error {
_, err := e.conn.
NewUpdate().
Model(emoji).
Where("? = ?", bun.Ident("emoji.id"), emoji.ID).
Column(columns...).
Exec(ctx)
return e.conn.ProcessError(err)
})
if err != nil {
return nil, err
}
e.state.Caches.GTS.Emoji().Invalidate("ID", emoji.ID)
return emoji, nil
}
func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) db.Error {
if err := e.conn.RunInTx(ctx, func(tx bun.Tx) error {
defer e.state.Caches.GTS.Emoji().Invalidate("ID", id)
// Load emoji into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := e.GetEmojiByID(
gtscontext.SetBarebones(ctx),
id,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// NOTE: even if db.ErrNoEntries is returned, we
// still run the below transaction to ensure related
// objects are appropriately deleted.
return err
}
return e.conn.RunInTx(ctx, func(tx bun.Tx) error {
// delete links between this emoji and any statuses that use it
if _, err := tx.
NewDelete().
@ -101,12 +125,7 @@ func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) db.Error {
}
return nil
}); err != nil {
return err
}
e.state.Caches.GTS.Emoji().Invalidate("ID", id)
return nil
})
}
func (e *emojiDB) GetEmojis(ctx context.Context, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) ([]*gtsmodel.Emoji, db.Error) {

View file

@ -52,7 +52,8 @@ func processSQLiteError(err error) db.Error {
// Handle supplied error code:
switch sqliteErr.Code() {
case sqlite3.SQLITE_CONSTRAINT_UNIQUE, sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY:
case sqlite3.SQLITE_CONSTRAINT_UNIQUE,
sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY:
return db.ErrAlreadyExists
default:
return err

View file

@ -19,9 +19,11 @@ package bundb
import (
"context"
"errors"
"time"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
@ -103,17 +105,26 @@ func (m *mediaDB) UpdateAttachment(ctx context.Context, media *gtsmodel.MediaAtt
}
func (m *mediaDB) DeleteAttachment(ctx context.Context, id string) error {
// Attempt to delete from database.
if _, err := m.conn.NewDelete().
TableExpr("? AS ?", bun.Ident("media_attachments"), bun.Ident("media_attachment")).
Where("? = ?", bun.Ident("media_attachment.id"), id).
Exec(ctx); err != nil {
return m.conn.ProcessError(err)
defer m.state.Caches.GTS.Media().Invalidate("ID", id)
// Load media into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := m.GetAttachmentByID(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
// Invalidate this media item from the cache.
m.state.Caches.GTS.Media().Invalidate("ID", id)
return nil
// Finally delete media from DB.
_, err = m.conn.NewDelete().
TableExpr("? AS ?", bun.Ident("media_attachments"), bun.Ident("media_attachment")).
Where("? = ?", bun.Ident("media_attachment.id"), id).
Exec(ctx)
return m.conn.ProcessError(err)
}
func (m *mediaDB) GetRemoteOlderThan(ctx context.Context, olderThan time.Time, limit int) ([]*gtsmodel.MediaAttachment, db.Error) {

View file

@ -19,6 +19,7 @@ package bundb
import (
"context"
"errors"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -109,16 +110,24 @@ func (m *mentionDB) PutMention(ctx context.Context, mention *gtsmodel.Mention) e
}
func (m *mentionDB) DeleteMentionByID(ctx context.Context, id string) error {
if _, err := m.conn.
NewDelete().
Table("mentions").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx); err != nil {
return m.conn.ProcessError(err)
defer m.state.Caches.GTS.Mention().Invalidate("ID", id)
// Load mention into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := m.GetMention(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
// Invalidate mention from the lookup cache.
m.state.Caches.GTS.Mention().Invalidate("ID", id)
return nil
// Finally delete mention from DB.
_, err = m.conn.NewDelete().
Table("mentions").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx)
return m.conn.ProcessError(err)
}

View file

@ -0,0 +1,47 @@
// 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 <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
"strings"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
_, err := tx.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TIMESTAMPTZ", bun.Ident("statuses"), bun.Ident("fetched_at"))
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
return err
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -22,6 +22,7 @@ import (
"errors"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
@ -179,16 +180,26 @@ func (n *notificationDB) PutNotification(ctx context.Context, notif *gtsmodel.No
}
func (n *notificationDB) DeleteNotificationByID(ctx context.Context, id string) db.Error {
if _, err := n.conn.
NewDelete().
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
Where("? = ?", bun.Ident("notification.id"), id).
Exec(ctx); err != nil {
return n.conn.ProcessError(err)
defer n.state.Caches.GTS.Notification().Invalidate("ID", id)
// Load notif into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := n.GetNotificationByID(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
n.state.Caches.GTS.Notification().Invalidate("ID", id)
return nil
// Finally delete notif from DB.
_, err = n.conn.NewDelete().
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
Where("? = ?", bun.Ident("notification.id"), id).
Exec(ctx)
return n.conn.ProcessError(err)
}
func (n *notificationDB) DeleteNotifications(ctx context.Context, types []string, targetAccountID string, originAccountID string) db.Error {
@ -196,56 +207,88 @@ func (n *notificationDB) DeleteNotifications(ctx context.Context, types []string
return errors.New("DeleteNotifications: one of targetAccountID or originAccountID must be set")
}
// Capture notification IDs in a RETURNING statement.
var ids []string
var notifIDs []string
q := n.conn.
NewDelete().
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
Returning("?", bun.Ident("id"))
NewSelect().
Column("id").
Table("notifications")
if len(types) > 0 {
q = q.Where("? IN (?)", bun.Ident("notification.notification_type"), bun.In(types))
q = q.Where("? IN (?)", bun.Ident("notification_type"), bun.In(types))
}
if targetAccountID != "" {
q = q.Where("? = ?", bun.Ident("notification.target_account_id"), targetAccountID)
q = q.Where("? = ?", bun.Ident("target_account_id"), targetAccountID)
}
if originAccountID != "" {
q = q.Where("? = ?", bun.Ident("notification.origin_account_id"), originAccountID)
q = q.Where("? = ?", bun.Ident("origin_account_id"), originAccountID)
}
if _, err := q.Exec(ctx, &ids); err != nil {
if _, err := q.Exec(ctx, &notifIDs); err != nil {
return n.conn.ProcessError(err)
}
// Invalidate each returned ID.
for _, id := range ids {
n.state.Caches.GTS.Notification().Invalidate("ID", id)
defer func() {
// Invalidate all IDs on return.
for _, id := range notifIDs {
n.state.Caches.GTS.Notification().Invalidate("ID", id)
}
}()
// Load all notif into cache, this *really* isn't great
// but it is the only way we can ensure we invalidate all
// related caches correctly (e.g. visibility).
for _, id := range notifIDs {
_, err := n.GetNotificationByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
}
return nil
// Finally delete all from DB.
_, err := n.conn.NewDelete().
Table("notifications").
Where("? IN (?)", bun.Ident("id"), bun.In(notifIDs)).
Exec(ctx)
return n.conn.ProcessError(err)
}
func (n *notificationDB) DeleteNotificationsForStatus(ctx context.Context, statusID string) db.Error {
// Capture notification IDs in a RETURNING statement.
var ids []string
var notifIDs []string
q := n.conn.
NewDelete().
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
Where("? = ?", bun.Ident("notification.status_id"), statusID).
Returning("?", bun.Ident("id"))
NewSelect().
Column("id").
Table("notifications").
Where("? = ?", bun.Ident("status_id"), statusID)
if _, err := q.Exec(ctx, &ids); err != nil {
if _, err := q.Exec(ctx, &notifIDs); err != nil {
return n.conn.ProcessError(err)
}
// Invalidate each returned ID.
for _, id := range ids {
n.state.Caches.GTS.Notification().Invalidate("ID", id)
defer func() {
// Invalidate all IDs on return.
for _, id := range notifIDs {
n.state.Caches.GTS.Notification().Invalidate("ID", id)
}
}()
// Load all notif into cache, this *really* isn't great
// but it is the only way we can ensure we invalidate all
// related caches correctly (e.g. visibility).
for _, id := range notifIDs {
_, err := n.GetNotificationByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
}
return nil
// Finally delete all from DB.
_, err := n.conn.NewDelete().
Table("notifications").
Where("? IN (?)", bun.Ident("id"), bun.In(notifIDs)).
Exec(ctx)
return n.conn.ProcessError(err)
}

View file

@ -25,7 +25,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/uptrace/bun"
)
@ -142,62 +141,65 @@ func (r *relationshipDB) getBlock(ctx context.Context, lookup string, dbQuery fu
}
func (r *relationshipDB) PutBlock(ctx context.Context, block *gtsmodel.Block) error {
err := r.state.Caches.GTS.Block().Store(block, func() error {
return r.state.Caches.GTS.Block().Store(block, func() error {
_, err := r.conn.NewInsert().Model(block).Exec(ctx)
return r.conn.ProcessError(err)
})
if err != nil {
return err
}
// Invalidate block origin account ID cached visibility.
r.state.Caches.Visibility.Invalidate("ItemID", block.AccountID)
r.state.Caches.Visibility.Invalidate("RequesterID", block.AccountID)
// Invalidate block target account ID cached visibility.
r.state.Caches.Visibility.Invalidate("ItemID", block.TargetAccountID)
r.state.Caches.Visibility.Invalidate("RequesterID", block.TargetAccountID)
return nil
}
func (r *relationshipDB) DeleteBlockByID(ctx context.Context, id string) error {
block, err := r.GetBlockByID(gtscontext.SetBarebones(ctx), id)
defer r.state.Caches.GTS.Block().Invalidate("ID", id)
// Load block into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetBlockByID(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
return r.deleteBlock(ctx, block)
// Finally delete block from DB.
_, err = r.conn.NewDelete().
Table("blocks").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx)
return r.conn.ProcessError(err)
}
func (r *relationshipDB) DeleteBlockByURI(ctx context.Context, uri string) error {
block, err := r.GetBlockByURI(gtscontext.SetBarebones(ctx), uri)
defer r.state.Caches.GTS.Block().Invalidate("URI", uri)
// Load block into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetBlockByURI(gtscontext.SetBarebones(ctx), uri)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
return r.deleteBlock(ctx, block)
}
func (r *relationshipDB) deleteBlock(ctx context.Context, block *gtsmodel.Block) error {
if _, err := r.conn.
NewDelete().
// Finally delete block from DB.
_, err = r.conn.NewDelete().
Table("blocks").
Where("? = ?", bun.Ident("id"), block.ID).
Exec(ctx); err != nil {
return r.conn.ProcessError(err)
}
// Invalidate block from cache lookups.
r.state.Caches.GTS.Block().Invalidate("ID", block.ID)
return nil
Where("? = ?", bun.Ident("uri"), uri).
Exec(ctx)
return r.conn.ProcessError(err)
}
func (r *relationshipDB) DeleteAccountBlocks(ctx context.Context, accountID string) error {
var blockIDs []string
// Get full list of IDs.
if err := r.conn.NewSelect().
Column("id").
Table("blocks").
ColumnExpr("?", bun.Ident("id")).
WhereOr("? = ? OR ? = ?",
bun.Ident("account_id"),
accountID,
@ -208,11 +210,27 @@ func (r *relationshipDB) DeleteAccountBlocks(ctx context.Context, accountID stri
return r.conn.ProcessError(err)
}
defer func() {
// Invalidate all IDs on return.
for _, id := range blockIDs {
r.state.Caches.GTS.Block().Invalidate("ID", id)
}
}()
// Load all blocks into cache, this *really* isn't great
// but it is the only way we can ensure we invalidate all
// related caches correctly (e.g. visibility).
for _, id := range blockIDs {
if err := r.DeleteBlockByID(ctx, id); err != nil {
log.Errorf(ctx, "error deleting block %q: %v", id, err)
_, err := r.GetBlockByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
}
return nil
// Finally delete all from DB.
_, err := r.conn.NewDelete().
Table("blocks").
Where("? IN (?)", bun.Ident("id"), bun.In(blockIDs)).
Exec(ctx)
return r.conn.ProcessError(err)
}

View file

@ -171,23 +171,10 @@ func (r *relationshipDB) getFollow(ctx context.Context, lookup string, dbQuery f
}
func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error {
err := r.state.Caches.GTS.Follow().Store(follow, func() error {
return r.state.Caches.GTS.Follow().Store(follow, func() error {
_, err := r.conn.NewInsert().Model(follow).Exec(ctx)
return r.conn.ProcessError(err)
})
if err != nil {
return err
}
// Invalidate follow origin account ID cached visibility.
r.state.Caches.Visibility.Invalidate("ItemID", follow.AccountID)
r.state.Caches.Visibility.Invalidate("RequesterID", follow.AccountID)
// Invalidate follow target account ID cached visibility.
r.state.Caches.Visibility.Invalidate("ItemID", follow.TargetAccountID)
r.state.Caches.Visibility.Invalidate("RequesterID", follow.TargetAccountID)
return nil
}
func (r *relationshipDB) UpdateFollow(ctx context.Context, follow *gtsmodel.Follow, columns ...string) error {
@ -211,38 +198,58 @@ func (r *relationshipDB) UpdateFollow(ctx context.Context, follow *gtsmodel.Foll
}
func (r *relationshipDB) DeleteFollowByID(ctx context.Context, id string) error {
if _, err := r.conn.NewDelete().
Table("follows").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx); err != nil {
return r.conn.ProcessError(err)
defer r.state.Caches.GTS.Follow().Invalidate("ID", id)
// Load follow into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetFollowByID(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
// Invalidate follow from cache lookups.
r.state.Caches.GTS.Follow().Invalidate("ID", id)
return nil
// Finally delete follow from DB.
_, err = r.conn.NewDelete().
Table("follows").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx)
return r.conn.ProcessError(err)
}
func (r *relationshipDB) DeleteFollowByURI(ctx context.Context, uri string) error {
if _, err := r.conn.NewDelete().
Table("follows").
Where("? = ?", bun.Ident("uri"), uri).
Exec(ctx); err != nil {
return r.conn.ProcessError(err)
defer r.state.Caches.GTS.Follow().Invalidate("URI", uri)
// Load follow into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetFollowByURI(gtscontext.SetBarebones(ctx), uri)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
// Invalidate follow from cache lookups.
r.state.Caches.GTS.Follow().Invalidate("URI", uri)
return nil
// Finally delete follow from DB.
_, err = r.conn.NewDelete().
Table("follows").
Where("? = ?", bun.Ident("uri"), uri).
Exec(ctx)
return r.conn.ProcessError(err)
}
func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID string) error {
var followIDs []string
// Get full list of IDs.
if _, err := r.conn.
NewDelete().
NewSelect().
Column("id").
Table("follows").
WhereOr("? = ? OR ? = ?",
bun.Ident("account_id"),
@ -250,15 +257,31 @@ func (r *relationshipDB) DeleteAccountFollows(ctx context.Context, accountID str
bun.Ident("target_account_id"),
accountID,
).
Returning("?", bun.Ident("id")).
Exec(ctx, &followIDs); err != nil {
return r.conn.ProcessError(err)
}
// Invalidate each returned ID.
defer func() {
// Invalidate all IDs on return.
for _, id := range followIDs {
r.state.Caches.GTS.Follow().Invalidate("ID", id)
}
}()
// Load all follows into cache, this *really* isn't great
// but it is the only way we can ensure we invalidate all
// related caches correctly (e.g. visibility).
for _, id := range followIDs {
r.state.Caches.GTS.Follow().Invalidate("ID", id)
_, err := r.GetFollowByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
}
return nil
// Finally delete all from DB.
_, err := r.conn.NewDelete().
Table("follows").
Where("? IN (?)", bun.Ident("id"), bun.In(followIDs)).
Exec(ctx)
return r.conn.ProcessError(err)
}

View file

@ -149,23 +149,10 @@ func (r *relationshipDB) getFollowRequest(ctx context.Context, lookup string, db
}
func (r *relationshipDB) PutFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error {
err := r.state.Caches.GTS.FollowRequest().Store(follow, func() error {
return r.state.Caches.GTS.FollowRequest().Store(follow, func() error {
_, err := r.conn.NewInsert().Model(follow).Exec(ctx)
return r.conn.ProcessError(err)
})
if err != nil {
return err
}
// Invalidate follow request origin account ID cached visibility.
r.state.Caches.Visibility.Invalidate("ItemID", follow.AccountID)
r.state.Caches.Visibility.Invalidate("RequesterID", follow.AccountID)
// Invalidate follow request target account ID cached visibility.
r.state.Caches.Visibility.Invalidate("ItemID", follow.TargetAccountID)
r.state.Caches.Visibility.Invalidate("RequesterID", follow.TargetAccountID)
return nil
}
func (r *relationshipDB) UpdateFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest, columns ...string) error {
@ -221,6 +208,9 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI
return nil, err
}
// Invalidate follow request from cache lookups on return.
defer r.state.Caches.GTS.FollowRequest().Invalidate("ID", followReq.ID)
// Delete original follow request.
if _, err := r.conn.
NewDelete().
@ -230,9 +220,6 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI
return nil, r.conn.ProcessError(err)
}
// Invalidate follow request from cache lookups
r.state.Caches.GTS.FollowRequest().Invalidate("ID", followReq.ID)
// Delete original follow request notification
if err := r.state.DB.DeleteNotifications(ctx, []string{
string(gtsmodel.NotificationFollowRequest),
@ -244,15 +231,30 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, sourceAccountI
}
func (r *relationshipDB) RejectFollowRequest(ctx context.Context, sourceAccountID string, targetAccountID string) db.Error {
// Get original follow request.
followReq, err := r.GetFollowRequest(ctx, sourceAccountID, targetAccountID)
defer r.state.Caches.GTS.FollowRequest().Invalidate("AccountID.TargetAccountID", sourceAccountID, targetAccountID)
// Load followreq into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetFollowRequest(gtscontext.SetBarebones(ctx),
sourceAccountID,
targetAccountID,
)
if err != nil {
return err
}
// Delete original follow request.
if err := r.DeleteFollowRequestByID(ctx, followReq.ID); err != nil {
return err
// Attempt to delete follow request.
if _, err = r.conn.NewDelete().
Table("follow_requests").
Where("? = ? AND ? = ?",
bun.Ident("account_id"),
sourceAccountID,
bun.Ident("target_account_id"),
targetAccountID,
).
Exec(ctx); err != nil {
return r.conn.ProcessError(err)
}
// Delete original follow request notification
@ -262,54 +264,90 @@ func (r *relationshipDB) RejectFollowRequest(ctx context.Context, sourceAccountI
}
func (r *relationshipDB) DeleteFollowRequestByID(ctx context.Context, id string) error {
if _, err := r.conn.NewDelete().
Table("follow_requests").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx); err != nil {
return r.conn.ProcessError(err)
defer r.state.Caches.GTS.FollowRequest().Invalidate("ID", id)
// Load followreq into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetFollowRequestByID(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
// Invalidate follow request from cache lookups.
r.state.Caches.GTS.FollowRequest().Invalidate("ID", id)
return nil
// Finally delete followreq from DB.
_, err = r.conn.NewDelete().
Table("follow_requests").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx)
return r.conn.ProcessError(err)
}
func (r *relationshipDB) DeleteFollowRequestByURI(ctx context.Context, uri string) error {
if _, err := r.conn.NewDelete().
Table("follow_requests").
Where("? = ?", bun.Ident("uri"), uri).
Exec(ctx); err != nil {
return r.conn.ProcessError(err)
defer r.state.Caches.GTS.FollowRequest().Invalidate("URI", uri)
// Load followreq into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetFollowRequestByURI(gtscontext.SetBarebones(ctx), uri)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
// Invalidate follow request from cache lookups.
r.state.Caches.GTS.FollowRequest().Invalidate("URI", uri)
return nil
// Finally delete followreq from DB.
_, err = r.conn.NewDelete().
Table("follow_requests").
Where("? = ?", bun.Ident("uri"), uri).
Exec(ctx)
return r.conn.ProcessError(err)
}
func (r *relationshipDB) DeleteAccountFollowRequests(ctx context.Context, accountID string) error {
var followIDs []string
var followReqIDs []string
// Get full list of IDs.
if _, err := r.conn.
NewDelete().
Table("follow_requests").
NewSelect().
Column("id").
Table("follow_requestss").
WhereOr("? = ? OR ? = ?",
bun.Ident("account_id"),
accountID,
bun.Ident("target_account_id"),
accountID,
).
Returning("?", bun.Ident("id")).
Exec(ctx, &followIDs); err != nil {
Exec(ctx, &followReqIDs); err != nil {
return r.conn.ProcessError(err)
}
// Invalidate each returned ID.
for _, id := range followIDs {
r.state.Caches.GTS.FollowRequest().Invalidate("ID", id)
defer func() {
// Invalidate all IDs on return.
for _, id := range followReqIDs {
r.state.Caches.GTS.FollowRequest().Invalidate("ID", id)
}
}()
// Load all followreqs into cache, this *really* isn't
// great but it is the only way we can ensure we invalidate
// all related caches correctly (e.g. visibility).
for _, id := range followReqIDs {
_, err := r.GetFollowRequestByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
}
return nil
// Finally delete all from DB.
_, err := r.conn.NewDelete().
Table("follow_requests").
Where("? IN (?)", bun.Ident("id"), bun.In(followReqIDs)).
Exec(ctx)
return r.conn.ProcessError(err)
}

View file

@ -19,10 +19,12 @@ package bundb
import (
"context"
"errors"
"fmt"
"time"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
@ -192,14 +194,24 @@ func (r *reportDB) UpdateReport(ctx context.Context, report *gtsmodel.Report, co
}
func (r *reportDB) DeleteReportByID(ctx context.Context, id string) db.Error {
if _, err := r.conn.
NewDelete().
TableExpr("? AS ?", bun.Ident("reports"), bun.Ident("report")).
Where("? = ?", bun.Ident("report.id"), id).
Exec(ctx); err != nil {
return r.conn.ProcessError(err)
defer r.state.Caches.GTS.Report().Invalidate("ID", id)
// Load status into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := r.GetReportByID(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
r.state.Caches.GTS.Report().Invalidate("ID", id)
return nil
// Finally delete report from DB.
_, err = r.conn.NewDelete().
TableExpr("? AS ?", bun.Ident("reports"), bun.Ident("report")).
Where("? = ?", bun.Ident("report.id"), id).
Exec(ctx)
return r.conn.ProcessError(err)
}

View file

@ -244,7 +244,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
}
func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) db.Error {
err := s.state.Caches.GTS.Status().Store(status, func() error {
return s.state.Caches.GTS.Status().Store(status, func() error {
// It is safe to run this database transaction within cache.Store
// as the cache does not attempt a mutex lock until AFTER hook.
//
@ -304,21 +304,6 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) db.Er
return err
})
})
if err != nil {
return err
}
for _, id := range status.AttachmentIDs {
// Invalidate media attachments from cache.
//
// NOTE: this is needed due to the way in which
// we upload status attachments, and only after
// update them with a known status ID. This is
// not the case for header/avatar attachments.
s.state.Caches.GTS.Media().Invalidate("ID", id)
}
return nil
}
func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, columns ...string) db.Error {
@ -328,88 +313,91 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co
columns = append(columns, "updated_at")
}
if err := s.conn.RunInTx(ctx, func(tx bun.Tx) error {
// create links between this status and any emojis it uses
for _, i := range status.EmojiIDs {
if _, err := tx.
NewInsert().
Model(&gtsmodel.StatusToEmoji{
StatusID: status.ID,
EmojiID: i,
}).
On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("emoji_id")).
Exec(ctx); err != nil {
err = s.conn.ProcessError(err)
if !errors.Is(err, db.ErrAlreadyExists) {
return err
}
}
}
// create links between this status and any tags it uses
for _, i := range status.TagIDs {
if _, err := tx.
NewInsert().
Model(&gtsmodel.StatusToTag{
StatusID: status.ID,
TagID: i,
}).
On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("tag_id")).
Exec(ctx); err != nil {
err = s.conn.ProcessError(err)
if !errors.Is(err, db.ErrAlreadyExists) {
return err
}
}
}
// change the status ID of the media attachments to the new status
for _, a := range status.Attachments {
a.StatusID = status.ID
a.UpdatedAt = time.Now()
if _, err := tx.
NewUpdate().
Model(a).
Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
Exec(ctx); err != nil {
err = s.conn.ProcessError(err)
if !errors.Is(err, db.ErrAlreadyExists) {
return err
}
}
}
// Finally, update the status
_, err := tx.
NewUpdate().
Model(status).
Column(columns...).
Where("? = ?", bun.Ident("status.id"), status.ID).
Exec(ctx)
return err
}); err != nil {
// already processed
return err
}
// Invalidate status from database lookups.
s.state.Caches.GTS.Status().Invalidate("ID", status.ID)
for _, id := range status.AttachmentIDs {
// Invalidate media attachments from cache.
return s.state.Caches.GTS.Status().Store(status, func() error {
// It is safe to run this database transaction within cache.Store
// as the cache does not attempt a mutex lock until AFTER hook.
//
// NOTE: this is needed due to the way in which
// we upload status attachments, and only after
// update them with a known status ID. This is
// not the case for header/avatar attachments.
s.state.Caches.GTS.Media().Invalidate("ID", id)
}
return s.conn.RunInTx(ctx, func(tx bun.Tx) error {
// create links between this status and any emojis it uses
for _, i := range status.EmojiIDs {
if _, err := tx.
NewInsert().
Model(&gtsmodel.StatusToEmoji{
StatusID: status.ID,
EmojiID: i,
}).
On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("emoji_id")).
Exec(ctx); err != nil {
err = s.conn.ProcessError(err)
if !errors.Is(err, db.ErrAlreadyExists) {
return err
}
}
}
return nil
// create links between this status and any tags it uses
for _, i := range status.TagIDs {
if _, err := tx.
NewInsert().
Model(&gtsmodel.StatusToTag{
StatusID: status.ID,
TagID: i,
}).
On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("tag_id")).
Exec(ctx); err != nil {
err = s.conn.ProcessError(err)
if !errors.Is(err, db.ErrAlreadyExists) {
return err
}
}
}
// change the status ID of the media attachments to the new status
for _, a := range status.Attachments {
a.StatusID = status.ID
a.UpdatedAt = time.Now()
if _, err := tx.
NewUpdate().
Model(a).
Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
Exec(ctx); err != nil {
err = s.conn.ProcessError(err)
if !errors.Is(err, db.ErrAlreadyExists) {
return err
}
}
}
// Finally, update the status
_, err := tx.
NewUpdate().
Model(status).
Column(columns...).
Where("? = ?", bun.Ident("status.id"), status.ID).
Exec(ctx)
return err
})
})
}
func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) db.Error {
if err := s.conn.RunInTx(ctx, func(tx bun.Tx) error {
defer s.state.Caches.GTS.Status().Invalidate("ID", id)
// Load status into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := s.GetStatusByID(
gtscontext.SetBarebones(ctx),
id,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// NOTE: even if db.ErrNoEntries is returned, we
// still run the below transaction to ensure related
// objects are appropriately deleted.
return err
}
return s.conn.RunInTx(ctx, func(tx bun.Tx) error {
// delete links between this status and any emojis it uses
if _, err := tx.
NewDelete().
@ -438,17 +426,7 @@ func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) db.Error {
}
return nil
}); err != nil {
return err
}
// Invalidate status from database lookups.
s.state.Caches.GTS.Status().Invalidate("ID", id)
// Invalidate status from all visibility lookups.
s.state.Caches.Visibility.Invalidate("ItemID", id)
return nil
})
}
func (s *statusDB) GetStatusParents(ctx context.Context, status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) {

View file

@ -156,16 +156,26 @@ func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusF
}
func (s *statusFaveDB) DeleteStatusFaveByID(ctx context.Context, id string) db.Error {
if _, err := s.conn.
NewDelete().
Table("status_faves").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx); err != nil {
return s.conn.ProcessError(err)
defer s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
// Load fave into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := s.GetStatusFaveByID(gtscontext.SetBarebones(ctx), id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
return nil
// Finally delete fave from DB.
_, err = s.conn.NewDelete().
Table("status_faves").
Where("? = ?", bun.Ident("id"), id).
Exec(ctx)
return s.conn.ProcessError(err)
}
func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) db.Error {
@ -173,13 +183,12 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st
return errors.New("DeleteStatusFaves: one of targetAccountID or originAccountID must be set")
}
// Capture fave IDs in a RETURNING statement.
var faveIDs []string
q := s.conn.
NewDelete().
Table("status_faves").
Returning("?", bun.Ident("id"))
NewSelect().
Column("id").
Table("status_faves")
if targetAccountID != "" {
q = q.Where("? = ?", bun.Ident("target_account_id"), targetAccountID)
@ -193,12 +202,29 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st
return s.conn.ProcessError(err)
}
defer func() {
// Invalidate all IDs on return.
for _, id := range faveIDs {
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
}
}()
// Load all faves into cache, this *really* isn't great
// but it is the only way we can ensure we invalidate all
// related caches correctly (e.g. visibility).
for _, id := range faveIDs {
// Invalidate each of the returned status fave IDs.
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
_, err := s.GetStatusFaveByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
}
return nil
// Finally delete all from DB.
_, err := s.conn.NewDelete().
Table("status_faves").
Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)).
Exec(ctx)
return s.conn.ProcessError(err)
}
func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID string) db.Error {
@ -206,19 +232,35 @@ func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID
var faveIDs []string
q := s.conn.
NewDelete().
NewSelect().
Column("id").
Table("status_faves").
Where("? = ?", bun.Ident("status_id"), statusID).
Returning("?", bun.Ident("id"))
Where("? = ?", bun.Ident("status_id"), statusID)
if _, err := q.Exec(ctx, &faveIDs); err != nil {
return s.conn.ProcessError(err)
}
defer func() {
// Invalidate all IDs on return.
for _, id := range faveIDs {
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
}
}()
// Load all faves into cache, this *really* isn't great
// but it is the only way we can ensure we invalidate all
// related caches correctly (e.g. visibility).
for _, id := range faveIDs {
// Invalidate each of the returned status fave IDs.
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
_, err := s.GetStatusFaveByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
}
return nil
// Finally delete all from DB.
_, err := s.conn.NewDelete().
Table("status_faves").
Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)).
Exec(ctx)
return s.conn.ProcessError(err)
}

View file

@ -67,16 +67,12 @@ func (t *tombstoneDB) PutTombstone(ctx context.Context, tombstone *gtsmodel.Tomb
}
func (t *tombstoneDB) DeleteTombstone(ctx context.Context, id string) db.Error {
if _, err := t.conn.
NewDelete().
defer t.state.Caches.GTS.Tombstone().Invalidate("ID", id)
// Delete tombstone from DB.
_, err := t.conn.NewDelete().
TableExpr("? AS ?", bun.Ident("tombstones"), bun.Ident("tombstone")).
Where("? = ?", bun.Ident("tombstone.id"), id).
Exec(ctx); err != nil {
return t.conn.ProcessError(err)
}
// Invalidate from cache by ID
t.state.Caches.GTS.Tombstone().Invalidate("ID", id)
return nil
Exec(ctx)
return t.conn.ProcessError(err)
}

View file

@ -19,9 +19,11 @@ package bundb
import (
"context"
"errors"
"time"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/uptrace/bun"
@ -155,32 +157,36 @@ func (u *userDB) UpdateUser(ctx context.Context, user *gtsmodel.User, columns ..
columns = append(columns, "updated_at")
}
// Update the user in DB
_, err := u.conn.
NewUpdate().
Model(user).
Where("? = ?", bun.Ident("user.id"), user.ID).
Column(columns...).
Exec(ctx)
if err != nil {
return u.state.Caches.GTS.User().Store(user, func() error {
_, err := u.conn.
NewUpdate().
Model(user).
Where("? = ?", bun.Ident("user.id"), user.ID).
Column(columns...).
Exec(ctx)
return u.conn.ProcessError(err)
}
// Invalidate user from cache
u.state.Caches.GTS.User().Invalidate("ID", user.ID)
return nil
})
}
func (u *userDB) DeleteUserByID(ctx context.Context, userID string) db.Error {
if _, err := u.conn.
NewDelete().
TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")).
Where("? = ?", bun.Ident("user.id"), userID).
Exec(ctx); err != nil {
return u.conn.ProcessError(err)
defer u.state.Caches.GTS.User().Invalidate("ID", userID)
// Load user into cache before attempting a delete,
// as we need it cached in order to trigger the invalidate
// callback. This in turn invalidates others.
_, err := u.GetUserByID(gtscontext.SetBarebones(ctx), userID)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// not an issue.
err = nil
}
return err
}
// Invalidate user from cache
u.state.Caches.GTS.User().Invalidate("ID", userID)
return nil
// Finally delete user from DB.
_, err = u.conn.NewDelete().
TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")).
Where("? = ?", bun.Ident("user.id"), userID).
Exec(ctx)
return u.conn.ProcessError(err)
}