(s string, op *OpStatus)
| 131 | } |
| 132 | |
| 133 | func (g *GalleryService) UpdateStatus(s string, op *OpStatus) { |
| 134 | g.Lock() |
| 135 | // Preserve any per-node entries already accumulated by UpdateNodeProgress: |
| 136 | // the legacy progressCb path (used by the Phase 2 install bridge) calls |
| 137 | // UpdateStatus with a fresh *OpStatus on every tick, which would otherwise |
| 138 | // wipe the Nodes slice and leave the UI flickering between one node and |
| 139 | // another. If the caller explicitly populates Nodes on the incoming op, |
| 140 | // that wins; an empty Nodes slice on the incoming op is treated as "no |
| 141 | // new per-node data" and the previous Nodes are carried forward. |
| 142 | if op != nil && len(op.Nodes) == 0 { |
| 143 | if prev := g.statuses[s]; prev != nil && len(prev.Nodes) > 0 { |
| 144 | op.Nodes = prev.Nodes |
| 145 | } |
| 146 | } |
| 147 | g.statuses[s] = op |
| 148 | store := g.galleryStore |
| 149 | nc := g.natsClient |
| 150 | g.Unlock() |
| 151 | |
| 152 | // I/O happens after Unlock. The NATS broadcast loops back into our own |
| 153 | // wildcard subscriber (mergeStatus), which would deadlock on this mutex |
| 154 | // if we still held it. Holding the lock across a PostgreSQL round-trip |
| 155 | // would also stall every concurrent reader on each progress tick. |
| 156 | if store != nil && op != nil { |
| 157 | if op.Processed { |
| 158 | status, errMsg := "completed", "" |
| 159 | if op.Error != nil { |
| 160 | status = "failed" |
| 161 | errMsg = op.Error.Error() |
| 162 | } |
| 163 | if op.Cancelled { |
| 164 | status = "cancelled" |
| 165 | } |
| 166 | if err := store.UpdateStatus(s, status, errMsg); err != nil { |
| 167 | xlog.Warn("Failed to persist gallery operation status", "op_id", s, "error", err) |
| 168 | } |
| 169 | } else { |
| 170 | if err := store.UpdateProgress(s, op.Progress, op.Message, op.DownloadedFileSize, op.Cancellable); err != nil { |
| 171 | xlog.Warn("Failed to persist gallery operation progress", "op_id", s, "error", err) |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | // Publish progress to NATS in distributed mode. The payload wraps the |
| 177 | // OpStatus with the opID so peer replicas reading the wildcard subject |
| 178 | // don't need to parse it back out of the NATS subject string. |
| 179 | if nc != nil { |
| 180 | if err := nc.Publish(messaging.SubjectGalleryProgress(s), GalleryProgressEvent{ |
| 181 | JobID: s, |
| 182 | Status: op, |
| 183 | }); err != nil { |
| 184 | xlog.Warn("Failed to broadcast gallery progress", "op_id", s, "error", err) |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | // publishCacheInvalidate broadcasts a cache invalidation event so peer |
| 190 | // replicas refresh whatever in-memory state mirrors disk. No-op when |
no test coverage detected