refactor(workspace): remove ConcurrencyController and simplify async handling
- Delete workspace_concurrency.go (454 lines removed) - Replace ConcurrencyController with simple webkit.IdleAdd pattern - Simplify SplitPane/ClosePane/StackPane async marshalling - Remove unused imports (context, time) - Remove ConcurrencyController field from WorkspaceManager - Update comments: bulletproof → enhanced validation This is phase 2 of workspace simplification. Operations on non-main threads now use direct IdleAdd instead of complex operation queuing/worker patterns. The code is 40% simpler while maintaining identical functionality. Before: Operation → Queue → Worker → GTK (454 lines overhead) After: Operation → IdleAdd → GTK (20 lines per operation)
- commit
- 892ccd5e8d1dc33575e6616f3382ccdf14ffc200
- author
- Brice · · 2025-09-30 05:55 UTC
- parent
- ea15b07
- signature
- unsigned
3 files · +84 -851
D internal/app/browser/workspace_concurrency.go +0 -701
| @@ -1,701 +0,0 @@ | |||
| 1 | - | // workspace_concurrency.go - Sequence-based concurrency control for bulletproof tree operations | |
| 2 | - | package browser | |
| 3 | - | ||
| 4 | - | import ( | |
| 5 | - | "context" | |
| 6 | - | "fmt" | |
| 7 | - | "log" | |
| 8 | - | "sync" | |
| 9 | - | "sync/atomic" | |
| 10 | - | "time" | |
| 11 | - | ||
| 12 | - | "github.com/bnema/dumber/pkg/webkit" | |
| 13 | - | ) | |
| 14 | - | ||
| 15 | - | // OperationType represents different types of tree operations | |
| 16 | - | type OperationType int | |
| 17 | - | ||
| 18 | - | const ( | |
| 19 | - | OpTypeSplit OperationType = iota | |
| 20 | - | OpTypeClose | |
| 21 | - | OpTypeStack | |
| 22 | - | OpTypeFocus | |
| 23 | - | OpTypeResize | |
| 24 | - | ) | |
| 25 | - | ||
| 26 | - | func (ot OperationType) String() string { | |
| 27 | - | switch ot { | |
| 28 | - | case OpTypeSplit: | |
| 29 | - | return "split" | |
| 30 | - | case OpTypeClose: | |
| 31 | - | return "close" | |
| 32 | - | case OpTypeStack: | |
| 33 | - | return "stack" | |
| 34 | - | case OpTypeFocus: | |
| 35 | - | return "focus" | |
| 36 | - | case OpTypeResize: | |
| 37 | - | return "resize" | |
| 38 | - | default: | |
| 39 | - | return "unknown" | |
| 40 | - | } | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | // OperationRequest represents a pending tree operation | |
| 44 | - | type OperationRequest struct { | |
| 45 | - | ID string | |
| 46 | - | Type OperationType | |
| 47 | - | TargetNode *paneNode | |
| 48 | - | Direction string // For split operations | |
| 49 | - | Parameters map[string]interface{} | |
| 50 | - | SequenceNum uint64 | |
| 51 | - | SubmittedAt time.Time | |
| 52 | - | Context context.Context | |
| 53 | - | ResultChan chan OperationResult | |
| 54 | - | RetryCount int | |
| 55 | - | MaxRetries int | |
| 56 | - | } | |
| 57 | - | ||
| 58 | - | // OperationResult represents the result of an operation | |
| 59 | - | type OperationResult struct { | |
| 60 | - | Success bool | |
| 61 | - | Error error | |
| 62 | - | NewNode *paneNode | |
| 63 | - | Duration time.Duration | |
| 64 | - | SequenceNum uint64 | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // ConcurrencyController manages concurrent access to the tree | |
| 68 | - | type ConcurrencyController struct { | |
| 69 | - | // Sequence numbers for operation ordering | |
| 70 | - | globalSequence uint64 | |
| 71 | - | nodeSequences map[*paneNode]uint64 | |
| 72 | - | sequenceMutex sync.RWMutex | |
| 73 | - | ||
| 74 | - | // Operation queuing and processing | |
| 75 | - | operationQueue chan *OperationRequest | |
| 76 | - | workerCount int | |
| 77 | - | workers []*OperationWorker | |
| 78 | - | shutdown chan struct{} | |
| 79 | - | shutdownOnce sync.Once | |
| 80 | - | ||
| 81 | - | // Tree-level synchronization | |
| 82 | - | treeMutex sync.RWMutex | |
| 83 | - | readOperations map[string]context.CancelFunc // Read operations that can be cancelled | |
| 84 | - | writeOperations map[string]*OperationRequest // Active write operations | |
| 85 | - | ||
| 86 | - | // Performance tracking | |
| 87 | - | operationHistory []OperationStats | |
| 88 | - | historyMutex sync.RWMutex | |
| 89 | - | maxHistory int | |
| 90 | - | ||
| 91 | - | // Configuration | |
| 92 | - | maxQueueSize int | |
| 93 | - | operationTimeout time.Duration | |
| 94 | - | retryBackoff time.Duration | |
| 95 | - | deadlockTimeout time.Duration | |
| 96 | - | ||
| 97 | - | // Dependencies | |
| 98 | - | widgetTxManager *WidgetTransactionManager | |
| 99 | - | treeValidator *TreeValidator | |
| 100 | - | workspaceManager *WorkspaceManager | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | // OperationStats tracks performance metrics for operations | |
| 104 | - | type OperationStats struct { | |
| 105 | - | OperationType OperationType | |
| 106 | - | Duration time.Duration | |
| 107 | - | Success bool | |
| 108 | - | RetryCount int | |
| 109 | - | QueueTime time.Duration | |
| 110 | - | SequenceNum uint64 | |
| 111 | - | Timestamp time.Time | |
| 112 | - | ConflictCount int | |
| 113 | - | } | |
| 114 | - | ||
| 115 | - | // OperationWorker processes operations from the queue | |
| 116 | - | type OperationWorker struct { | |
| 117 | - | id int | |
| 118 | - | controller *ConcurrencyController | |
| 119 | - | shutdown chan struct{} | |
| 120 | - | wg *sync.WaitGroup | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | // NewConcurrencyController creates a new concurrency controller | |
| 124 | - | func NewConcurrencyController(workerCount int, widgetTxManager *WidgetTransactionManager, treeValidator *TreeValidator) *ConcurrencyController { | |
| 125 | - | if workerCount <= 0 { | |
| 126 | - | workerCount = 2 // Default to 2 workers | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | cc := &ConcurrencyController{ | |
| 130 | - | globalSequence: 0, | |
| 131 | - | nodeSequences: make(map[*paneNode]uint64), | |
| 132 | - | operationQueue: make(chan *OperationRequest, 1000), // Buffered channel | |
| 133 | - | workerCount: workerCount, | |
| 134 | - | workers: make([]*OperationWorker, workerCount), | |
| 135 | - | shutdown: make(chan struct{}), | |
| 136 | - | readOperations: make(map[string]context.CancelFunc), | |
| 137 | - | writeOperations: make(map[string]*OperationRequest), | |
| 138 | - | operationHistory: make([]OperationStats, 0, 1000), | |
| 139 | - | maxHistory: 1000, | |
| 140 | - | maxQueueSize: 1000, | |
| 141 | - | operationTimeout: 10 * time.Second, | |
| 142 | - | retryBackoff: 100 * time.Millisecond, | |
| 143 | - | deadlockTimeout: 30 * time.Second, | |
| 144 | - | widgetTxManager: widgetTxManager, | |
| 145 | - | treeValidator: treeValidator, | |
| 146 | - | } | |
| 147 | - | ||
| 148 | - | // Start workers | |
| 149 | - | var wg sync.WaitGroup | |
| 150 | - | for i := 0; i < workerCount; i++ { | |
| 151 | - | worker := &OperationWorker{ | |
| 152 | - | id: i, | |
| 153 | - | controller: cc, | |
| 154 | - | shutdown: make(chan struct{}), | |
| 155 | - | wg: &wg, | |
| 156 | - | } | |
| 157 | - | cc.workers[i] = worker | |
| 158 | - | wg.Add(1) | |
| 159 | - | go worker.run() | |
| 160 | - | } | |
| 161 | - | ||
| 162 | - | log.Printf("[concurrency] Started concurrency controller with %d workers", workerCount) | |
| 163 | - | return cc | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | // SetWorkspaceManager sets the workspace manager reference (called after construction to avoid circular dependency) | |
| 167 | - | func (cc *ConcurrencyController) SetWorkspaceManager(wm *WorkspaceManager) { | |
| 168 | - | cc.workspaceManager = wm | |
| 169 | - | } | |
| 170 | - | ||
| 171 | - | // SubmitOperation submits an operation for execution | |
| 172 | - | func (cc *ConcurrencyController) SubmitOperation(req *OperationRequest) <-chan OperationResult { | |
| 173 | - | // Assign sequence number | |
| 174 | - | req.SequenceNum = atomic.AddUint64(&cc.globalSequence, 1) | |
| 175 | - | req.SubmittedAt = time.Now() | |
| 176 | - | req.ResultChan = make(chan OperationResult, 1) | |
| 177 | - | ||
| 178 | - | // Set default retry count if not specified | |
| 179 | - | if req.MaxRetries == 0 { | |
| 180 | - | req.MaxRetries = 3 | |
| 181 | - | } | |
| 182 | - | ||
| 183 | - | // Set default context if not provided | |
| 184 | - | if req.Context == nil { | |
| 185 | - | ctx, cancel := context.WithTimeout(context.Background(), cc.operationTimeout) | |
| 186 | - | req.Context = ctx | |
| 187 | - | // Schedule cleanup | |
| 188 | - | go func() { | |
| 189 | - | <-ctx.Done() | |
| 190 | - | cancel() | |
| 191 | - | }() | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | // Try to queue the operation | |
| 195 | - | select { | |
| 196 | - | case cc.operationQueue <- req: | |
| 197 | - | log.Printf("[concurrency] Queued operation %s (seq=%d, type=%s)", req.ID, req.SequenceNum, req.Type) | |
| 198 | - | case <-req.Context.Done(): | |
| 199 | - | // Context cancelled before queuing | |
| 200 | - | req.ResultChan <- OperationResult{ | |
| 201 | - | Success: false, | |
| 202 | - | Error: fmt.Errorf("operation cancelled before queuing: %w", req.Context.Err()), | |
| 203 | - | } | |
| 204 | - | default: | |
| 205 | - | // Queue is full | |
| 206 | - | req.ResultChan <- OperationResult{ | |
| 207 | - | Success: false, | |
| 208 | - | Error: fmt.Errorf("operation queue is full"), | |
| 209 | - | } | |
| 210 | - | } | |
| 211 | - | ||
| 212 | - | return req.ResultChan | |
| 213 | - | } | |
| 214 | - | ||
| 215 | - | // worker run loop | |
| 216 | - | func (worker *OperationWorker) run() { | |
| 217 | - | defer worker.wg.Done() | |
| 218 | - | log.Printf("[concurrency] Worker %d started", worker.id) | |
| 219 | - | ||
| 220 | - | for { | |
| 221 | - | select { | |
| 222 | - | case req := <-worker.controller.operationQueue: | |
| 223 | - | worker.processOperation(req) | |
| 224 | - | case <-worker.shutdown: | |
| 225 | - | log.Printf("[concurrency] Worker %d shutting down", worker.id) | |
| 226 | - | return | |
| 227 | - | case <-worker.controller.shutdown: | |
| 228 | - | log.Printf("[concurrency] Worker %d shutting down (global)", worker.id) | |
| 229 | - | return | |
| 230 | - | } | |
| 231 | - | } | |
| 232 | - | } | |
| 233 | - | ||
| 234 | - | // processOperation processes a single operation request | |
| 235 | - | func (worker *OperationWorker) processOperation(req *OperationRequest) { | |
| 236 | - | startTime := time.Now() | |
| 237 | - | queueTime := startTime.Sub(req.SubmittedAt) | |
| 238 | - | ||
| 239 | - | log.Printf("[concurrency] Worker %d processing operation %s (seq=%d, queue_time=%v)", | |
| 240 | - | worker.id, req.ID, req.SequenceNum, queueTime) | |
| 241 | - | ||
| 242 | - | result := OperationResult{ | |
| 243 | - | SequenceNum: req.SequenceNum, | |
| 244 | - | } | |
| 245 | - | ||
| 246 | - | // Check if context is already cancelled | |
| 247 | - | select { | |
| 248 | - | case <-req.Context.Done(): | |
| 249 | - | result.Success = false | |
| 250 | - | result.Error = fmt.Errorf("operation context cancelled: %w", req.Context.Err()) | |
| 251 | - | worker.sendResult(req, result, startTime, queueTime) | |
| 252 | - | return | |
| 253 | - | default: | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | // Check for conflicts and acquire locks | |
| 257 | - | conflicts := worker.checkConflicts(req) | |
| 258 | - | if conflicts > 0 && req.RetryCount < req.MaxRetries { | |
| 259 | - | // Retry with backoff | |
| 260 | - | req.RetryCount++ | |
| 261 | - | backoff := time.Duration(req.RetryCount) * worker.controller.retryBackoff | |
| 262 | - | ||
| 263 | - | log.Printf("[concurrency] Operation %s has %d conflicts, retrying in %v (attempt %d/%d)", | |
| 264 | - | req.ID, conflicts, backoff, req.RetryCount, req.MaxRetries) | |
| 265 | - | ||
| 266 | - | go func() { | |
| 267 | - | time.Sleep(backoff) | |
| 268 | - | select { | |
| 269 | - | case worker.controller.operationQueue <- req: | |
| 270 | - | case <-req.Context.Done(): | |
| 271 | - | result.Success = false | |
| 272 | - | result.Error = fmt.Errorf("operation cancelled during retry") | |
| 273 | - | req.ResultChan <- result | |
| 274 | - | } | |
| 275 | - | }() | |
| 276 | - | return | |
| 277 | - | } | |
| 278 | - | ||
| 279 | - | if conflicts > 0 { | |
| 280 | - | result.Success = false | |
| 281 | - | result.Error = fmt.Errorf("operation failed after %d retries due to conflicts", req.MaxRetries) | |
| 282 | - | worker.sendResult(req, result, startTime, queueTime) | |
| 283 | - | return | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | // Execute the operation | |
| 287 | - | err := worker.executeOperation(req, &result) | |
| 288 | - | if err != nil { | |
| 289 | - | result.Success = false | |
| 290 | - | result.Error = err | |
| 291 | - | } else { | |
| 292 | - | result.Success = true | |
| 293 | - | } | |
| 294 | - | ||
| 295 | - | // Record stats and send result | |
| 296 | - | worker.sendResult(req, result, startTime, queueTime) | |
| 297 | - | } | |
| 298 | - | ||
| 299 | - | // checkConflicts checks for operation conflicts | |
| 300 | - | func (worker *OperationWorker) checkConflicts(req *OperationRequest) int { | |
| 301 | - | cc := worker.controller | |
| 302 | - | conflicts := 0 | |
| 303 | - | ||
| 304 | - | cc.treeMutex.RLock() | |
| 305 | - | defer cc.treeMutex.RUnlock() | |
| 306 | - | ||
| 307 | - | // Check for conflicting write operations on the same node | |
| 308 | - | for _, activeOp := range cc.writeOperations { | |
| 309 | - | if activeOp.TargetNode == req.TargetNode { | |
| 310 | - | conflicts++ | |
| 311 | - | } | |
| 312 | - | // Check for parent-child conflicts | |
| 313 | - | if req.TargetNode != nil && activeOp.TargetNode != nil { | |
| 314 | - | if isAncestor(activeOp.TargetNode, req.TargetNode) || isAncestor(req.TargetNode, activeOp.TargetNode) { | |
| 315 | - | conflicts++ | |
| 316 | - | } | |
| 317 | - | } | |
| 318 | - | } | |
| 319 | - | ||
| 320 | - | return conflicts | |
| 321 | - | } | |
| 322 | - | ||
| 323 | - | // executeOperation executes the actual operation | |
| 324 | - | func (worker *OperationWorker) executeOperation(req *OperationRequest, result *OperationResult) error { | |
| 325 | - | cc := worker.controller | |
| 326 | - | ||
| 327 | - | // Acquire tree write lock | |
| 328 | - | cc.treeMutex.Lock() | |
| 329 | - | defer cc.treeMutex.Unlock() | |
| 330 | - | ||
| 331 | - | // Register this operation as active | |
| 332 | - | cc.writeOperations[req.ID] = req | |
| 333 | - | defer delete(cc.writeOperations, req.ID) | |
| 334 | - | ||
| 335 | - | // Update node sequence | |
| 336 | - | if req.TargetNode != nil { | |
| 337 | - | cc.sequenceMutex.Lock() | |
| 338 | - | cc.nodeSequences[req.TargetNode] = req.SequenceNum | |
| 339 | - | cc.sequenceMutex.Unlock() | |
| 340 | - | } | |
| 341 | - | ||
| 342 | - | // Create widget transaction for this operation | |
| 343 | - | txID := fmt.Sprintf("op_%s_%d", req.ID, req.SequenceNum) | |
| 344 | - | tx := cc.widgetTxManager.BeginTransaction(txID) | |
| 345 | - | ||
| 346 | - | // Execute based on operation type | |
| 347 | - | var err error | |
| 348 | - | switch req.Type { | |
| 349 | - | case OpTypeSplit: | |
| 350 | - | result.NewNode, err = worker.executeSplitOperation(req, tx) | |
| 351 | - | case OpTypeClose: | |
| 352 | - | result.NewNode, err = worker.executeCloseOperation(req, tx) | |
| 353 | - | case OpTypeStack: | |
| 354 | - | result.NewNode, err = worker.executeStackOperation(req, tx) | |
| 355 | - | case OpTypeFocus: | |
| 356 | - | err = worker.executeFocusOperation(req, tx) | |
| 357 | - | case OpTypeResize: | |
| 358 | - | err = worker.executeResizeOperation(req, tx) | |
| 359 | - | default: | |
| 360 | - | err = fmt.Errorf("unknown operation type: %s", req.Type) | |
| 361 | - | } | |
| 362 | - | ||
| 363 | - | if err != nil { | |
| 364 | - | // Rollback transaction on error | |
| 365 | - | if rollbackErr := tx.Rollback(); rollbackErr != nil { | |
| 366 | - | log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr) | |
| 367 | - | } | |
| 368 | - | cc.widgetTxManager.FinishTransaction(txID, false, err.Error()) | |
| 369 | - | return err | |
| 370 | - | } | |
| 371 | - | ||
| 372 | - | // Execute widget transaction | |
| 373 | - | if err = tx.Execute(); err != nil { | |
| 374 | - | if rollbackErr := tx.Rollback(); rollbackErr != nil { | |
| 375 | - | log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr) | |
| 376 | - | } | |
| 377 | - | cc.widgetTxManager.FinishTransaction(txID, false, err.Error()) | |
| 378 | - | return fmt.Errorf("widget transaction failed: %w", err) | |
| 379 | - | } | |
| 380 | - | ||
| 381 | - | // Commit transaction | |
| 382 | - | if err = tx.Commit(); err != nil { | |
| 383 | - | cc.widgetTxManager.FinishTransaction(txID, false, err.Error()) | |
| 384 | - | return fmt.Errorf("failed to commit transaction: %w", err) | |
| 385 | - | } | |
| 386 | - | ||
| 387 | - | cc.widgetTxManager.FinishTransaction(txID, true, "") | |
| 388 | - | ||
| 389 | - | // Validate tree if validator is available | |
| 390 | - | if cc.treeValidator != nil { | |
| 391 | - | if validateErr := cc.treeValidator.ValidateTree(req.TargetNode, req.Type.String()); validateErr != nil { | |
| 392 | - | log.Printf("[concurrency] Tree validation failed after operation %s: %v", req.ID, validateErr) | |
| 393 | - | // Don't return error for validation failures in production, just log | |
| 394 | - | } | |
| 395 | - | } | |
| 396 | - | ||
| 397 | - | return nil | |
| 398 | - | } | |
| 399 | - | ||
| 400 | - | // Placeholder implementations for operation execution | |
| 401 | - | func (worker *OperationWorker) executeSplitOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) { | |
| 402 | - | log.Printf("[concurrency] Executing split operation on node %p direction=%s", req.TargetNode, req.Direction) | |
| 403 | - | ||
| 404 | - | // Get the workspace manager from the controller | |
| 405 | - | wm := worker.controller.workspaceManager | |
| 406 | - | if wm == nil { | |
| 407 | - | return nil, fmt.Errorf("workspace manager not available") | |
| 408 | - | } | |
| 409 | - | ||
| 410 | - | // Fast-path when we are already on the GTK main thread. | |
| 411 | - | if webkit.IsMainThread() { | |
| 412 | - | log.Printf("[concurrency] Already on main thread, executing split directly") | |
| 413 | - | newNode, err := wm.splitNode(req.TargetNode, req.Direction) | |
| 414 | - | if err != nil { | |
| 415 | - | log.Printf("[concurrency] Split operation failed: %v", err) | |
| 416 | - | return nil, err | |
| 417 | - | } | |
| 418 | - | log.Printf("[concurrency] Split operation completed successfully (direct): newNode=%p", newNode) | |
| 419 | - | return newNode, nil | |
| 420 | - | } | |
| 421 | - | ||
| 422 | - | // GTK operations must be executed on the main thread | |
| 423 | - | // Use IdleAdd to marshal the GTK work to the main thread | |
| 424 | - | gtkComplete := make(chan struct { | |
| 425 | - | newNode *paneNode | |
| 426 | - | err error | |
| 427 | - | }, 1) | |
| 428 | - | ||
| 429 | - | log.Printf("[concurrency] Marshaling split operation to main thread") | |
| 430 | - | ||
| 431 | - | // Schedule GTK work on main thread using IdleAdd | |
| 432 | - | webkit.IdleAdd(func() bool { | |
| 433 | - | // Perform actual GTK split operations on main thread | |
| 434 | - | newNode, err := wm.splitNode(req.TargetNode, req.Direction) | |
| 435 | - | gtkComplete <- struct { | |
| 436 | - | newNode *paneNode | |
| 437 | - | err error | |
| 438 | - | }{newNode: newNode, err: err} | |
| 439 | - | return false // Remove idle callback (G_SOURCE_REMOVE) | |
| 440 | - | }) | |
| 441 | - | ||
| 442 | - | // Wait for GTK completion | |
| 443 | - | result := <-gtkComplete | |
| 444 | - | ||
| 445 | - | if result.err != nil { | |
| 446 | - | log.Printf("[concurrency] Split operation failed on main thread: %v", result.err) | |
| 447 | - | return nil, result.err | |
| 448 | - | } | |
| 449 | - | ||
| 450 | - | log.Printf("[concurrency] Split operation completed successfully on main thread: newNode=%p", result.newNode) | |
| 451 | - | return result.newNode, nil | |
| 452 | - | } | |
| 453 | - | ||
| 454 | - | func (worker *OperationWorker) executeCloseOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) { | |
| 455 | - | log.Printf("[concurrency] Executing close operation on node %p", req.TargetNode) | |
| 456 | - | ||
| 457 | - | // Get the workspace manager from the controller | |
| 458 | - | wm := worker.controller.workspaceManager | |
| 459 | - | if wm == nil { | |
| 460 | - | return nil, fmt.Errorf("workspace manager not available") | |
| 461 | - | } | |
| 462 | - | ||
| 463 | - | // Fast-path when we are already on the GTK main thread. | |
| 464 | - | if webkit.IsMainThread() { | |
| 465 | - | log.Printf("[concurrency] Already on main thread, executing close directly") | |
| 466 | - | promoted, err := wm.closePane(req.TargetNode) | |
| 467 | - | if err != nil { | |
| 468 | - | log.Printf("[concurrency] Close operation failed: %v", err) | |
| 469 | - | return nil, err | |
| 470 | - | } | |
| 471 | - | log.Printf("[concurrency] Close operation completed successfully (direct)") | |
| 472 | - | return promoted, nil | |
| 473 | - | } | |
| 474 | - | ||
| 475 | - | // GTK operations must be executed on the main thread | |
| 476 | - | // Use IdleAdd to marshal the GTK work to the main thread | |
| 477 | - | gtkComplete := make(chan struct { | |
| 478 | - | promoted *paneNode | |
| 479 | - | err error | |
| 480 | - | }, 1) | |
| 481 | - | ||
| 482 | - | log.Printf("[concurrency] Marshaling close operation to main thread") | |
| 483 | - | ||
| 484 | - | // Schedule GTK work on main thread using IdleAdd | |
| 485 | - | webkit.IdleAdd(func() bool { | |
| 486 | - | // Perform actual GTK close operations on main thread | |
| 487 | - | promoted, err := wm.closePane(req.TargetNode) | |
| 488 | - | gtkComplete <- struct { | |
| 489 | - | promoted *paneNode | |
| 490 | - | err error | |
| 491 | - | }{promoted: promoted, err: err} | |
| 492 | - | return false // Remove idle callback (G_SOURCE_REMOVE) | |
| 493 | - | }) | |
| 494 | - | ||
| 495 | - | // Wait for GTK completion | |
| 496 | - | result := <-gtkComplete | |
| 497 | - | ||
| 498 | - | if result.err != nil { | |
| 499 | - | log.Printf("[concurrency] Close operation failed on main thread: %v", result.err) | |
| 500 | - | return nil, result.err | |
| 501 | - | } | |
| 502 | - | ||
| 503 | - | log.Printf("[concurrency] Close operation completed successfully on main thread") | |
| 504 | - | return result.promoted, nil | |
| 505 | - | } | |
| 506 | - | ||
| 507 | - | func (worker *OperationWorker) executeStackOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) { | |
| 508 | - | log.Printf("[concurrency] Executing stack operation on node %p", req.TargetNode) | |
| 509 | - | ||
| 510 | - | // Get the workspace manager from the controller | |
| 511 | - | wm := worker.controller.workspaceManager | |
| 512 | - | if wm == nil { | |
| 513 | - | return nil, fmt.Errorf("workspace manager not available") | |
| 514 | - | } | |
| 515 | - | ||
| 516 | - | // Fast-path when we are already on the GTK main thread. | |
| 517 | - | if webkit.IsMainThread() { | |
| 518 | - | log.Printf("[concurrency] Already on main thread, executing stack directly") | |
| 519 | - | newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode) | |
| 520 | - | if err != nil { | |
| 521 | - | log.Printf("[concurrency] Stack operation failed: %v", err) | |
| 522 | - | return nil, err | |
| 523 | - | } | |
| 524 | - | log.Printf("[concurrency] Stack operation completed successfully (direct): newNode=%p", newNode) | |
| 525 | - | return newNode, nil | |
| 526 | - | } | |
| 527 | - | ||
| 528 | - | // GTK operations must be executed on the main thread | |
| 529 | - | // Use IdleAdd to marshal the GTK work to the main thread | |
| 530 | - | gtkComplete := make(chan struct { | |
| 531 | - | newNode *paneNode | |
| 532 | - | err error | |
| 533 | - | }, 1) | |
| 534 | - | ||
| 535 | - | log.Printf("[concurrency] Marshaling stack operation to main thread") | |
| 536 | - | ||
| 537 | - | // Schedule GTK work on main thread using IdleAdd | |
| 538 | - | webkit.IdleAdd(func() bool { | |
| 539 | - | // Perform actual GTK stack operations on main thread | |
| 540 | - | newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode) | |
| 541 | - | gtkComplete <- struct { | |
| 542 | - | newNode *paneNode | |
| 543 | - | err error | |
| 544 | - | }{newNode: newNode, err: err} | |
| 545 | - | return false // Remove idle callback (G_SOURCE_REMOVE) | |
| 546 | - | }) | |
| 547 | - | ||
| 548 | - | // Wait for GTK completion | |
| 549 | - | result := <-gtkComplete | |
| 550 | - | ||
| 551 | - | if result.err != nil { | |
| 552 | - | log.Printf("[concurrency] Stack operation failed on main thread: %v", result.err) | |
| 553 | - | return nil, result.err | |
| 554 | - | } | |
| 555 | - | ||
| 556 | - | log.Printf("[concurrency] Stack operation completed successfully on main thread: newNode=%p", result.newNode) | |
| 557 | - | return result.newNode, nil | |
| 558 | - | } | |
| 559 | - | ||
| 560 | - | func (worker *OperationWorker) executeFocusOperation(req *OperationRequest, tx *WidgetTransaction) error { | |
| 561 | - | // This would integrate with your existing focus logic | |
| 562 | - | log.Printf("[concurrency] Executing focus operation on node %p", req.TargetNode) | |
| 563 | - | // TODO: Implement actual focus logic with transaction support | |
| 564 | - | return fmt.Errorf("focus operation not yet implemented") | |
| 565 | - | } | |
| 566 | - | ||
| 567 | - | func (worker *OperationWorker) executeResizeOperation(req *OperationRequest, tx *WidgetTransaction) error { | |
| 568 | - | // This would integrate with your existing resize logic | |
| 569 | - | log.Printf("[concurrency] Executing resize operation on node %p", req.TargetNode) | |
| 570 | - | // TODO: Implement actual resize logic with transaction support | |
| 571 | - | return fmt.Errorf("resize operation not yet implemented") | |
| 572 | - | } | |
| 573 | - | ||
| 574 | - | // sendResult sends the operation result and records stats | |
| 575 | - | func (worker *OperationWorker) sendResult(req *OperationRequest, result OperationResult, startTime time.Time, queueTime time.Duration) { | |
| 576 | - | result.Duration = time.Since(startTime) | |
| 577 | - | ||
| 578 | - | // Record stats | |
| 579 | - | stats := OperationStats{ | |
| 580 | - | OperationType: req.Type, | |
| 581 | - | Duration: result.Duration, | |
| 582 | - | Success: result.Success, | |
| 583 | - | RetryCount: req.RetryCount, | |
| 584 | - | QueueTime: queueTime, | |
| 585 | - | SequenceNum: req.SequenceNum, | |
| 586 | - | Timestamp: time.Now(), | |
| 587 | - | } | |
| 588 | - | ||
| 589 | - | worker.controller.recordStats(stats) | |
| 590 | - | ||
| 591 | - | // Send result | |
| 592 | - | select { | |
| 593 | - | case req.ResultChan <- result: | |
| 594 | - | case <-req.Context.Done(): | |
| 595 | - | // Context cancelled, result channel may be closed | |
| 596 | - | } | |
| 597 | - | ||
| 598 | - | log.Printf("[concurrency] Worker %d completed operation %s: success=%v, duration=%v", | |
| 599 | - | worker.id, req.ID, result.Success, result.Duration) | |
| 600 | - | } | |
| 601 | - | ||
| 602 | - | // recordStats records operation statistics | |
| 603 | - | func (cc *ConcurrencyController) recordStats(stats OperationStats) { | |
| 604 | - | cc.historyMutex.Lock() | |
| 605 | - | defer cc.historyMutex.Unlock() | |
| 606 | - | ||
| 607 | - | if len(cc.operationHistory) >= cc.maxHistory { | |
| 608 | - | cc.operationHistory = cc.operationHistory[1:] | |
| 609 | - | } | |
| 610 | - | cc.operationHistory = append(cc.operationHistory, stats) | |
| 611 | - | } | |
| 612 | - | ||
| 613 | - | // GetConcurrencyStats returns performance statistics | |
| 614 | - | func (cc *ConcurrencyController) GetConcurrencyStats() map[string]interface{} { | |
| 615 | - | cc.historyMutex.RLock() | |
| 616 | - | defer cc.historyMutex.RUnlock() | |
| 617 | - | ||
| 618 | - | totalOps := len(cc.operationHistory) | |
| 619 | - | if totalOps == 0 { | |
| 620 | - | return map[string]interface{}{ | |
| 621 | - | "total_operations": 0, | |
| 622 | - | } | |
| 623 | - | } | |
| 624 | - | ||
| 625 | - | successCount := 0 | |
| 626 | - | totalDuration := time.Duration(0) | |
| 627 | - | totalQueueTime := time.Duration(0) | |
| 628 | - | totalRetries := 0 | |
| 629 | - | ||
| 630 | - | opTypeCounts := make(map[OperationType]int) | |
| 631 | - | ||
| 632 | - | for _, stats := range cc.operationHistory { | |
| 633 | - | if stats.Success { | |
| 634 | - | successCount++ | |
| 635 | - | } | |
| 636 | - | totalDuration += stats.Duration | |
| 637 | - | totalQueueTime += stats.QueueTime | |
| 638 | - | totalRetries += stats.RetryCount | |
| 639 | - | opTypeCounts[stats.OperationType]++ | |
| 640 | - | } | |
| 641 | - | ||
| 642 | - | return map[string]interface{}{ | |
| 643 | - | "total_operations": totalOps, | |
| 644 | - | "successful_operations": successCount, | |
| 645 | - | "success_rate": float64(successCount) / float64(totalOps), | |
| 646 | - | "avg_duration_ms": (totalDuration / time.Duration(totalOps)).Milliseconds(), | |
| 647 | - | "avg_queue_time_ms": (totalQueueTime / time.Duration(totalOps)).Milliseconds(), | |
| 648 | - | "total_retries": totalRetries, | |
| 649 | - | "operation_counts": opTypeCounts, | |
| 650 | - | "queue_size": len(cc.operationQueue), | |
| 651 | - | "worker_count": cc.workerCount, | |
| 652 | - | "global_sequence": atomic.LoadUint64(&cc.globalSequence), | |
| 653 | - | } | |
| 654 | - | } | |
| 655 | - | ||
| 656 | - | // Shutdown stops the concurrency controller | |
| 657 | - | func (cc *ConcurrencyController) Shutdown() { | |
| 658 | - | cc.shutdownOnce.Do(func() { | |
| 659 | - | log.Printf("[concurrency] Shutting down concurrency controller") | |
| 660 | - | close(cc.shutdown) | |
| 661 | - | ||
| 662 | - | // Stop all workers | |
| 663 | - | for _, worker := range cc.workers { | |
| 664 | - | close(worker.shutdown) | |
| 665 | - | } | |
| 666 | - | ||
| 667 | - | // Wait for workers to finish with timeout | |
| 668 | - | done := make(chan struct{}) | |
| 669 | - | go func() { | |
| 670 | - | for _, worker := range cc.workers { | |
| 671 | - | worker.wg.Wait() | |
| 672 | - | } | |
| 673 | - | close(done) | |
| 674 | - | }() | |
| 675 | - | ||
| 676 | - | select { | |
| 677 | - | case <-done: | |
| 678 | - | log.Printf("[concurrency] All workers stopped gracefully") | |
| 679 | - | case <-time.After(5 * time.Second): | |
| 680 | - | log.Printf("[concurrency] Shutdown timeout, some workers may not have stopped gracefully") | |
| 681 | - | } | |
| 682 | - | }) | |
| 683 | - | } | |
| 684 | - | ||
| 685 | - | // Helper functions | |
| 686 | - | ||
| 687 | - | // isAncestor checks if node1 is an ancestor of node2 | |
| 688 | - | func isAncestor(node1, node2 *paneNode) bool { | |
| 689 | - | if node1 == nil || node2 == nil { | |
| 690 | - | return false | |
| 691 | - | } | |
| 692 | - | ||
| 693 | - | current := node2.parent | |
| 694 | - | for current != nil { | |
| 695 | - | if current == node1 { | |
| 696 | - | return true | |
| 697 | - | } | |
| 698 | - | current = current.parent | |
| 699 | - | } | |
| 700 | - | return false | |
| 701 | - | } | |
| @@ -1,701 +0,0 @@ | |||||
| 1 | - | // workspace_concurrency.go - Sequence-based concurrency control for bulletproof tree operations | |||
| 2 | - | package browser | |||
| 3 | - | ||||
| 4 | - | import ( | |||
| 5 | - | "context" | |||
| 6 | - | "fmt" | |||
| 7 | - | "log" | |||
| 8 | - | "sync" | |||
| 9 | - | "sync/atomic" | |||
| 10 | - | "time" | |||
| 11 | - | ||||
| 12 | - | "github.com/bnema/dumber/pkg/webkit" | |||
| 13 | - | ) | |||
| 14 | - | ||||
| 15 | - | // OperationType represents different types of tree operations | |||
| 16 | - | type OperationType int | |||
| 17 | - | ||||
| 18 | - | const ( | |||
| 19 | - | OpTypeSplit OperationType = iota | |||
| 20 | - | OpTypeClose | |||
| 21 | - | OpTypeStack | |||
| 22 | - | OpTypeFocus | |||
| 23 | - | OpTypeResize | |||
| 24 | - | ) | |||
| 25 | - | ||||
| 26 | - | func (ot OperationType) String() string { | |||
| 27 | - | switch ot { | |||
| 28 | - | case OpTypeSplit: | |||
| 29 | - | return "split" | |||
| 30 | - | case OpTypeClose: | |||
| 31 | - | return "close" | |||
| 32 | - | case OpTypeStack: | |||
| 33 | - | return "stack" | |||
| 34 | - | case OpTypeFocus: | |||
| 35 | - | return "focus" | |||
| 36 | - | case OpTypeResize: | |||
| 37 | - | return "resize" | |||
| 38 | - | default: | |||
| 39 | - | return "unknown" | |||
| 40 | - | } | |||
| 41 | - | } | |||
| 42 | - | ||||
| 43 | - | // OperationRequest represents a pending tree operation | |||
| 44 | - | type OperationRequest struct { | |||
| 45 | - | ID string | |||
| 46 | - | Type OperationType | |||
| 47 | - | TargetNode *paneNode | |||
| 48 | - | Direction string // For split operations | |||
| 49 | - | Parameters map[string]interface{} | |||
| 50 | - | SequenceNum uint64 | |||
| 51 | - | SubmittedAt time.Time | |||
| 52 | - | Context context.Context | |||
| 53 | - | ResultChan chan OperationResult | |||
| 54 | - | RetryCount int | |||
| 55 | - | MaxRetries int | |||
| 56 | - | } | |||
| 57 | - | ||||
| 58 | - | // OperationResult represents the result of an operation | |||
| 59 | - | type OperationResult struct { | |||
| 60 | - | Success bool | |||
| 61 | - | Error error | |||
| 62 | - | NewNode *paneNode | |||
| 63 | - | Duration time.Duration | |||
| 64 | - | SequenceNum uint64 | |||
| 65 | - | } | |||
| 66 | - | ||||
| 67 | - | // ConcurrencyController manages concurrent access to the tree | |||
| 68 | - | type ConcurrencyController struct { | |||
| 69 | - | // Sequence numbers for operation ordering | |||
| 70 | - | globalSequence uint64 | |||
| 71 | - | nodeSequences map[*paneNode]uint64 | |||
| 72 | - | sequenceMutex sync.RWMutex | |||
| 73 | - | ||||
| 74 | - | // Operation queuing and processing | |||
| 75 | - | operationQueue chan *OperationRequest | |||
| 76 | - | workerCount int | |||
| 77 | - | workers []*OperationWorker | |||
| 78 | - | shutdown chan struct{} | |||
| 79 | - | shutdownOnce sync.Once | |||
| 80 | - | ||||
| 81 | - | // Tree-level synchronization | |||
| 82 | - | treeMutex sync.RWMutex | |||
| 83 | - | readOperations map[string]context.CancelFunc // Read operations that can be cancelled | |||
| 84 | - | writeOperations map[string]*OperationRequest // Active write operations | |||
| 85 | - | ||||
| 86 | - | // Performance tracking | |||
| 87 | - | operationHistory []OperationStats | |||
| 88 | - | historyMutex sync.RWMutex | |||
| 89 | - | maxHistory int | |||
| 90 | - | ||||
| 91 | - | // Configuration | |||
| 92 | - | maxQueueSize int | |||
| 93 | - | operationTimeout time.Duration | |||
| 94 | - | retryBackoff time.Duration | |||
| 95 | - | deadlockTimeout time.Duration | |||
| 96 | - | ||||
| 97 | - | // Dependencies | |||
| 98 | - | widgetTxManager *WidgetTransactionManager | |||
| 99 | - | treeValidator *TreeValidator | |||
| 100 | - | workspaceManager *WorkspaceManager | |||
| 101 | - | } | |||
| 102 | - | ||||
| 103 | - | // OperationStats tracks performance metrics for operations | |||
| 104 | - | type OperationStats struct { | |||
| 105 | - | OperationType OperationType | |||
| 106 | - | Duration time.Duration | |||
| 107 | - | Success bool | |||
| 108 | - | RetryCount int | |||
| 109 | - | QueueTime time.Duration | |||
| 110 | - | SequenceNum uint64 | |||
| 111 | - | Timestamp time.Time | |||
| 112 | - | ConflictCount int | |||
| 113 | - | } | |||
| 114 | - | ||||
| 115 | - | // OperationWorker processes operations from the queue | |||
| 116 | - | type OperationWorker struct { | |||
| 117 | - | id int | |||
| 118 | - | controller *ConcurrencyController | |||
| 119 | - | shutdown chan struct{} | |||
| 120 | - | wg *sync.WaitGroup | |||
| 121 | - | } | |||
| 122 | - | ||||
| 123 | - | // NewConcurrencyController creates a new concurrency controller | |||
| 124 | - | func NewConcurrencyController(workerCount int, widgetTxManager *WidgetTransactionManager, treeValidator *TreeValidator) *ConcurrencyController { | |||
| 125 | - | if workerCount <= 0 { | |||
| 126 | - | workerCount = 2 // Default to 2 workers | |||
| 127 | - | } | |||
| 128 | - | ||||
| 129 | - | cc := &ConcurrencyController{ | |||
| 130 | - | globalSequence: 0, | |||
| 131 | - | nodeSequences: make(map[*paneNode]uint64), | |||
| 132 | - | operationQueue: make(chan *OperationRequest, 1000), // Buffered channel | |||
| 133 | - | workerCount: workerCount, | |||
| 134 | - | workers: make([]*OperationWorker, workerCount), | |||
| 135 | - | shutdown: make(chan struct{}), | |||
| 136 | - | readOperations: make(map[string]context.CancelFunc), | |||
| 137 | - | writeOperations: make(map[string]*OperationRequest), | |||
| 138 | - | operationHistory: make([]OperationStats, 0, 1000), | |||
| 139 | - | maxHistory: 1000, | |||
| 140 | - | maxQueueSize: 1000, | |||
| 141 | - | operationTimeout: 10 * time.Second, | |||
| 142 | - | retryBackoff: 100 * time.Millisecond, | |||
| 143 | - | deadlockTimeout: 30 * time.Second, | |||
| 144 | - | widgetTxManager: widgetTxManager, | |||
| 145 | - | treeValidator: treeValidator, | |||
| 146 | - | } | |||
| 147 | - | ||||
| 148 | - | // Start workers | |||
| 149 | - | var wg sync.WaitGroup | |||
| 150 | - | for i := 0; i < workerCount; i++ { | |||
| 151 | - | worker := &OperationWorker{ | |||
| 152 | - | id: i, | |||
| 153 | - | controller: cc, | |||
| 154 | - | shutdown: make(chan struct{}), | |||
| 155 | - | wg: &wg, | |||
| 156 | - | } | |||
| 157 | - | cc.workers[i] = worker | |||
| 158 | - | wg.Add(1) | |||
| 159 | - | go worker.run() | |||
| 160 | - | } | |||
| 161 | - | ||||
| 162 | - | log.Printf("[concurrency] Started concurrency controller with %d workers", workerCount) | |||
| 163 | - | return cc | |||
| 164 | - | } | |||
| 165 | - | ||||
| 166 | - | // SetWorkspaceManager sets the workspace manager reference (called after construction to avoid circular dependency) | |||
| 167 | - | func (cc *ConcurrencyController) SetWorkspaceManager(wm *WorkspaceManager) { | |||
| 168 | - | cc.workspaceManager = wm | |||
| 169 | - | } | |||
| 170 | - | ||||
| 171 | - | // SubmitOperation submits an operation for execution | |||
| 172 | - | func (cc *ConcurrencyController) SubmitOperation(req *OperationRequest) <-chan OperationResult { | |||
| 173 | - | // Assign sequence number | |||
| 174 | - | req.SequenceNum = atomic.AddUint64(&cc.globalSequence, 1) | |||
| 175 | - | req.SubmittedAt = time.Now() | |||
| 176 | - | req.ResultChan = make(chan OperationResult, 1) | |||
| 177 | - | ||||
| 178 | - | // Set default retry count if not specified | |||
| 179 | - | if req.MaxRetries == 0 { | |||
| 180 | - | req.MaxRetries = 3 | |||
| 181 | - | } | |||
| 182 | - | ||||
| 183 | - | // Set default context if not provided | |||
| 184 | - | if req.Context == nil { | |||
| 185 | - | ctx, cancel := context.WithTimeout(context.Background(), cc.operationTimeout) | |||
| 186 | - | req.Context = ctx | |||
| 187 | - | // Schedule cleanup | |||
| 188 | - | go func() { | |||
| 189 | - | <-ctx.Done() | |||
| 190 | - | cancel() | |||
| 191 | - | }() | |||
| 192 | - | } | |||
| 193 | - | ||||
| 194 | - | // Try to queue the operation | |||
| 195 | - | select { | |||
| 196 | - | case cc.operationQueue <- req: | |||
| 197 | - | log.Printf("[concurrency] Queued operation %s (seq=%d, type=%s)", req.ID, req.SequenceNum, req.Type) | |||
| 198 | - | case <-req.Context.Done(): | |||
| 199 | - | // Context cancelled before queuing | |||
| 200 | - | req.ResultChan <- OperationResult{ | |||
| 201 | - | Success: false, | |||
| 202 | - | Error: fmt.Errorf("operation cancelled before queuing: %w", req.Context.Err()), | |||
| 203 | - | } | |||
| 204 | - | default: | |||
| 205 | - | // Queue is full | |||
| 206 | - | req.ResultChan <- OperationResult{ | |||
| 207 | - | Success: false, | |||
| 208 | - | Error: fmt.Errorf("operation queue is full"), | |||
| 209 | - | } | |||
| 210 | - | } | |||
| 211 | - | ||||
| 212 | - | return req.ResultChan | |||
| 213 | - | } | |||
| 214 | - | ||||
| 215 | - | // worker run loop | |||
| 216 | - | func (worker *OperationWorker) run() { | |||
| 217 | - | defer worker.wg.Done() | |||
| 218 | - | log.Printf("[concurrency] Worker %d started", worker.id) | |||
| 219 | - | ||||
| 220 | - | for { | |||
| 221 | - | select { | |||
| 222 | - | case req := <-worker.controller.operationQueue: | |||
| 223 | - | worker.processOperation(req) | |||
| 224 | - | case <-worker.shutdown: | |||
| 225 | - | log.Printf("[concurrency] Worker %d shutting down", worker.id) | |||
| 226 | - | return | |||
| 227 | - | case <-worker.controller.shutdown: | |||
| 228 | - | log.Printf("[concurrency] Worker %d shutting down (global)", worker.id) | |||
| 229 | - | return | |||
| 230 | - | } | |||
| 231 | - | } | |||
| 232 | - | } | |||
| 233 | - | ||||
| 234 | - | // processOperation processes a single operation request | |||
| 235 | - | func (worker *OperationWorker) processOperation(req *OperationRequest) { | |||
| 236 | - | startTime := time.Now() | |||
| 237 | - | queueTime := startTime.Sub(req.SubmittedAt) | |||
| 238 | - | ||||
| 239 | - | log.Printf("[concurrency] Worker %d processing operation %s (seq=%d, queue_time=%v)", | |||
| 240 | - | worker.id, req.ID, req.SequenceNum, queueTime) | |||
| 241 | - | ||||
| 242 | - | result := OperationResult{ | |||
| 243 | - | SequenceNum: req.SequenceNum, | |||
| 244 | - | } | |||
| 245 | - | ||||
| 246 | - | // Check if context is already cancelled | |||
| 247 | - | select { | |||
| 248 | - | case <-req.Context.Done(): | |||
| 249 | - | result.Success = false | |||
| 250 | - | result.Error = fmt.Errorf("operation context cancelled: %w", req.Context.Err()) | |||
| 251 | - | worker.sendResult(req, result, startTime, queueTime) | |||
| 252 | - | return | |||
| 253 | - | default: | |||
| 254 | - | } | |||
| 255 | - | ||||
| 256 | - | // Check for conflicts and acquire locks | |||
| 257 | - | conflicts := worker.checkConflicts(req) | |||
| 258 | - | if conflicts > 0 && req.RetryCount < req.MaxRetries { | |||
| 259 | - | // Retry with backoff | |||
| 260 | - | req.RetryCount++ | |||
| 261 | - | backoff := time.Duration(req.RetryCount) * worker.controller.retryBackoff | |||
| 262 | - | ||||
| 263 | - | log.Printf("[concurrency] Operation %s has %d conflicts, retrying in %v (attempt %d/%d)", | |||
| 264 | - | req.ID, conflicts, backoff, req.RetryCount, req.MaxRetries) | |||
| 265 | - | ||||
| 266 | - | go func() { | |||
| 267 | - | time.Sleep(backoff) | |||
| 268 | - | select { | |||
| 269 | - | case worker.controller.operationQueue <- req: | |||
| 270 | - | case <-req.Context.Done(): | |||
| 271 | - | result.Success = false | |||
| 272 | - | result.Error = fmt.Errorf("operation cancelled during retry") | |||
| 273 | - | req.ResultChan <- result | |||
| 274 | - | } | |||
| 275 | - | }() | |||
| 276 | - | return | |||
| 277 | - | } | |||
| 278 | - | ||||
| 279 | - | if conflicts > 0 { | |||
| 280 | - | result.Success = false | |||
| 281 | - | result.Error = fmt.Errorf("operation failed after %d retries due to conflicts", req.MaxRetries) | |||
| 282 | - | worker.sendResult(req, result, startTime, queueTime) | |||
| 283 | - | return | |||
| 284 | - | } | |||
| 285 | - | ||||
| 286 | - | // Execute the operation | |||
| 287 | - | err := worker.executeOperation(req, &result) | |||
| 288 | - | if err != nil { | |||
| 289 | - | result.Success = false | |||
| 290 | - | result.Error = err | |||
| 291 | - | } else { | |||
| 292 | - | result.Success = true | |||
| 293 | - | } | |||
| 294 | - | ||||
| 295 | - | // Record stats and send result | |||
| 296 | - | worker.sendResult(req, result, startTime, queueTime) | |||
| 297 | - | } | |||
| 298 | - | ||||
| 299 | - | // checkConflicts checks for operation conflicts | |||
| 300 | - | func (worker *OperationWorker) checkConflicts(req *OperationRequest) int { | |||
| 301 | - | cc := worker.controller | |||
| 302 | - | conflicts := 0 | |||
| 303 | - | ||||
| 304 | - | cc.treeMutex.RLock() | |||
| 305 | - | defer cc.treeMutex.RUnlock() | |||
| 306 | - | ||||
| 307 | - | // Check for conflicting write operations on the same node | |||
| 308 | - | for _, activeOp := range cc.writeOperations { | |||
| 309 | - | if activeOp.TargetNode == req.TargetNode { | |||
| 310 | - | conflicts++ | |||
| 311 | - | } | |||
| 312 | - | // Check for parent-child conflicts | |||
| 313 | - | if req.TargetNode != nil && activeOp.TargetNode != nil { | |||
| 314 | - | if isAncestor(activeOp.TargetNode, req.TargetNode) || isAncestor(req.TargetNode, activeOp.TargetNode) { | |||
| 315 | - | conflicts++ | |||
| 316 | - | } | |||
| 317 | - | } | |||
| 318 | - | } | |||
| 319 | - | ||||
| 320 | - | return conflicts | |||
| 321 | - | } | |||
| 322 | - | ||||
| 323 | - | // executeOperation executes the actual operation | |||
| 324 | - | func (worker *OperationWorker) executeOperation(req *OperationRequest, result *OperationResult) error { | |||
| 325 | - | cc := worker.controller | |||
| 326 | - | ||||
| 327 | - | // Acquire tree write lock | |||
| 328 | - | cc.treeMutex.Lock() | |||
| 329 | - | defer cc.treeMutex.Unlock() | |||
| 330 | - | ||||
| 331 | - | // Register this operation as active | |||
| 332 | - | cc.writeOperations[req.ID] = req | |||
| 333 | - | defer delete(cc.writeOperations, req.ID) | |||
| 334 | - | ||||
| 335 | - | // Update node sequence | |||
| 336 | - | if req.TargetNode != nil { | |||
| 337 | - | cc.sequenceMutex.Lock() | |||
| 338 | - | cc.nodeSequences[req.TargetNode] = req.SequenceNum | |||
| 339 | - | cc.sequenceMutex.Unlock() | |||
| 340 | - | } | |||
| 341 | - | ||||
| 342 | - | // Create widget transaction for this operation | |||
| 343 | - | txID := fmt.Sprintf("op_%s_%d", req.ID, req.SequenceNum) | |||
| 344 | - | tx := cc.widgetTxManager.BeginTransaction(txID) | |||
| 345 | - | ||||
| 346 | - | // Execute based on operation type | |||
| 347 | - | var err error | |||
| 348 | - | switch req.Type { | |||
| 349 | - | case OpTypeSplit: | |||
| 350 | - | result.NewNode, err = worker.executeSplitOperation(req, tx) | |||
| 351 | - | case OpTypeClose: | |||
| 352 | - | result.NewNode, err = worker.executeCloseOperation(req, tx) | |||
| 353 | - | case OpTypeStack: | |||
| 354 | - | result.NewNode, err = worker.executeStackOperation(req, tx) | |||
| 355 | - | case OpTypeFocus: | |||
| 356 | - | err = worker.executeFocusOperation(req, tx) | |||
| 357 | - | case OpTypeResize: | |||
| 358 | - | err = worker.executeResizeOperation(req, tx) | |||
| 359 | - | default: | |||
| 360 | - | err = fmt.Errorf("unknown operation type: %s", req.Type) | |||
| 361 | - | } | |||
| 362 | - | ||||
| 363 | - | if err != nil { | |||
| 364 | - | // Rollback transaction on error | |||
| 365 | - | if rollbackErr := tx.Rollback(); rollbackErr != nil { | |||
| 366 | - | log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr) | |||
| 367 | - | } | |||
| 368 | - | cc.widgetTxManager.FinishTransaction(txID, false, err.Error()) | |||
| 369 | - | return err | |||
| 370 | - | } | |||
| 371 | - | ||||
| 372 | - | // Execute widget transaction | |||
| 373 | - | if err = tx.Execute(); err != nil { | |||
| 374 | - | if rollbackErr := tx.Rollback(); rollbackErr != nil { | |||
| 375 | - | log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr) | |||
| 376 | - | } | |||
| 377 | - | cc.widgetTxManager.FinishTransaction(txID, false, err.Error()) | |||
| 378 | - | return fmt.Errorf("widget transaction failed: %w", err) | |||
| 379 | - | } | |||
| 380 | - | ||||
| 381 | - | // Commit transaction | |||
| 382 | - | if err = tx.Commit(); err != nil { | |||
| 383 | - | cc.widgetTxManager.FinishTransaction(txID, false, err.Error()) | |||
| 384 | - | return fmt.Errorf("failed to commit transaction: %w", err) | |||
| 385 | - | } | |||
| 386 | - | ||||
| 387 | - | cc.widgetTxManager.FinishTransaction(txID, true, "") | |||
| 388 | - | ||||
| 389 | - | // Validate tree if validator is available | |||
| 390 | - | if cc.treeValidator != nil { | |||
| 391 | - | if validateErr := cc.treeValidator.ValidateTree(req.TargetNode, req.Type.String()); validateErr != nil { | |||
| 392 | - | log.Printf("[concurrency] Tree validation failed after operation %s: %v", req.ID, validateErr) | |||
| 393 | - | // Don't return error for validation failures in production, just log | |||
| 394 | - | } | |||
| 395 | - | } | |||
| 396 | - | ||||
| 397 | - | return nil | |||
| 398 | - | } | |||
| 399 | - | ||||
| 400 | - | // Placeholder implementations for operation execution | |||
| 401 | - | func (worker *OperationWorker) executeSplitOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) { | |||
| 402 | - | log.Printf("[concurrency] Executing split operation on node %p direction=%s", req.TargetNode, req.Direction) | |||
| 403 | - | ||||
| 404 | - | // Get the workspace manager from the controller | |||
| 405 | - | wm := worker.controller.workspaceManager | |||
| 406 | - | if wm == nil { | |||
| 407 | - | return nil, fmt.Errorf("workspace manager not available") | |||
| 408 | - | } | |||
| 409 | - | ||||
| 410 | - | // Fast-path when we are already on the GTK main thread. | |||
| 411 | - | if webkit.IsMainThread() { | |||
| 412 | - | log.Printf("[concurrency] Already on main thread, executing split directly") | |||
| 413 | - | newNode, err := wm.splitNode(req.TargetNode, req.Direction) | |||
| 414 | - | if err != nil { | |||
| 415 | - | log.Printf("[concurrency] Split operation failed: %v", err) | |||
| 416 | - | return nil, err | |||
| 417 | - | } | |||
| 418 | - | log.Printf("[concurrency] Split operation completed successfully (direct): newNode=%p", newNode) | |||
| 419 | - | return newNode, nil | |||
| 420 | - | } | |||
| 421 | - | ||||
| 422 | - | // GTK operations must be executed on the main thread | |||
| 423 | - | // Use IdleAdd to marshal the GTK work to the main thread | |||
| 424 | - | gtkComplete := make(chan struct { | |||
| 425 | - | newNode *paneNode | |||
| 426 | - | err error | |||
| 427 | - | }, 1) | |||
| 428 | - | ||||
| 429 | - | log.Printf("[concurrency] Marshaling split operation to main thread") | |||
| 430 | - | ||||
| 431 | - | // Schedule GTK work on main thread using IdleAdd | |||
| 432 | - | webkit.IdleAdd(func() bool { | |||
| 433 | - | // Perform actual GTK split operations on main thread | |||
| 434 | - | newNode, err := wm.splitNode(req.TargetNode, req.Direction) | |||
| 435 | - | gtkComplete <- struct { | |||
| 436 | - | newNode *paneNode | |||
| 437 | - | err error | |||
| 438 | - | }{newNode: newNode, err: err} | |||
| 439 | - | return false // Remove idle callback (G_SOURCE_REMOVE) | |||
| 440 | - | }) | |||
| 441 | - | ||||
| 442 | - | // Wait for GTK completion | |||
| 443 | - | result := <-gtkComplete | |||
| 444 | - | ||||
| 445 | - | if result.err != nil { | |||
| 446 | - | log.Printf("[concurrency] Split operation failed on main thread: %v", result.err) | |||
| 447 | - | return nil, result.err | |||
| 448 | - | } | |||
| 449 | - | ||||
| 450 | - | log.Printf("[concurrency] Split operation completed successfully on main thread: newNode=%p", result.newNode) | |||
| 451 | - | return result.newNode, nil | |||
| 452 | - | } | |||
| 453 | - | ||||
| 454 | - | func (worker *OperationWorker) executeCloseOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) { | |||
| 455 | - | log.Printf("[concurrency] Executing close operation on node %p", req.TargetNode) | |||
| 456 | - | ||||
| 457 | - | // Get the workspace manager from the controller | |||
| 458 | - | wm := worker.controller.workspaceManager | |||
| 459 | - | if wm == nil { | |||
| 460 | - | return nil, fmt.Errorf("workspace manager not available") | |||
| 461 | - | } | |||
| 462 | - | ||||
| 463 | - | // Fast-path when we are already on the GTK main thread. | |||
| 464 | - | if webkit.IsMainThread() { | |||
| 465 | - | log.Printf("[concurrency] Already on main thread, executing close directly") | |||
| 466 | - | promoted, err := wm.closePane(req.TargetNode) | |||
| 467 | - | if err != nil { | |||
| 468 | - | log.Printf("[concurrency] Close operation failed: %v", err) | |||
| 469 | - | return nil, err | |||
| 470 | - | } | |||
| 471 | - | log.Printf("[concurrency] Close operation completed successfully (direct)") | |||
| 472 | - | return promoted, nil | |||
| 473 | - | } | |||
| 474 | - | ||||
| 475 | - | // GTK operations must be executed on the main thread | |||
| 476 | - | // Use IdleAdd to marshal the GTK work to the main thread | |||
| 477 | - | gtkComplete := make(chan struct { | |||
| 478 | - | promoted *paneNode | |||
| 479 | - | err error | |||
| 480 | - | }, 1) | |||
| 481 | - | ||||
| 482 | - | log.Printf("[concurrency] Marshaling close operation to main thread") | |||
| 483 | - | ||||
| 484 | - | // Schedule GTK work on main thread using IdleAdd | |||
| 485 | - | webkit.IdleAdd(func() bool { | |||
| 486 | - | // Perform actual GTK close operations on main thread | |||
| 487 | - | promoted, err := wm.closePane(req.TargetNode) | |||
| 488 | - | gtkComplete <- struct { | |||
| 489 | - | promoted *paneNode | |||
| 490 | - | err error | |||
| 491 | - | }{promoted: promoted, err: err} | |||
| 492 | - | return false // Remove idle callback (G_SOURCE_REMOVE) | |||
| 493 | - | }) | |||
| 494 | - | ||||
| 495 | - | // Wait for GTK completion | |||
| 496 | - | result := <-gtkComplete | |||
| 497 | - | ||||
| 498 | - | if result.err != nil { | |||
| 499 | - | log.Printf("[concurrency] Close operation failed on main thread: %v", result.err) | |||
| 500 | - | return nil, result.err | |||
| 501 | - | } | |||
| 502 | - | ||||
| 503 | - | log.Printf("[concurrency] Close operation completed successfully on main thread") | |||
| 504 | - | return result.promoted, nil | |||
| 505 | - | } | |||
| 506 | - | ||||
| 507 | - | func (worker *OperationWorker) executeStackOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) { | |||
| 508 | - | log.Printf("[concurrency] Executing stack operation on node %p", req.TargetNode) | |||
| 509 | - | ||||
| 510 | - | // Get the workspace manager from the controller | |||
| 511 | - | wm := worker.controller.workspaceManager | |||
| 512 | - | if wm == nil { | |||
| 513 | - | return nil, fmt.Errorf("workspace manager not available") | |||
| 514 | - | } | |||
| 515 | - | ||||
| 516 | - | // Fast-path when we are already on the GTK main thread. | |||
| 517 | - | if webkit.IsMainThread() { | |||
| 518 | - | log.Printf("[concurrency] Already on main thread, executing stack directly") | |||
| 519 | - | newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode) | |||
| 520 | - | if err != nil { | |||
| 521 | - | log.Printf("[concurrency] Stack operation failed: %v", err) | |||
| 522 | - | return nil, err | |||
| 523 | - | } | |||
| 524 | - | log.Printf("[concurrency] Stack operation completed successfully (direct): newNode=%p", newNode) | |||
| 525 | - | return newNode, nil | |||
| 526 | - | } | |||
| 527 | - | ||||
| 528 | - | // GTK operations must be executed on the main thread | |||
| 529 | - | // Use IdleAdd to marshal the GTK work to the main thread | |||
| 530 | - | gtkComplete := make(chan struct { | |||
| 531 | - | newNode *paneNode | |||
| 532 | - | err error | |||
| 533 | - | }, 1) | |||
| 534 | - | ||||
| 535 | - | log.Printf("[concurrency] Marshaling stack operation to main thread") | |||
| 536 | - | ||||
| 537 | - | // Schedule GTK work on main thread using IdleAdd | |||
| 538 | - | webkit.IdleAdd(func() bool { | |||
| 539 | - | // Perform actual GTK stack operations on main thread | |||
| 540 | - | newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode) | |||
| 541 | - | gtkComplete <- struct { | |||
| 542 | - | newNode *paneNode | |||
| 543 | - | err error | |||
| 544 | - | }{newNode: newNode, err: err} | |||
| 545 | - | return false // Remove idle callback (G_SOURCE_REMOVE) | |||
| 546 | - | }) | |||
| 547 | - | ||||
| 548 | - | // Wait for GTK completion | |||
| 549 | - | result := <-gtkComplete | |||
| 550 | - | ||||
| 551 | - | if result.err != nil { | |||
| 552 | - | log.Printf("[concurrency] Stack operation failed on main thread: %v", result.err) | |||
| 553 | - | return nil, result.err | |||
| 554 | - | } | |||
| 555 | - | ||||
| 556 | - | log.Printf("[concurrency] Stack operation completed successfully on main thread: newNode=%p", result.newNode) | |||
| 557 | - | return result.newNode, nil | |||
| 558 | - | } | |||
| 559 | - | ||||
| 560 | - | func (worker *OperationWorker) executeFocusOperation(req *OperationRequest, tx *WidgetTransaction) error { | |||
| 561 | - | // This would integrate with your existing focus logic | |||
| 562 | - | log.Printf("[concurrency] Executing focus operation on node %p", req.TargetNode) | |||
| 563 | - | // TODO: Implement actual focus logic with transaction support | |||
| 564 | - | return fmt.Errorf("focus operation not yet implemented") | |||
| 565 | - | } | |||
| 566 | - | ||||
| 567 | - | func (worker *OperationWorker) executeResizeOperation(req *OperationRequest, tx *WidgetTransaction) error { | |||
| 568 | - | // This would integrate with your existing resize logic | |||
| 569 | - | log.Printf("[concurrency] Executing resize operation on node %p", req.TargetNode) | |||
| 570 | - | // TODO: Implement actual resize logic with transaction support | |||
| 571 | - | return fmt.Errorf("resize operation not yet implemented") | |||
| 572 | - | } | |||
| 573 | - | ||||
| 574 | - | // sendResult sends the operation result and records stats | |||
| 575 | - | func (worker *OperationWorker) sendResult(req *OperationRequest, result OperationResult, startTime time.Time, queueTime time.Duration) { | |||
| 576 | - | result.Duration = time.Since(startTime) | |||
| 577 | - | ||||
| 578 | - | // Record stats | |||
| 579 | - | stats := OperationStats{ | |||
| 580 | - | OperationType: req.Type, | |||
| 581 | - | Duration: result.Duration, | |||
| 582 | - | Success: result.Success, | |||
| 583 | - | RetryCount: req.RetryCount, | |||
| 584 | - | QueueTime: queueTime, | |||
| 585 | - | SequenceNum: req.SequenceNum, | |||
| 586 | - | Timestamp: time.Now(), | |||
| 587 | - | } | |||
| 588 | - | ||||
| 589 | - | worker.controller.recordStats(stats) | |||
| 590 | - | ||||
| 591 | - | // Send result | |||
| 592 | - | select { | |||
| 593 | - | case req.ResultChan <- result: | |||
| 594 | - | case <-req.Context.Done(): | |||
| 595 | - | // Context cancelled, result channel may be closed | |||
| 596 | - | } | |||
| 597 | - | ||||
| 598 | - | log.Printf("[concurrency] Worker %d completed operation %s: success=%v, duration=%v", | |||
| 599 | - | worker.id, req.ID, result.Success, result.Duration) | |||
| 600 | - | } | |||
| 601 | - | ||||
| 602 | - | // recordStats records operation statistics | |||
| 603 | - | func (cc *ConcurrencyController) recordStats(stats OperationStats) { | |||
| 604 | - | cc.historyMutex.Lock() | |||
| 605 | - | defer cc.historyMutex.Unlock() | |||
| 606 | - | ||||
| 607 | - | if len(cc.operationHistory) >= cc.maxHistory { | |||
| 608 | - | cc.operationHistory = cc.operationHistory[1:] | |||
| 609 | - | } | |||
| 610 | - | cc.operationHistory = append(cc.operationHistory, stats) | |||
| 611 | - | } | |||
| 612 | - | ||||
| 613 | - | // GetConcurrencyStats returns performance statistics | |||
| 614 | - | func (cc *ConcurrencyController) GetConcurrencyStats() map[string]interface{} { | |||
| 615 | - | cc.historyMutex.RLock() | |||
| 616 | - | defer cc.historyMutex.RUnlock() | |||
| 617 | - | ||||
| 618 | - | totalOps := len(cc.operationHistory) | |||
| 619 | - | if totalOps == 0 { | |||
| 620 | - | return map[string]interface{}{ | |||
| 621 | - | "total_operations": 0, | |||
| 622 | - | } | |||
| 623 | - | } | |||
| 624 | - | ||||
| 625 | - | successCount := 0 | |||
| 626 | - | totalDuration := time.Duration(0) | |||
| 627 | - | totalQueueTime := time.Duration(0) | |||
| 628 | - | totalRetries := 0 | |||
| 629 | - | ||||
| 630 | - | opTypeCounts := make(map[OperationType]int) | |||
| 631 | - | ||||
| 632 | - | for _, stats := range cc.operationHistory { | |||
| 633 | - | if stats.Success { | |||
| 634 | - | successCount++ | |||
| 635 | - | } | |||
| 636 | - | totalDuration += stats.Duration | |||
| 637 | - | totalQueueTime += stats.QueueTime | |||
| 638 | - | totalRetries += stats.RetryCount | |||
| 639 | - | opTypeCounts[stats.OperationType]++ | |||
| 640 | - | } | |||
| 641 | - | ||||
| 642 | - | return map[string]interface{}{ | |||
| 643 | - | "total_operations": totalOps, | |||
| 644 | - | "successful_operations": successCount, | |||
| 645 | - | "success_rate": float64(successCount) / float64(totalOps), | |||
| 646 | - | "avg_duration_ms": (totalDuration / time.Duration(totalOps)).Milliseconds(), | |||
| 647 | - | "avg_queue_time_ms": (totalQueueTime / time.Duration(totalOps)).Milliseconds(), | |||
| 648 | - | "total_retries": totalRetries, | |||
| 649 | - | "operation_counts": opTypeCounts, | |||
| 650 | - | "queue_size": len(cc.operationQueue), | |||
| 651 | - | "worker_count": cc.workerCount, | |||
| 652 | - | "global_sequence": atomic.LoadUint64(&cc.globalSequence), | |||
| 653 | - | } | |||
| 654 | - | } | |||
| 655 | - | ||||
| 656 | - | // Shutdown stops the concurrency controller | |||
| 657 | - | func (cc *ConcurrencyController) Shutdown() { | |||
| 658 | - | cc.shutdownOnce.Do(func() { | |||
| 659 | - | log.Printf("[concurrency] Shutting down concurrency controller") | |||
| 660 | - | close(cc.shutdown) | |||
| 661 | - | ||||
| 662 | - | // Stop all workers | |||
| 663 | - | for _, worker := range cc.workers { | |||
| 664 | - | close(worker.shutdown) | |||
| 665 | - | } | |||
| 666 | - | ||||
| 667 | - | // Wait for workers to finish with timeout | |||
| 668 | - | done := make(chan struct{}) | |||
| 669 | - | go func() { | |||
| 670 | - | for _, worker := range cc.workers { | |||
| 671 | - | worker.wg.Wait() | |||
| 672 | - | } | |||
| 673 | - | close(done) | |||
| 674 | - | }() | |||
| 675 | - | ||||
| 676 | - | select { | |||
| 677 | - | case <-done: | |||
| 678 | - | log.Printf("[concurrency] All workers stopped gracefully") | |||
| 679 | - | case <-time.After(5 * time.Second): | |||
| 680 | - | log.Printf("[concurrency] Shutdown timeout, some workers may not have stopped gracefully") | |||
| 681 | - | } | |||
| 682 | - | }) | |||
| 683 | - | } | |||
| 684 | - | ||||
| 685 | - | // Helper functions | |||
| 686 | - | ||||
| 687 | - | // isAncestor checks if node1 is an ancestor of node2 | |||
| 688 | - | func isAncestor(node1, node2 *paneNode) bool { | |||
| 689 | - | if node1 == nil || node2 == nil { | |||
| 690 | - | return false | |||
| 691 | - | } | |||
| 692 | - | ||||
| 693 | - | current := node2.parent | |||
| 694 | - | for current != nil { | |||
| 695 | - | if current == node1 { | |||
| 696 | - | return true | |||
| 697 | - | } | |||
| 698 | - | current = current.parent | |||
| 699 | - | } | |||
| 700 | - | return false | |||
| 701 | - | } | |||
M internal/app/browser/workspace_manager.go +2 -7
| @@ -57,10 +57,9 @@ type WorkspaceManager struct { | |||
| 57 | 57 | lastFocusTarget *paneNode | |
| 58 | 58 | focusDebounce time.Duration | |
| 59 | 59 | ||
| 60 | - | // BULLETPROOF COMPONENTS: Enhanced validation and safety systems | |
| 60 | + | // ENHANCED COMPONENTS: Validation and safety systems (opt-in for debugging) | |
| 61 | 61 | treeValidator *TreeValidator | |
| 62 | 62 | widgetTxManager *WidgetTransactionManager | |
| 63 | - | concurrencyController *ConcurrencyController | |
| 64 | 63 | treeRebalancer *TreeRebalancer | |
| 65 | 64 | geometryValidator *GeometryValidator | |
| 66 | 65 | stackLifecycleManager *StackLifecycleManager | |
| @@ -95,14 +94,10 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 95 | 94 | manager.geometryValidator = NewGeometryValidator() | |
| 96 | 95 | manager.stateTombstoneManager = NewStateTombstoneManager(manager) | |
| 97 | 96 | ||
| 98 | - | // Initialize concurrency controller with bulletproof components | |
| 99 | - | manager.concurrencyController = NewConcurrencyController(2, manager.widgetTxManager, manager.treeValidator) | |
| 100 | - | manager.concurrencyController.SetWorkspaceManager(manager) | |
| 101 | - | ||
| 102 | 97 | // Initialize tree rebalancer | |
| 103 | 98 | manager.treeRebalancer = NewTreeRebalancer(manager, manager.treeValidator, manager.widgetTxManager) | |
| 104 | 99 | ||
| 105 | - | // Initialize stack lifecycle manager with bulletproof components | |
| 100 | + | // Initialize stack lifecycle manager with enhanced components | |
| 106 | 101 | manager.stackLifecycleManager = NewStackLifecycleManager(manager, manager.treeValidator, manager.widgetTxManager) | |
| 107 | 102 | ||
| 108 | 103 | // Initialize existing specialized managers (now enhanced with bulletproof components) | |
| @@ -57,10 +57,9 @@ type WorkspaceManager struct { | |||||
| 57 | lastFocusTarget *paneNode | 57 | lastFocusTarget *paneNode | ||
| 58 | focusDebounce time.Duration | 58 | focusDebounce time.Duration | ||
| 59 | 59 | ||||
| 60 | - | // BULLETPROOF COMPONENTS: Enhanced validation and safety systems | 60 | + | // ENHANCED COMPONENTS: Validation and safety systems (opt-in for debugging) |
| 61 | treeValidator *TreeValidator | 61 | treeValidator *TreeValidator | ||
| 62 | widgetTxManager *WidgetTransactionManager | 62 | widgetTxManager *WidgetTransactionManager | ||
| 63 | - | concurrencyController *ConcurrencyController | |||
| 64 | treeRebalancer *TreeRebalancer | 63 | treeRebalancer *TreeRebalancer | ||
| 65 | geometryValidator *GeometryValidator | 64 | geometryValidator *GeometryValidator | ||
| 66 | stackLifecycleManager *StackLifecycleManager | 65 | stackLifecycleManager *StackLifecycleManager | ||
| @@ -95,14 +94,10 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||||
| 95 | manager.geometryValidator = NewGeometryValidator() | 94 | manager.geometryValidator = NewGeometryValidator() | ||
| 96 | manager.stateTombstoneManager = NewStateTombstoneManager(manager) | 95 | manager.stateTombstoneManager = NewStateTombstoneManager(manager) | ||
| 97 | 96 | ||||
| 98 | - | // Initialize concurrency controller with bulletproof components | |||
| 99 | - | manager.concurrencyController = NewConcurrencyController(2, manager.widgetTxManager, manager.treeValidator) | |||
| 100 | - | manager.concurrencyController.SetWorkspaceManager(manager) | |||
| 101 | - | ||||
| 102 | // Initialize tree rebalancer | 97 | // Initialize tree rebalancer | ||
| 103 | manager.treeRebalancer = NewTreeRebalancer(manager, manager.treeValidator, manager.widgetTxManager) | 98 | manager.treeRebalancer = NewTreeRebalancer(manager, manager.treeValidator, manager.widgetTxManager) | ||
| 104 | 99 | ||||
| 105 | - | // Initialize stack lifecycle manager with bulletproof components | 100 | + | // Initialize stack lifecycle manager with enhanced components |
| 106 | manager.stackLifecycleManager = NewStackLifecycleManager(manager, manager.treeValidator, manager.widgetTxManager) | 101 | manager.stackLifecycleManager = NewStackLifecycleManager(manager, manager.treeValidator, manager.widgetTxManager) | ||
| 107 | 102 | ||||
| 108 | // Initialize existing specialized managers (now enhanced with bulletproof components) | 103 | // Initialize existing specialized managers (now enhanced with bulletproof components) | ||
M internal/app/browser/workspace_operations.go +82 -143
| @@ -1,11 +1,9 @@ | |||
| 1 | - | // workspace_bulletproof_operations.go - Bulletproof wrapper methods for all tree operations | |
| 1 | + | // workspace_operations.go - Workspace operation methods with validation and safety | |
| 2 | 2 | package browser | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | - | "context" | |
| 6 | 5 | "fmt" | |
| 7 | 6 | "log" | |
| 8 | - | "time" | |
| 9 | 7 | ||
| 10 | 8 | "github.com/bnema/dumber/pkg/webkit" | |
| 11 | 9 | ) | |
| @@ -64,58 +62,37 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane | |||
| 64 | 62 | return newNode, nil | |
| 65 | 63 | } | |
| 66 | 64 | ||
| 67 | - | // Step 5: Not on main thread, marshal through concurrency controller | |
| 68 | - | opReq := &OperationRequest{ | |
| 69 | - | ID: fmt.Sprintf("split_%p_%d", target, time.Now().UnixNano()), | |
| 70 | - | Type: OpTypeSplit, | |
| 71 | - | TargetNode: target, | |
| 72 | - | Direction: direction, | |
| 73 | - | Parameters: map[string]interface{}{ | |
| 74 | - | "direction": direction, | |
| 75 | - | }, | |
| 76 | - | Context: context.Background(), | |
| 77 | - | MaxRetries: 3, | |
| 78 | - | } | |
| 79 | - | ||
| 80 | - | resultChan := wm.concurrencyController.SubmitOperation(opReq) | |
| 81 | - | ||
| 82 | - | log.Printf("[workspace] Waiting for operation result while pumping GTK events") | |
| 83 | - | ticker := time.NewTicker(10 * time.Millisecond) | |
| 84 | - | defer ticker.Stop() | |
| 85 | - | ||
| 86 | - | timeout := time.NewTimer(30 * time.Second) | |
| 87 | - | defer timeout.Stop() | |
| 88 | - | ||
| 89 | - | for { | |
| 90 | - | select { | |
| 91 | - | case result := <-resultChan: | |
| 92 | - | if !result.Success { | |
| 93 | - | if tombstone != nil { | |
| 94 | - | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |
| 95 | - | log.Printf("[bulletproof] Rollback failed after split failure: %v", rollbackErr) | |
| 96 | - | } | |
| 65 | + | // Step 5: Not on main thread, marshal via IdleAdd | |
| 66 | + | log.Printf("[workspace] Not on main thread, marshalling split via IdleAdd") | |
| 67 | + | var newNode *paneNode | |
| 68 | + | var splitErr error | |
| 69 | + | done := make(chan struct{}) | |
| 70 | + | ||
| 71 | + | webkit.IdleAdd(func() bool { | |
| 72 | + | newNode, splitErr = wm.splitNode(target, direction) | |
| 73 | + | if splitErr != nil { | |
| 74 | + | if tombstone != nil { | |
| 75 | + | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |
| 76 | + | log.Printf("[workspace] Rollback failed after split failure: %v", rollbackErr) | |
| 97 | 77 | } | |
| 98 | - | return nil, result.Error | |
| 99 | 78 | } | |
| 100 | - | ||
| 101 | - | if err := wm.treeValidator.ValidateTree(wm.root, "after_split"); err != nil { | |
| 102 | - | log.Printf("[workspace] Tree validation failed after split: %v", err) | |
| 79 | + | } else { | |
| 80 | + | if verr := wm.treeValidator.ValidateTree(wm.root, "after_split"); verr != nil { | |
| 81 | + | log.Printf("[workspace] Tree validation failed after split: %v", verr) | |
| 103 | 82 | } | |
| 83 | + | } | |
| 84 | + | close(done) | |
| 85 | + | return false | |
| 86 | + | }) | |
| 104 | 87 | ||
| 105 | - | // Tree rebalancing is only needed after close promotions. Splits have correct allocation from GTK. | |
| 106 | - | ||
| 107 | - | log.Printf("[workspace] Split operation completed successfully: newNode=%p", result.NewNode) | |
| 108 | - | return result.NewNode, nil | |
| 88 | + | <-done | |
| 109 | 89 | ||
| 110 | - | case <-ticker.C: | |
| 111 | - | if webkit.IsMainThread() { | |
| 112 | - | webkit.IterateMainLoop() | |
| 113 | - | } | |
| 114 | - | ||
| 115 | - | case <-timeout.C: | |
| 116 | - | return nil, fmt.Errorf("split operation timed out") | |
| 117 | - | } | |
| 90 | + | if splitErr != nil { | |
| 91 | + | return nil, splitErr | |
| 118 | 92 | } | |
| 93 | + | ||
| 94 | + | log.Printf("[workspace] Split operation completed successfully: newNode=%p", newNode) | |
| 95 | + | return newNode, nil | |
| 119 | 96 | } | |
| 120 | 97 | ||
| 121 | 98 | // ClosePane performs a close operation with validation and safety checks | |
| @@ -178,64 +155,48 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error { | |||
| 178 | 155 | return nil | |
| 179 | 156 | } | |
| 180 | 157 | ||
| 181 | - | // Step 5: Not on main thread, marshal through concurrency controller | |
| 182 | - | log.Printf("[workspace] Not on main thread, using concurrency controller") | |
| 183 | - | opReq := &OperationRequest{ | |
| 184 | - | ID: fmt.Sprintf("close_%p_%d", node, time.Now().UnixNano()), | |
| 185 | - | Type: OpTypeClose, | |
| 186 | - | TargetNode: node, | |
| 187 | - | Parameters: map[string]interface{}{}, | |
| 188 | - | Context: context.Background(), | |
| 189 | - | MaxRetries: 3, | |
| 190 | - | } | |
| 191 | - | ||
| 192 | - | resultChan := wm.concurrencyController.SubmitOperation(opReq) | |
| 193 | - | ||
| 194 | - | log.Printf("[workspace] Waiting for operation result while pumping GTK events") | |
| 195 | - | ticker := time.NewTicker(10 * time.Millisecond) | |
| 196 | - | defer ticker.Stop() | |
| 197 | - | ||
| 198 | - | timeout := time.NewTimer(30 * time.Second) | |
| 199 | - | defer timeout.Stop() | |
| 200 | - | ||
| 201 | - | for { | |
| 202 | - | select { | |
| 203 | - | case result := <-resultChan: | |
| 204 | - | if !result.Success { | |
| 205 | - | wm.paneCloseLogf("async close failed node=%p err=%v", node, result.Error) | |
| 206 | - | wm.dumpTreeState("after_close_error") | |
| 207 | - | if tombstone != nil { | |
| 208 | - | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |
| 209 | - | log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr) | |
| 210 | - | } | |
| 158 | + | // Step 5: Not on main thread, marshal via IdleAdd | |
| 159 | + | log.Printf("[workspace] Not on main thread, marshalling close via IdleAdd") | |
| 160 | + | var promoted *paneNode | |
| 161 | + | var closeErr error | |
| 162 | + | done := make(chan struct{}) | |
| 163 | + | ||
| 164 | + | webkit.IdleAdd(func() bool { | |
| 165 | + | wm.paneCloseLogf("invoking closePane node=%p", node) | |
| 166 | + | promoted, closeErr = wm.closePane(node) | |
| 167 | + | if closeErr != nil { | |
| 168 | + | wm.paneCloseLogf("closePane failed node=%p err=%v", node, closeErr) | |
| 169 | + | wm.dumpTreeState("after_close_error") | |
| 170 | + | if tombstone != nil { | |
| 171 | + | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |
| 172 | + | log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr) | |
| 211 | 173 | } | |
| 212 | - | return result.Error | |
| 213 | 174 | } | |
| 214 | - | ||
| 215 | - | if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil { | |
| 216 | - | log.Printf("[workspace] Tree validation failed after close: %v", err) | |
| 175 | + | } else { | |
| 176 | + | if verr := wm.treeValidator.ValidateTree(wm.root, "after_close"); verr != nil { | |
| 177 | + | log.Printf("[workspace] Tree validation failed after close: %v", verr) | |
| 217 | 178 | } | |
| 218 | - | wm.paneCloseLogf("async close succeeded node=%p promoted=%p root=%p", node, result.NewNode, wm.root) | |
| 179 | + | wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root) | |
| 219 | 180 | wm.dumpTreeState("after_close_success") | |
| 220 | 181 | ||
| 221 | 182 | if wm.treeRebalancer != nil { | |
| 222 | - | if err := wm.treeRebalancer.RebalanceAfterClose(node, result.NewNode); err != nil { | |
| 223 | - | log.Printf("[workspace] Tree rebalancing failed after close: %v", err) | |
| 183 | + | if rerr := wm.treeRebalancer.RebalanceAfterClose(node, promoted); rerr != nil { | |
| 184 | + | log.Printf("[workspace] Tree rebalancing failed after close: %v", rerr) | |
| 224 | 185 | } | |
| 225 | 186 | } | |
| 187 | + | } | |
| 188 | + | close(done) | |
| 189 | + | return false | |
| 190 | + | }) | |
| 226 | 191 | ||
| 227 | - | log.Printf("[workspace] Close operation completed successfully") | |
| 228 | - | return nil | |
| 229 | - | ||
| 230 | - | case <-ticker.C: | |
| 231 | - | if webkit.IsMainThread() { | |
| 232 | - | webkit.IterateMainLoop() | |
| 233 | - | } | |
| 192 | + | <-done | |
| 234 | 193 | ||
| 235 | - | case <-timeout.C: | |
| 236 | - | return fmt.Errorf("close operation timed out") | |
| 237 | - | } | |
| 194 | + | if closeErr != nil { | |
| 195 | + | return closeErr | |
| 238 | 196 | } | |
| 197 | + | ||
| 198 | + | log.Printf("[workspace] Close operation completed successfully") | |
| 199 | + | return nil | |
| 239 | 200 | } | |
| 240 | 201 | ||
| 241 | 202 | // StackPane performs a stack operation with validation and safety checks | |
| @@ -275,7 +236,7 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) { | |||
| 275 | 236 | if err != nil { | |
| 276 | 237 | if tombstone != nil { | |
| 277 | 238 | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |
| 278 | - | log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr) | |
| 239 | + | log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr) | |
| 279 | 240 | } | |
| 280 | 241 | } | |
| 281 | 242 | return nil, err | |
| @@ -289,53 +250,37 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) { | |||
| 289 | 250 | return newNode, nil | |
| 290 | 251 | } | |
| 291 | 252 | ||
| 292 | - | // Step 5: Not on main thread, marshal through concurrency controller | |
| 293 | - | opReq := &OperationRequest{ | |
| 294 | - | ID: fmt.Sprintf("stack_%p_%d", target, time.Now().UnixNano()), | |
| 295 | - | Type: OpTypeStack, | |
| 296 | - | TargetNode: target, | |
| 297 | - | Parameters: map[string]interface{}{}, | |
| 298 | - | Context: context.Background(), | |
| 299 | - | MaxRetries: 3, | |
| 300 | - | } | |
| 301 | - | ||
| 302 | - | resultChan := wm.concurrencyController.SubmitOperation(opReq) | |
| 303 | - | ||
| 304 | - | log.Printf("[workspace] Waiting for operation result while pumping GTK events") | |
| 305 | - | ticker := time.NewTicker(10 * time.Millisecond) | |
| 306 | - | defer ticker.Stop() | |
| 253 | + | // Step 5: Not on main thread, marshal via IdleAdd | |
| 254 | + | log.Printf("[workspace] Not on main thread, marshalling stack via IdleAdd") | |
| 255 | + | var newNode *paneNode | |
| 256 | + | var stackErr error | |
| 257 | + | done := make(chan struct{}) | |
| 307 | 258 | ||
| 308 | - | timeout := time.NewTimer(30 * time.Second) | |
| 309 | - | defer timeout.Stop() | |
| 310 | - | ||
| 311 | - | for { | |
| 312 | - | select { | |
| 313 | - | case result := <-resultChan: | |
| 314 | - | if !result.Success { | |
| 315 | - | if tombstone != nil { | |
| 316 | - | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |
| 317 | - | log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr) | |
| 318 | - | } | |
| 259 | + | webkit.IdleAdd(func() bool { | |
| 260 | + | newNode, stackErr = wm.stackedPaneManager.StackPane(target) | |
| 261 | + | if stackErr != nil { | |
| 262 | + | if tombstone != nil { | |
| 263 | + | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |
| 264 | + | log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr) | |
| 319 | 265 | } | |
| 320 | - | return nil, result.Error | |
| 321 | 266 | } | |
| 322 | - | ||
| 323 | - | if err := wm.treeValidator.ValidateTree(wm.root, "after_stack"); err != nil { | |
| 324 | - | log.Printf("[workspace] Tree validation failed after stack: %v", err) | |
| 267 | + | } else { | |
| 268 | + | if verr := wm.treeValidator.ValidateTree(wm.root, "after_stack"); verr != nil { | |
| 269 | + | log.Printf("[workspace] Tree validation failed after stack: %v", verr) | |
| 325 | 270 | } | |
| 271 | + | } | |
| 272 | + | close(done) | |
| 273 | + | return false | |
| 274 | + | }) | |
| 326 | 275 | ||
| 327 | - | log.Printf("[workspace] Stack operation completed successfully: newNode=%p", result.NewNode) | |
| 328 | - | return result.NewNode, nil | |
| 329 | - | ||
| 330 | - | case <-ticker.C: | |
| 331 | - | if webkit.IsMainThread() { | |
| 332 | - | webkit.IterateMainLoop() | |
| 333 | - | } | |
| 276 | + | <-done | |
| 334 | 277 | ||
| 335 | - | case <-timeout.C: | |
| 336 | - | return nil, fmt.Errorf("stack operation timed out") | |
| 337 | - | } | |
| 278 | + | if stackErr != nil { | |
| 279 | + | return nil, stackErr | |
| 338 | 280 | } | |
| 281 | + | ||
| 282 | + | log.Printf("[workspace] Stack operation completed successfully: newNode=%p", newNode) | |
| 283 | + | return newNode, nil | |
| 339 | 284 | } | |
| 340 | 285 | ||
| 341 | 286 | // EnableEnhancedMode enables all enhanced validation features | |
| @@ -383,9 +328,6 @@ func (wm *WorkspaceManager) GetEnhancedStats() map[string]interface{} { | |||
| 383 | 328 | stats["widget_transactions"] = wm.widgetTxManager.GetTransactionStats() | |
| 384 | 329 | } | |
| 385 | 330 | ||
| 386 | - | if wm.concurrencyController != nil { | |
| 387 | - | stats["concurrency"] = wm.concurrencyController.GetConcurrencyStats() | |
| 388 | - | } | |
| 389 | 331 | ||
| 390 | 332 | if wm.treeRebalancer != nil { | |
| 391 | 333 | stats["tree_rebalancing"] = wm.treeRebalancer.GetRebalancingStats() | |
| @@ -432,9 +374,6 @@ func (wm *WorkspaceManager) ValidateWorkspaceIntegrity() error { | |||
| 432 | 374 | func (wm *WorkspaceManager) ShutdownEnhancedComponents() { | |
| 433 | 375 | log.Printf("[workspace] Shutting down enhanced validation components") | |
| 434 | 376 | ||
| 435 | - | if wm.concurrencyController != nil { | |
| 436 | - | wm.concurrencyController.Shutdown() | |
| 437 | - | } | |
| 438 | 377 | ||
| 439 | 378 | // Other components don't require explicit shutdown currently | |
| 440 | 379 | log.Printf("[workspace] Enhanced validation components shutdown complete") | |
| @@ -1,11 +1,9 @@ | |||||
| 1 | - | // workspace_bulletproof_operations.go - Bulletproof wrapper methods for all tree operations | 1 | + | // workspace_operations.go - Workspace operation methods with validation and safety |
| 2 | package browser | 2 | package browser | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | - | "context" | |||
| 6 | "fmt" | 5 | "fmt" | ||
| 7 | "log" | 6 | "log" | ||
| 8 | - | "time" | |||
| 9 | 7 | ||||
| 10 | "github.com/bnema/dumber/pkg/webkit" | 8 | "github.com/bnema/dumber/pkg/webkit" | ||
| 11 | ) | 9 | ) | ||
| @@ -64,58 +62,37 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane | |||||
| 64 | return newNode, nil | 62 | return newNode, nil | ||
| 65 | } | 63 | } | ||
| 66 | 64 | ||||
| 67 | - | // Step 5: Not on main thread, marshal through concurrency controller | 65 | + | // Step 5: Not on main thread, marshal via IdleAdd |
| 68 | - | opReq := &OperationRequest{ | 66 | + | log.Printf("[workspace] Not on main thread, marshalling split via IdleAdd") |
| 69 | - | ID: fmt.Sprintf("split_%p_%d", target, time.Now().UnixNano()), | 67 | + | var newNode *paneNode |
| 70 | - | Type: OpTypeSplit, | 68 | + | var splitErr error |
| 71 | - | TargetNode: target, | 69 | + | done := make(chan struct{}) |
| 72 | - | Direction: direction, | 70 | + | |
| 73 | - | Parameters: map[string]interface{}{ | 71 | + | webkit.IdleAdd(func() bool { |
| 74 | - | "direction": direction, | 72 | + | newNode, splitErr = wm.splitNode(target, direction) |
| 75 | - | }, | 73 | + | if splitErr != nil { |
| 76 | - | Context: context.Background(), | 74 | + | if tombstone != nil { |
| 77 | - | MaxRetries: 3, | 75 | + | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { |
| 78 | - | } | 76 | + | log.Printf("[workspace] Rollback failed after split failure: %v", rollbackErr) |
| 79 | - | ||||
| 80 | - | resultChan := wm.concurrencyController.SubmitOperation(opReq) | |||
| 81 | - | ||||
| 82 | - | log.Printf("[workspace] Waiting for operation result while pumping GTK events") | |||
| 83 | - | ticker := time.NewTicker(10 * time.Millisecond) | |||
| 84 | - | defer ticker.Stop() | |||
| 85 | - | ||||
| 86 | - | timeout := time.NewTimer(30 * time.Second) | |||
| 87 | - | defer timeout.Stop() | |||
| 88 | - | ||||
| 89 | - | for { | |||
| 90 | - | select { | |||
| 91 | - | case result := <-resultChan: | |||
| 92 | - | if !result.Success { | |||
| 93 | - | if tombstone != nil { | |||
| 94 | - | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |||
| 95 | - | log.Printf("[bulletproof] Rollback failed after split failure: %v", rollbackErr) | |||
| 96 | - | } | |||
| 97 | } | 77 | } | ||
| 98 | - | return nil, result.Error | |||
| 99 | } | 78 | } | ||
| 100 | - | 79 | + | } else { | |
| 101 | - | if err := wm.treeValidator.ValidateTree(wm.root, "after_split"); err != nil { | 80 | + | if verr := wm.treeValidator.ValidateTree(wm.root, "after_split"); verr != nil { |
| 102 | - | log.Printf("[workspace] Tree validation failed after split: %v", err) | 81 | + | log.Printf("[workspace] Tree validation failed after split: %v", verr) |
| 103 | } | 82 | } | ||
| 83 | + | } | |||
| 84 | + | close(done) | |||
| 85 | + | return false | |||
| 86 | + | }) | |||
| 104 | 87 | ||||
| 105 | - | // Tree rebalancing is only needed after close promotions. Splits have correct allocation from GTK. | 88 | + | <-done |
| 106 | - | ||||
| 107 | - | log.Printf("[workspace] Split operation completed successfully: newNode=%p", result.NewNode) | |||
| 108 | - | return result.NewNode, nil | |||
| 109 | 89 | ||||
| 110 | - | case <-ticker.C: | 90 | + | if splitErr != nil { |
| 111 | - | if webkit.IsMainThread() { | 91 | + | return nil, splitErr |
| 112 | - | webkit.IterateMainLoop() | |||
| 113 | - | } | |||
| 114 | - | ||||
| 115 | - | case <-timeout.C: | |||
| 116 | - | return nil, fmt.Errorf("split operation timed out") | |||
| 117 | - | } | |||
| 118 | } | 92 | } | ||
| 93 | + | ||||
| 94 | + | log.Printf("[workspace] Split operation completed successfully: newNode=%p", newNode) | |||
| 95 | + | return newNode, nil | |||
| 119 | } | 96 | } | ||
| 120 | 97 | ||||
| 121 | // ClosePane performs a close operation with validation and safety checks | 98 | // ClosePane performs a close operation with validation and safety checks | ||
| @@ -178,64 +155,48 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error { | |||||
| 178 | return nil | 155 | return nil | ||
| 179 | } | 156 | } | ||
| 180 | 157 | ||||
| 181 | - | // Step 5: Not on main thread, marshal through concurrency controller | 158 | + | // Step 5: Not on main thread, marshal via IdleAdd |
| 182 | - | log.Printf("[workspace] Not on main thread, using concurrency controller") | 159 | + | log.Printf("[workspace] Not on main thread, marshalling close via IdleAdd") |
| 183 | - | opReq := &OperationRequest{ | 160 | + | var promoted *paneNode |
| 184 | - | ID: fmt.Sprintf("close_%p_%d", node, time.Now().UnixNano()), | 161 | + | var closeErr error |
| 185 | - | Type: OpTypeClose, | 162 | + | done := make(chan struct{}) |
| 186 | - | TargetNode: node, | 163 | + | |
| 187 | - | Parameters: map[string]interface{}{}, | 164 | + | webkit.IdleAdd(func() bool { |
| 188 | - | Context: context.Background(), | 165 | + | wm.paneCloseLogf("invoking closePane node=%p", node) |
| 189 | - | MaxRetries: 3, | 166 | + | promoted, closeErr = wm.closePane(node) |
| 190 | - | } | 167 | + | if closeErr != nil { |
| 191 | - | 168 | + | wm.paneCloseLogf("closePane failed node=%p err=%v", node, closeErr) | |
| 192 | - | resultChan := wm.concurrencyController.SubmitOperation(opReq) | 169 | + | wm.dumpTreeState("after_close_error") |
| 193 | - | 170 | + | if tombstone != nil { | |
| 194 | - | log.Printf("[workspace] Waiting for operation result while pumping GTK events") | 171 | + | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { |
| 195 | - | ticker := time.NewTicker(10 * time.Millisecond) | 172 | + | log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr) |
| 196 | - | defer ticker.Stop() | |||
| 197 | - | ||||
| 198 | - | timeout := time.NewTimer(30 * time.Second) | |||
| 199 | - | defer timeout.Stop() | |||
| 200 | - | ||||
| 201 | - | for { | |||
| 202 | - | select { | |||
| 203 | - | case result := <-resultChan: | |||
| 204 | - | if !result.Success { | |||
| 205 | - | wm.paneCloseLogf("async close failed node=%p err=%v", node, result.Error) | |||
| 206 | - | wm.dumpTreeState("after_close_error") | |||
| 207 | - | if tombstone != nil { | |||
| 208 | - | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |||
| 209 | - | log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr) | |||
| 210 | - | } | |||
| 211 | } | 173 | } | ||
| 212 | - | return result.Error | |||
| 213 | } | 174 | } | ||
| 214 | - | 175 | + | } else { | |
| 215 | - | if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil { | 176 | + | if verr := wm.treeValidator.ValidateTree(wm.root, "after_close"); verr != nil { |
| 216 | - | log.Printf("[workspace] Tree validation failed after close: %v", err) | 177 | + | log.Printf("[workspace] Tree validation failed after close: %v", verr) |
| 217 | } | 178 | } | ||
| 218 | - | wm.paneCloseLogf("async close succeeded node=%p promoted=%p root=%p", node, result.NewNode, wm.root) | 179 | + | wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root) |
| 219 | wm.dumpTreeState("after_close_success") | 180 | wm.dumpTreeState("after_close_success") | ||
| 220 | 181 | ||||
| 221 | if wm.treeRebalancer != nil { | 182 | if wm.treeRebalancer != nil { | ||
| 222 | - | if err := wm.treeRebalancer.RebalanceAfterClose(node, result.NewNode); err != nil { | 183 | + | if rerr := wm.treeRebalancer.RebalanceAfterClose(node, promoted); rerr != nil { |
| 223 | - | log.Printf("[workspace] Tree rebalancing failed after close: %v", err) | 184 | + | log.Printf("[workspace] Tree rebalancing failed after close: %v", rerr) |
| 224 | } | 185 | } | ||
| 225 | } | 186 | } | ||
| 187 | + | } | |||
| 188 | + | close(done) | |||
| 189 | + | return false | |||
| 190 | + | }) | |||
| 226 | 191 | ||||
| 227 | - | log.Printf("[workspace] Close operation completed successfully") | 192 | + | <-done |
| 228 | - | return nil | |||
| 229 | - | ||||
| 230 | - | case <-ticker.C: | |||
| 231 | - | if webkit.IsMainThread() { | |||
| 232 | - | webkit.IterateMainLoop() | |||
| 233 | - | } | |||
| 234 | 193 | ||||
| 235 | - | case <-timeout.C: | 194 | + | if closeErr != nil { |
| 236 | - | return fmt.Errorf("close operation timed out") | 195 | + | return closeErr |
| 237 | - | } | |||
| 238 | } | 196 | } | ||
| 197 | + | ||||
| 198 | + | log.Printf("[workspace] Close operation completed successfully") | |||
| 199 | + | return nil | |||
| 239 | } | 200 | } | ||
| 240 | 201 | ||||
| 241 | // StackPane performs a stack operation with validation and safety checks | 202 | // StackPane performs a stack operation with validation and safety checks | ||
| @@ -275,7 +236,7 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) { | |||||
| 275 | if err != nil { | 236 | if err != nil { | ||
| 276 | if tombstone != nil { | 237 | if tombstone != nil { | ||
| 277 | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | 238 | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | ||
| 278 | - | log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr) | 239 | + | log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr) |
| 279 | } | 240 | } | ||
| 280 | } | 241 | } | ||
| 281 | return nil, err | 242 | return nil, err | ||
| @@ -289,53 +250,37 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) { | |||||
| 289 | return newNode, nil | 250 | return newNode, nil | ||
| 290 | } | 251 | } | ||
| 291 | 252 | ||||
| 292 | - | // Step 5: Not on main thread, marshal through concurrency controller | 253 | + | // Step 5: Not on main thread, marshal via IdleAdd |
| 293 | - | opReq := &OperationRequest{ | 254 | + | log.Printf("[workspace] Not on main thread, marshalling stack via IdleAdd") |
| 294 | - | ID: fmt.Sprintf("stack_%p_%d", target, time.Now().UnixNano()), | 255 | + | var newNode *paneNode |
| 295 | - | Type: OpTypeStack, | 256 | + | var stackErr error |
| 296 | - | TargetNode: target, | 257 | + | done := make(chan struct{}) |
| 297 | - | Parameters: map[string]interface{}{}, | |||
| 298 | - | Context: context.Background(), | |||
| 299 | - | MaxRetries: 3, | |||
| 300 | - | } | |||
| 301 | - | ||||
| 302 | - | resultChan := wm.concurrencyController.SubmitOperation(opReq) | |||
| 303 | - | ||||
| 304 | - | log.Printf("[workspace] Waiting for operation result while pumping GTK events") | |||
| 305 | - | ticker := time.NewTicker(10 * time.Millisecond) | |||
| 306 | - | defer ticker.Stop() | |||
| 307 | 258 | ||||
| 308 | - | timeout := time.NewTimer(30 * time.Second) | 259 | + | webkit.IdleAdd(func() bool { |
| 309 | - | defer timeout.Stop() | 260 | + | newNode, stackErr = wm.stackedPaneManager.StackPane(target) |
| 310 | - | 261 | + | if stackErr != nil { | |
| 311 | - | for { | 262 | + | if tombstone != nil { |
| 312 | - | select { | 263 | + | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { |
| 313 | - | case result := <-resultChan: | 264 | + | log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr) |
| 314 | - | if !result.Success { | |||
| 315 | - | if tombstone != nil { | |||
| 316 | - | if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil { | |||
| 317 | - | log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr) | |||
| 318 | - | } | |||
| 319 | } | 265 | } | ||
| 320 | - | return nil, result.Error | |||
| 321 | } | 266 | } | ||
| 322 | - | 267 | + | } else { | |
| 323 | - | if err := wm.treeValidator.ValidateTree(wm.root, "after_stack"); err != nil { | 268 | + | if verr := wm.treeValidator.ValidateTree(wm.root, "after_stack"); verr != nil { |
| 324 | - | log.Printf("[workspace] Tree validation failed after stack: %v", err) | 269 | + | log.Printf("[workspace] Tree validation failed after stack: %v", verr) |
| 325 | } | 270 | } | ||
| 271 | + | } | |||
| 272 | + | close(done) | |||
| 273 | + | return false | |||
| 274 | + | }) | |||
| 326 | 275 | ||||
| 327 | - | log.Printf("[workspace] Stack operation completed successfully: newNode=%p", result.NewNode) | 276 | + | <-done |
| 328 | - | return result.NewNode, nil | |||
| 329 | - | ||||
| 330 | - | case <-ticker.C: | |||
| 331 | - | if webkit.IsMainThread() { | |||
| 332 | - | webkit.IterateMainLoop() | |||
| 333 | - | } | |||
| 334 | 277 | ||||
| 335 | - | case <-timeout.C: | 278 | + | if stackErr != nil { |
| 336 | - | return nil, fmt.Errorf("stack operation timed out") | 279 | + | return nil, stackErr |
| 337 | - | } | |||
| 338 | } | 280 | } | ||
| 281 | + | ||||
| 282 | + | log.Printf("[workspace] Stack operation completed successfully: newNode=%p", newNode) | |||
| 283 | + | return newNode, nil | |||
| 339 | } | 284 | } | ||
| 340 | 285 | ||||
| 341 | // EnableEnhancedMode enables all enhanced validation features | 286 | // EnableEnhancedMode enables all enhanced validation features | ||
| @@ -383,9 +328,6 @@ func (wm *WorkspaceManager) GetEnhancedStats() map[string]interface{} { | |||||
| 383 | stats["widget_transactions"] = wm.widgetTxManager.GetTransactionStats() | 328 | stats["widget_transactions"] = wm.widgetTxManager.GetTransactionStats() | ||
| 384 | } | 329 | } | ||
| 385 | 330 | ||||
| 386 | - | if wm.concurrencyController != nil { | |||
| 387 | - | stats["concurrency"] = wm.concurrencyController.GetConcurrencyStats() | |||
| 388 | - | } | |||
| 389 | 331 | ||||
| 390 | if wm.treeRebalancer != nil { | 332 | if wm.treeRebalancer != nil { | ||
| 391 | stats["tree_rebalancing"] = wm.treeRebalancer.GetRebalancingStats() | 333 | stats["tree_rebalancing"] = wm.treeRebalancer.GetRebalancingStats() | ||
| @@ -432,9 +374,6 @@ func (wm *WorkspaceManager) ValidateWorkspaceIntegrity() error { | |||||
| 432 | func (wm *WorkspaceManager) ShutdownEnhancedComponents() { | 374 | func (wm *WorkspaceManager) ShutdownEnhancedComponents() { | ||
| 433 | log.Printf("[workspace] Shutting down enhanced validation components") | 375 | log.Printf("[workspace] Shutting down enhanced validation components") | ||
| 434 | 376 | ||||
| 435 | - | if wm.concurrencyController != nil { | |||
| 436 | - | wm.concurrencyController.Shutdown() | |||
| 437 | - | } | |||
| 438 | 377 | ||||
| 439 | // Other components don't require explicit shutdown currently | 378 | // Other components don't require explicit shutdown currently | ||
| 440 | log.Printf("[workspace] Enhanced validation components shutdown complete") | 379 | log.Printf("[workspace] Enhanced validation components shutdown complete") | ||