summaryrefslogtreecommitdiff
path: root/internal/admin/actions.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/actions.go')
-rw-r--r--internal/admin/actions.go190
1 files changed, 190 insertions, 0 deletions
diff --git a/internal/admin/actions.go b/internal/admin/actions.go
new file mode 100644
index 000000000..057bfe07d
--- /dev/null
+++ b/internal/admin/actions.go
@@ -0,0 +1,190 @@
+// 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"
+ "slices"
+ "sync"
+ "time"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/workers"
+)
+
+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 {
+ // Map of running actions.
+ running map[string]*gtsmodel.AdminAction
+
+ // Lock for running admin actions.
+ //
+ // Not embedded struct, to shield
+ // from access by outside packages.
+ m sync.Mutex
+
+ // DB for storing, updating,
+ // deleting admin actions etc.
+ db db.DB
+
+ // Workers for queuing
+ // admin action side effects.
+ workers *workers.Workers
+}
+
+func New(db db.DB, workers *workers.Workers) *Actions {
+ return &Actions{
+ running: make(map[string]*gtsmodel.AdminAction),
+ db: db,
+ workers: workers,
+ }
+}
+
+type ActionF func(context.Context) gtserror.MultiError
+
+// 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,
+ adminAction *gtsmodel.AdminAction,
+ f ActionF,
+) gtserror.WithCode {
+ actionKey := adminAction.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.running[actionKey]
+ if ok {
+ a.m.Unlock()
+ return errActionConflict(running)
+ }
+
+ // Action with this key not
+ // yet running, create it.
+ if err := a.db.PutAdminAction(ctx, adminAction); 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.running[actionKey] = adminAction
+
+ // UNLOCK THE MAP HERE, since
+ // we're done modifying it for now.
+ a.m.Unlock()
+
+ go func() {
+ // Use a background context with existing values.
+ ctx = gtscontext.WithValues(context.Background(), ctx)
+
+ // Run the thing and collect errors.
+ if errs := f(ctx); errs != nil {
+ adminAction.Errors = make([]string, 0, len(errs))
+ for _, err := range errs {
+ adminAction.Errors = append(adminAction.Errors, err.Error())
+ }
+ }
+
+ // Action is no longer running:
+ // remove from running map.
+ a.m.Lock()
+ delete(a.running, actionKey)
+ a.m.Unlock()
+
+ // Mark as completed in the db,
+ // storing errors for later review.
+ adminAction.CompletedAt = time.Now()
+ if err := a.db.UpdateAdminAction(ctx, adminAction, "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.running))
+ for _, action := range a.running {
+ running = append(running, action)
+ }
+
+ // Order by ID descending (creation date).
+ slices.SortFunc(
+ running,
+ func(a *gtsmodel.AdminAction, b *gtsmodel.AdminAction) int {
+ const k = -1
+ switch {
+ case a.ID > b.ID:
+ return +k
+ case a.ID < b.ID:
+ return -k
+ default:
+ return 0
+ }
+ },
+ )
+
+ 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.running)
+}