github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/synchronization/manager.go (about)

     1  package synchronization
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sort"
     8  
     9  	"github.com/mutagen-io/mutagen/pkg/filesystem"
    10  	"github.com/mutagen-io/mutagen/pkg/identifier"
    11  	"github.com/mutagen-io/mutagen/pkg/logging"
    12  	"github.com/mutagen-io/mutagen/pkg/selection"
    13  	"github.com/mutagen-io/mutagen/pkg/state"
    14  	"github.com/mutagen-io/mutagen/pkg/synchronization/core"
    15  	"github.com/mutagen-io/mutagen/pkg/url"
    16  )
    17  
    18  const (
    19  	// maximumListConflicts is the maximum number of conflicts that will be
    20  	// reported by Manager.List for a single session before conflict list
    21  	// truncation for that session.
    22  	maximumListConflicts = 10
    23  	// maximumListScanProblems is the maximum number of scan problems that will
    24  	// be reported by Manager.List for a single endpoint in a session before
    25  	// scan problem list truncation for that endpoint.
    26  	maximumListScanProblems = 10
    27  	// maximumListTransitionProblems is the maximum number of transition
    28  	// problems that will be reported by Manager.List for a single endpoint in a
    29  	// session before transition problem list truncation for that endpoint.
    30  	maximumListTransitionProblems = 10
    31  )
    32  
    33  // Manager provides synchronization session management facilities. Its methods
    34  // are safe for concurrent usage, so it can be easily exported via an RPC
    35  // interface.
    36  type Manager struct {
    37  	// logger is the underlying logger.
    38  	logger *logging.Logger
    39  	// tracker tracks changes to session states.
    40  	tracker *state.Tracker
    41  	// sessionLock locks the sessions registry.
    42  	sessionsLock *state.TrackingLock
    43  	// sessions maps sessions to their respective controllers.
    44  	sessions map[string]*controller
    45  }
    46  
    47  // NewManager creates a new Manager instance.
    48  func NewManager(logger *logging.Logger) (*Manager, error) {
    49  	// Create a tracker and corresponding lock to watch for state changes.
    50  	tracker := state.NewTracker()
    51  	sessionsLock := state.NewTrackingLock(tracker)
    52  
    53  	// Create the session registry.
    54  	sessions := make(map[string]*controller)
    55  
    56  	// Load existing sessions.
    57  	logger.Info("Looking for existing sessions")
    58  	sessionsDirectory, err := pathForSession("")
    59  	if err != nil {
    60  		return nil, fmt.Errorf("unable to compute sessions directory: %w", err)
    61  	}
    62  	sessionsDirectoryContents, err := filesystem.DirectoryContentsByPath(sessionsDirectory)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("unable to read contents of sessions directory: %w", err)
    65  	}
    66  	for _, c := range sessionsDirectoryContents {
    67  		id := c.Name()
    68  		if !identifier.IsValid(id) {
    69  			logger.Warn("Ignoring invalid session identifier:", id)
    70  			continue
    71  		}
    72  		logger.Info("Loading session", id)
    73  		if controller, err := loadSession(logger.Sublogger(identifier.Truncated(id)), tracker, id); err != nil {
    74  			logger.Warnf("Failed to load session %s: %v", id, err)
    75  			continue
    76  		} else {
    77  			sessions[id] = controller
    78  		}
    79  	}
    80  
    81  	// Success.
    82  	logger.Info("Session manager initialized")
    83  	return &Manager{
    84  		logger:       logger,
    85  		tracker:      tracker,
    86  		sessionsLock: sessionsLock,
    87  		sessions:     sessions,
    88  	}, nil
    89  }
    90  
    91  // allControllers creates a list of all controllers managed by the manager.
    92  func (m *Manager) allControllers() []*controller {
    93  	// Grab the registry lock and defer its release.
    94  	m.sessionsLock.Lock()
    95  	defer m.sessionsLock.UnlockWithoutNotify()
    96  
    97  	// Generate a list of all controllers.
    98  	controllers := make([]*controller, 0, len(m.sessions))
    99  	for _, controller := range m.sessions {
   100  		controllers = append(controllers, controller)
   101  	}
   102  
   103  	// Done.
   104  	return controllers
   105  }
   106  
   107  // findControllersBySpecification generates a list of controllers matching the
   108  // given specifications.
   109  func (m *Manager) findControllersBySpecification(specifications []string) ([]*controller, error) {
   110  	// Grab the registry lock and defer its release.
   111  	m.sessionsLock.Lock()
   112  	defer m.sessionsLock.UnlockWithoutNotify()
   113  
   114  	// Generate a list of controllers matching the specifications. We allow each
   115  	// specification to match multiple controllers, so we store matches in a set
   116  	// before converting them to a list. We do require that each specification
   117  	// match at least one controller.
   118  	controllerSet := make(map[*controller]bool)
   119  	for _, specification := range specifications {
   120  		var matched bool
   121  		for _, controller := range m.sessions {
   122  			if controller.session.Identifier == specification || controller.session.Name == specification {
   123  				controllerSet[controller] = true
   124  				matched = true
   125  			}
   126  		}
   127  		if !matched {
   128  			return nil, fmt.Errorf("specification \"%s\" did not match any sessions", specification)
   129  		}
   130  	}
   131  
   132  	// Convert the set to a list.
   133  	controllers := make([]*controller, 0, len(controllerSet))
   134  	for c := range controllerSet {
   135  		controllers = append(controllers, c)
   136  	}
   137  
   138  	// Done.
   139  	return controllers, nil
   140  }
   141  
   142  // findControllersByLabelSelector generates a list of controllers using the
   143  // specified label selector.
   144  func (m *Manager) findControllersByLabelSelector(labelSelector string) ([]*controller, error) {
   145  	// Parse the label selector.
   146  	selector, err := selection.ParseLabelSelector(labelSelector)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("unable to parse label selector: %w", err)
   149  	}
   150  
   151  	// Grab the registry lock and defer its release.
   152  	m.sessionsLock.Lock()
   153  	defer m.sessionsLock.UnlockWithoutNotify()
   154  
   155  	// Loop over controllers and look for matches.
   156  	var controllers []*controller
   157  	for _, controller := range m.sessions {
   158  		if selector.Matches(controller.session.Labels) {
   159  			controllers = append(controllers, controller)
   160  		}
   161  	}
   162  
   163  	// Done.
   164  	return controllers, nil
   165  }
   166  
   167  // selectControllers generates a list of controllers using the mechanism
   168  // specified by the provided selection.
   169  func (m *Manager) selectControllers(selection *selection.Selection) ([]*controller, error) {
   170  	// Dispatch selection based on the requested mechanism.
   171  	if selection.All {
   172  		return m.allControllers(), nil
   173  	} else if len(selection.Specifications) > 0 {
   174  		return m.findControllersBySpecification(selection.Specifications)
   175  	} else if selection.LabelSelector != "" {
   176  		return m.findControllersByLabelSelector(selection.LabelSelector)
   177  	} else {
   178  		// TODO: Should we panic here instead?
   179  		return nil, errors.New("invalid session selection")
   180  	}
   181  }
   182  
   183  // Shutdown tells the manager to gracefully halt sessions.
   184  func (m *Manager) Shutdown() {
   185  	// Log the shutdown.
   186  	m.logger.Info("Shutting down")
   187  
   188  	// Terminate state tracking to terminate monitoring.
   189  	m.tracker.Terminate()
   190  
   191  	// Grab the registry lock and defer its release.
   192  	m.sessionsLock.Lock()
   193  	defer m.sessionsLock.UnlockWithoutNotify()
   194  
   195  	// Attempt to halt each session so that it can shutdown cleanly. Ignore but
   196  	// log any that fail to halt.
   197  	for _, controller := range m.sessions {
   198  		m.logger.Info("Halting session", controller.session.Identifier)
   199  		if err := controller.halt(context.Background(), controllerHaltModeShutdown, "", false); err != nil {
   200  			m.logger.Warnf("Failed to halt session %s: %v", controller.session.Identifier, err)
   201  		}
   202  	}
   203  }
   204  
   205  // Create tells the manager to create a new session.
   206  func (m *Manager) Create(
   207  	ctx context.Context,
   208  	alpha, beta *url.URL,
   209  	configuration, configurationAlpha, configurationBeta *Configuration,
   210  	name string,
   211  	labels map[string]string,
   212  	paused bool,
   213  	prompter string,
   214  ) (string, error) {
   215  	// Create a unique session identifier.
   216  	id, err := identifier.New(identifier.PrefixSynchronization)
   217  	if err != nil {
   218  		return "", fmt.Errorf("unable to generate identifier for session: %w", err)
   219  	}
   220  
   221  	// Attempt to create a session.
   222  	controller, err := newSession(
   223  		ctx,
   224  		m.logger.Sublogger(identifier.Truncated(id)),
   225  		m.tracker,
   226  		id,
   227  		alpha, beta,
   228  		configuration, configurationAlpha, configurationBeta,
   229  		name,
   230  		labels,
   231  		paused,
   232  		prompter,
   233  	)
   234  	if err != nil {
   235  		return "", err
   236  	}
   237  
   238  	// Register the controller.
   239  	m.sessionsLock.Lock()
   240  	m.sessions[controller.session.Identifier] = controller
   241  	m.sessionsLock.Unlock()
   242  
   243  	// Done.
   244  	return controller.session.Identifier, nil
   245  }
   246  
   247  // List requests a state snapshot for the specified sessions. Session states
   248  // will be ordered by creation time, from oldest to newest. Problem and conflict
   249  // lists will sorted by path and truncated to reasonable lengths, and conflicts
   250  // will be converted to their slim variants.
   251  func (m *Manager) List(ctx context.Context, selection *selection.Selection, previousStateIndex uint64) (uint64, []*State, error) {
   252  	// Wait for a state change from the previous index.
   253  	stateIndex, err := m.tracker.WaitForChange(ctx, previousStateIndex)
   254  	if err != nil {
   255  		return 0, nil, fmt.Errorf("unable to track state changes: %w", err)
   256  	}
   257  
   258  	// Extract the controllers for the sessions of interest.
   259  	controllers, err := m.selectControllers(selection)
   260  	if err != nil {
   261  		return 0, nil, fmt.Errorf("unable to locate requested sessions: %w", err)
   262  	}
   263  
   264  	// Create a static snapshot of the state from each controller, then perform
   265  	// additional deep copying of problem and conflict lists, sort these lists
   266  	// based on path, truncate them if they're too long, and convert conflicts
   267  	// to their slim representation.
   268  	//
   269  	// HACK: We're relying a lot on understanding the internals of currentState
   270  	// and its call stack. It promises a static snapshot of the state, but we
   271  	// don't really know what that means, or that we can modify the top level of
   272  	// that snapshot, at least not from the method documentation. Since that
   273  	// code exists within this package, it's sort of acceptable abstraction
   274  	// breaking. If we could better enforce field access for Protocol Buffers
   275  	// message types, then we could probably avoid these sorts of hacks, but in
   276  	// Go it's a balance between performance, code bloat, and enforcement of
   277  	// invariants. We could push the copying that we do here into State.copy,
   278  	// but that would probably be worse because it would involve some lower
   279  	// level part of the stack knowing about the behavior of some higher level
   280  	// part of the stack (which is arguably worse than the opposite situation
   281  	// that we have here).
   282  	states := make([]*State, len(controllers))
   283  	for i, controller := range controllers {
   284  		// Create the state snapshot.
   285  		state := controller.currentState()
   286  
   287  		// Sort and (potentially) truncate conflicts, then convert them to their
   288  		// slim representations.
   289  		state.Conflicts = core.CopyConflicts(state.Conflicts)
   290  		core.SortConflicts(state.Conflicts)
   291  		if len(state.Conflicts) > maximumListConflicts {
   292  			state.ExcludedConflicts = uint64(len(state.Conflicts) - maximumListConflicts)
   293  			state.Conflicts = state.Conflicts[:maximumListConflicts]
   294  		}
   295  		for c, conflict := range state.Conflicts {
   296  			state.Conflicts[c] = conflict.Slim()
   297  		}
   298  
   299  		// Sort and (potentially) truncate alpha scan problems.
   300  		state.AlphaState.ScanProblems = core.CopyProblems(state.AlphaState.ScanProblems)
   301  		core.SortProblems(state.AlphaState.ScanProblems)
   302  		if len(state.AlphaState.ScanProblems) > maximumListScanProblems {
   303  			state.AlphaState.ExcludedScanProblems = uint64(len(state.AlphaState.ScanProblems) - maximumListScanProblems)
   304  			state.AlphaState.ScanProblems = state.AlphaState.ScanProblems[:maximumListScanProblems]
   305  		}
   306  
   307  		// Sort and (potentially) truncate alpha transition problems.
   308  		state.AlphaState.TransitionProblems = core.CopyProblems(state.AlphaState.TransitionProblems)
   309  		core.SortProblems(state.AlphaState.TransitionProblems)
   310  		if len(state.AlphaState.TransitionProblems) > maximumListTransitionProblems {
   311  			state.AlphaState.ExcludedTransitionProblems = uint64(len(state.AlphaState.TransitionProblems) - maximumListTransitionProblems)
   312  			state.AlphaState.TransitionProblems = state.AlphaState.TransitionProblems[:maximumListTransitionProblems]
   313  		}
   314  
   315  		// Sort and (potentially) truncate alpha scan problems.
   316  		state.BetaState.ScanProblems = core.CopyProblems(state.BetaState.ScanProblems)
   317  		core.SortProblems(state.BetaState.ScanProblems)
   318  		if len(state.BetaState.ScanProblems) > maximumListScanProblems {
   319  			state.BetaState.ExcludedScanProblems = uint64(len(state.BetaState.ScanProblems) - maximumListScanProblems)
   320  			state.BetaState.ScanProblems = state.BetaState.ScanProblems[:maximumListScanProblems]
   321  		}
   322  
   323  		// Sort and (potentially) truncate alpha transition problems.
   324  		state.BetaState.TransitionProblems = core.CopyProblems(state.BetaState.TransitionProblems)
   325  		core.SortProblems(state.BetaState.TransitionProblems)
   326  		if len(state.BetaState.TransitionProblems) > maximumListTransitionProblems {
   327  			state.BetaState.ExcludedTransitionProblems = uint64(len(state.BetaState.TransitionProblems) - maximumListTransitionProblems)
   328  			state.BetaState.TransitionProblems = state.BetaState.TransitionProblems[:maximumListTransitionProblems]
   329  		}
   330  
   331  		// Store the state snapshot.
   332  		states[i] = state
   333  	}
   334  
   335  	// Sort session states by session creation time.
   336  	sort.Slice(states, func(i, j int) bool {
   337  		iTime := states[i].Session.CreationTime
   338  		jTime := states[j].Session.CreationTime
   339  		return iTime.Seconds < jTime.Seconds ||
   340  			(iTime.Seconds == jTime.Seconds && iTime.Nanos < jTime.Nanos)
   341  	})
   342  
   343  	// Success.
   344  	return stateIndex, states, nil
   345  }
   346  
   347  // Flush tells the manager to flush sessions matching the given specifications.
   348  func (m *Manager) Flush(ctx context.Context, selection *selection.Selection, prompter string, skipWait bool) error {
   349  	// Extract the controllers for the sessions of interest.
   350  	controllers, err := m.selectControllers(selection)
   351  	if err != nil {
   352  		return fmt.Errorf("unable to locate requested sessions: %w", err)
   353  	}
   354  
   355  	// Attempt to flush the sessions.
   356  	for _, controller := range controllers {
   357  		if err := controller.flush(ctx, prompter, skipWait); err != nil {
   358  			return fmt.Errorf("unable to flush session: %w", err)
   359  		}
   360  	}
   361  
   362  	// Success.
   363  	return nil
   364  }
   365  
   366  // Pause tells the manager to pause sessions matching the given specifications.
   367  func (m *Manager) Pause(ctx context.Context, selection *selection.Selection, prompter string) error {
   368  	// Extract the controllers for the sessions of interest.
   369  	controllers, err := m.selectControllers(selection)
   370  	if err != nil {
   371  		return fmt.Errorf("unable to locate requested sessions: %w", err)
   372  	}
   373  
   374  	// Attempt to pause the sessions.
   375  	for _, controller := range controllers {
   376  		if err := controller.halt(ctx, controllerHaltModePause, prompter, false); err != nil {
   377  			return fmt.Errorf("unable to pause session: %w", err)
   378  		}
   379  	}
   380  
   381  	// Success.
   382  	return nil
   383  }
   384  
   385  // Resume tells the manager to resume sessions matching the given
   386  // specifications.
   387  func (m *Manager) Resume(ctx context.Context, selection *selection.Selection, prompter string) error {
   388  	// Extract the controllers for the sessions of interest.
   389  	controllers, err := m.selectControllers(selection)
   390  	if err != nil {
   391  		return fmt.Errorf("unable to locate requested sessions: %w", err)
   392  	}
   393  
   394  	// Attempt to resume.
   395  	for _, controller := range controllers {
   396  		if err := controller.resume(ctx, prompter, false); err != nil {
   397  			return fmt.Errorf("unable to resume session: %w", err)
   398  		}
   399  	}
   400  
   401  	// Success.
   402  	return nil
   403  }
   404  
   405  // Reset tells the manager to reset session histories for sessions matching the
   406  // given specifications.
   407  func (m *Manager) Reset(ctx context.Context, selection *selection.Selection, prompter string) error {
   408  	// Extract the controllers for the sessions of interest.
   409  	controllers, err := m.selectControllers(selection)
   410  	if err != nil {
   411  		return fmt.Errorf("unable to locate requested sessions: %w", err)
   412  	}
   413  
   414  	// Attempt to reset.
   415  	for _, controller := range controllers {
   416  		if err := controller.reset(ctx, prompter); err != nil {
   417  			return fmt.Errorf("unable to reset session: %w", err)
   418  		}
   419  	}
   420  
   421  	// Success.
   422  	return nil
   423  }
   424  
   425  // Terminate tells the manager to terminate sessions matching the given
   426  // specifications.
   427  func (m *Manager) Terminate(ctx context.Context, selection *selection.Selection, prompter string) error {
   428  	// Extract the controllers for the sessions of interest.
   429  	controllers, err := m.selectControllers(selection)
   430  	if err != nil {
   431  		return fmt.Errorf("unable to locate requested sessions: %w", err)
   432  	}
   433  
   434  	// Attempt to terminate the sessions. Since we're terminating them, we're
   435  	// responsible for removing them from the session map.
   436  	for _, controller := range controllers {
   437  		if err := controller.halt(ctx, controllerHaltModeTerminate, prompter, false); err != nil {
   438  			return fmt.Errorf("unable to terminate session: %w", err)
   439  		}
   440  		m.sessionsLock.Lock()
   441  		delete(m.sessions, controller.session.Identifier)
   442  		m.sessionsLock.Unlock()
   443  	}
   444  
   445  	// Success.
   446  	return nil
   447  }