()
| 408 | } |
| 409 | |
| 410 | func (m *OpCache) GetStatus() (map[string]string, map[string]string) { |
| 411 | taskTypes := map[string]string{} |
| 412 | processingModelsData := map[string]string{} |
| 413 | |
| 414 | // Iterate a snapshot (Keys() copies) and build a fresh result map. We must |
| 415 | // NOT delete from m.Map() during the range: Map() returns the live internal |
| 416 | // map by reference, so a bare delete here would be an unsynchronized write |
| 417 | // to a map four HTTP handlers read every ~1s — a concurrent-map-write crash. |
| 418 | // Collect evictions and apply them via the locked DeleteUUID after the loop. |
| 419 | var evict []string |
| 420 | for _, k := range m.status.Keys() { |
| 421 | v := m.status.Get(k) |
| 422 | if v == "" { |
| 423 | continue // raced with a concurrent Delete |
| 424 | } |
| 425 | status := m.galleryService.GetStatus(v) |
| 426 | // Terminal ops must not keep showing as "processing". Cleanup was |
| 427 | // previously only triggered by a client polling /api/backends/job/:uid, |
| 428 | // but the Manage-page Reinstall/Upgrade buttons never poll, so completed |
| 429 | // ops leaked into processingBackends forever and the card spun |
| 430 | // "reinstalling" indefinitely. Evict here on the list read (the UI always |
| 431 | // calls this). DeleteUUID broadcasts the eviction so peer replicas converge. |
| 432 | // |
| 433 | // We evict ONLY a clean success (progress 100 + "completed", matching the |
| 434 | // job-poll's historical delete condition) or a cancellation. Deliberately |
| 435 | // NOT evicted: |
| 436 | // - failed ops (Error != nil): kept so /api/operations can surface the |
| 437 | // error and offer Dismiss. |
| 438 | // - the ErrWorkerStillInstalling soft-path (Processed=true, Error=nil, |
| 439 | // progress != 100): the worker is still installing in the background |
| 440 | // and the reconciler confirms the real outcome later — evicting it |
| 441 | // would hide an install that may still fail. |
| 442 | if status != nil && status.Processed && |
| 443 | ((status.Progress == 100 && status.Message == "completed") || status.Cancelled) { |
| 444 | evict = append(evict, v) |
| 445 | continue |
| 446 | } |
| 447 | processingModelsData[k] = v |
| 448 | taskTypes[k] = "Installation" |
| 449 | if status != nil && status.Deletion { |
| 450 | taskTypes[k] = "Deletion" |
| 451 | } else if status == nil { |
| 452 | taskTypes[k] = "Waiting" |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | for _, v := range evict { |
| 457 | m.DeleteUUID(v) |
| 458 | } |
| 459 | |
| 460 | return processingModelsData, taskTypes |
| 461 | } |
| 462 | |
| 463 | // NodeScopedKeyPrefix is the opcache key prefix used by InstallBackendOnNodeEndpoint |
| 464 | // so per-node installs do not collide on the bare backend name. Format: |
nothing calls this directly
no test coverage detected