summaryrefslogtreecommitdiff
path: root/internal/processing/admin/actions.go
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-09-04 15:55:17 +0200
committerLibravatar GitHub <noreply@github.com>2023-09-04 14:55:17 +0100
commit3ed1ca68e52527f74103e1a57ae48ae533508c3a (patch)
treed6113d71d6f88a3d99bbd2215ead6ca1d4fa6153 /internal/processing/admin/actions.go
parent[chore]: Bump golang.org/x/image from 0.11.0 to 0.12.0 (#2178) (diff)
downloadgotosocial-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.go159
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)
+}