diff options
author | 2023-09-04 15:55:17 +0200 | |
---|---|---|
committer | 2023-09-04 14:55:17 +0100 | |
commit | 3ed1ca68e52527f74103e1a57ae48ae533508c3a (patch) | |
tree | d6113d71d6f88a3d99bbd2215ead6ca1d4fa6153 /internal/processing/admin/actions.go | |
parent | [chore]: Bump golang.org/x/image from 0.11.0 to 0.12.0 (#2178) (diff) | |
download | gotosocial-3ed1ca68e52527f74103e1a57ae48ae533508c3a.tar.xz |
[feature] Store admin actions in the db, prevent conflicting actions (#2167)
Diffstat (limited to 'internal/processing/admin/actions.go')
-rw-r--r-- | internal/processing/admin/actions.go | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/internal/processing/admin/actions.go b/internal/processing/admin/actions.go new file mode 100644 index 000000000..b85f05065 --- /dev/null +++ b/internal/processing/admin/actions.go @@ -0,0 +1,159 @@ +// 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 admin + +import ( + "context" + "sync" + "time" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/state" + "golang.org/x/exp/slices" +) + +func errActionConflict(action *gtsmodel.AdminAction) gtserror.WithCode { + err := gtserror.NewfAt( + 4, // Include caller's function name. + "an action (%s) is currently running (duration %s) which conflicts with the attempted action", + action.Key(), time.Since(action.CreatedAt), + ) + + const help = "wait until this action is complete and try again" + return gtserror.NewErrorConflict(err, err.Error(), help) +} + +type Actions struct { + r map[string]*gtsmodel.AdminAction + state *state.State + + // Not embedded struct, + // to shield from access + // by outside packages. + m sync.Mutex +} + +// Run runs the given admin action by executing the supplied function. +// +// Run handles locking, action insertion and updating, so you don't have to! +// +// If an action is already running which overlaps/conflicts with the +// given action, an ErrorWithCode 409 will be returned. +// +// If execution of the provided function returns errors, the errors +// will be updated on the provided admin action in the database. +func (a *Actions) Run( + ctx context.Context, + action *gtsmodel.AdminAction, + f func(context.Context) gtserror.MultiError, +) gtserror.WithCode { + actionKey := action.Key() + + // LOCK THE MAP HERE, since we're + // going to do some operations on it. + a.m.Lock() + + // Bail if an action with + // this key is already running. + running, ok := a.r[actionKey] + if ok { + a.m.Unlock() + return errActionConflict(running) + } + + // Action with this key not + // yet running, create it. + if err := a.state.DB.PutAdminAction(ctx, action); err != nil { + err = gtserror.Newf("db error putting admin action %s: %w", actionKey, err) + + // Don't store in map + // if there's an error. + a.m.Unlock() + return gtserror.NewErrorInternalError(err) + } + + // Action was inserted, + // store in map. + a.r[actionKey] = action + + // UNLOCK THE MAP HERE, since + // we're done modifying it for now. + a.m.Unlock() + + // Do the rest of the work asynchronously. + a.state.Workers.ClientAPI.Enqueue(func(ctx context.Context) { + // Run the thing and collect errors. + if errs := f(ctx); errs != nil { + action.Errors = make([]string, 0, len(errs)) + for _, err := range errs { + action.Errors = append(action.Errors, err.Error()) + } + } + + // Action is no longer running: + // remove from running map. + a.m.Lock() + delete(a.r, actionKey) + a.m.Unlock() + + // Mark as completed in the db, + // storing errors for later review. + action.CompletedAt = time.Now() + if err := a.state.DB.UpdateAdminAction(ctx, action, "completed_at", "errors"); err != nil { + log.Errorf(ctx, "db error marking action %s as completed: %q", actionKey, err) + } + }) + + return nil +} + +// GetRunning sounds like a threat, but it actually just +// returns all of the currently running actions held by +// the Actions struct, ordered by ID descending. +func (a *Actions) GetRunning() []*gtsmodel.AdminAction { + a.m.Lock() + defer a.m.Unlock() + + // Assemble all currently running actions. + running := make([]*gtsmodel.AdminAction, 0, len(a.r)) + for _, action := range a.r { + running = append(running, action) + } + + // Order by ID descending (creation date). + slices.SortFunc( + running, + func(a *gtsmodel.AdminAction, b *gtsmodel.AdminAction) bool { + return a.ID > b.ID + }, + ) + + return running +} + +// TotalRunning is a sequel to the classic +// 1972 environmental-themed science fiction +// film Silent Running, starring Bruce Dern. +func (a *Actions) TotalRunning() int { + a.m.Lock() + defer a.m.Unlock() + + return len(a.r) +} |