()
| 182 | } |
| 183 | |
| 184 | private async processQueue(): Promise<void> { |
| 185 | if (this.isProcessingQueue || this.stopped) { |
| 186 | return; |
| 187 | } |
| 188 | |
| 189 | this.isProcessingQueue = true; |
| 190 | |
| 191 | try { |
| 192 | while (this.queue.length > 0) { |
| 193 | if (this.stopped) { |
| 194 | return; |
| 195 | } |
| 196 | |
| 197 | const next = this.queue.shift(); |
| 198 | if (!next) { |
| 199 | continue; |
| 200 | } |
| 201 | |
| 202 | const { workspaceId, thresholdMs } = next; |
| 203 | this.queuedWorkspaceIds.delete(workspaceId); |
| 204 | this.activeWorkspaceIds.add(workspaceId); |
| 205 | |
| 206 | try { |
| 207 | // Re-check eligibility right before execution to avoid stale queue decisions. |
| 208 | const eligibility = await this.checkEligibility(workspaceId, thresholdMs, Date.now()); |
| 209 | if (!eligibility.eligible) { |
| 210 | log.info("Skipped queued idle compaction because workspace became ineligible", { |
| 211 | workspaceId, |
| 212 | reason: eligibility.reason, |
| 213 | }); |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | log.info("Executing idle compaction", { |
| 218 | workspaceId, |
| 219 | idleHours: thresholdMs / HOURS_TO_MS, |
| 220 | remainingQueued: this.queue.length, |
| 221 | }); |
| 222 | |
| 223 | await this.executeIdleCompaction(workspaceId); |
| 224 | } catch (error) { |
| 225 | log.error("Idle compaction execution failed", { workspaceId, error }); |
| 226 | } finally { |
| 227 | this.activeWorkspaceIds.delete(workspaceId); |
| 228 | } |
| 229 | } |
| 230 | } finally { |
| 231 | this.isProcessingQueue = false; |
| 232 | |
| 233 | // If work arrived after we exited the loop and service is still running, |
| 234 | // kick processing again. |
| 235 | if (!this.stopped && this.queue.length > 0) { |
| 236 | void this.processQueue(); |
| 237 | } |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | /** |
no test coverage detected