| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | // 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" | 
					
						
							| 
									
										
										
										
											2024-01-17 14:54:30 +00:00
										 |  |  | 	"slices" | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 	"sync" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-26 15:34:10 +02:00
										 |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/db" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/gtscontext" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/gtsmodel" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/log" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/workers" | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 { | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	// Map of running actions. | 
					
						
							|  |  |  | 	running map[string]*gtsmodel.AdminAction | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	// Lock for running admin actions. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// Not embedded struct, to shield | 
					
						
							|  |  |  | 	// from access by outside packages. | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 	m sync.Mutex | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// 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, | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | type ActionF func(context.Context) gtserror.MultiError | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | // 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, | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	adminAction *gtsmodel.AdminAction, | 
					
						
							|  |  |  | 	f ActionF, | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | ) gtserror.WithCode { | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	actionKey := adminAction.Key() | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// 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. | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	running, ok := a.running[actionKey] | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 	if ok { | 
					
						
							|  |  |  | 		a.m.Unlock() | 
					
						
							|  |  |  | 		return errActionConflict(running) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Action with this key not | 
					
						
							|  |  |  | 	// yet running, create it. | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	if err := a.db.PutAdminAction(ctx, adminAction); err != nil { | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 		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. | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	a.running[actionKey] = adminAction | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// UNLOCK THE MAP HERE, since | 
					
						
							|  |  |  | 	// we're done modifying it for now. | 
					
						
							|  |  |  | 	a.m.Unlock() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-26 13:50:46 +01:00
										 |  |  | 	go func() { | 
					
						
							|  |  |  | 		// Use a background context with existing values. | 
					
						
							|  |  |  | 		ctx = gtscontext.WithValues(context.Background(), ctx) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 		// Run the thing and collect errors. | 
					
						
							|  |  |  | 		if errs := f(ctx); errs != nil { | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 			adminAction.Errors = make([]string, 0, len(errs)) | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 			for _, err := range errs { | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 				adminAction.Errors = append(adminAction.Errors, err.Error()) | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Action is no longer running: | 
					
						
							|  |  |  | 		// remove from running map. | 
					
						
							|  |  |  | 		a.m.Lock() | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 		delete(a.running, actionKey) | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 		a.m.Unlock() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Mark as completed in the db, | 
					
						
							|  |  |  | 		// storing errors for later review. | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 		adminAction.CompletedAt = time.Now() | 
					
						
							|  |  |  | 		if err := a.db.UpdateAdminAction(ctx, adminAction, "completed_at", "errors"); err != nil { | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 			log.Errorf(ctx, "db error marking action %s as completed: %q", actionKey, err) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-04-26 13:50:46 +01:00
										 |  |  | 	}() | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	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. | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	running := make([]*gtsmodel.AdminAction, 0, len(a.running)) | 
					
						
							|  |  |  | 	for _, action := range a.running { | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 		running = append(running, action) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Order by ID descending (creation date). | 
					
						
							|  |  |  | 	slices.SortFunc( | 
					
						
							|  |  |  | 		running, | 
					
						
							| 
									
										
										
										
											2024-01-17 14:54:30 +00:00
										 |  |  | 		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 | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-08 11:29:40 +01:00
										 |  |  | 	return len(a.running) | 
					
						
							| 
									
										
										
										
											2023-09-04 15:55:17 +02:00
										 |  |  | } |