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 }