Sprint 65: Parallel Pair Upgrades and Queue Optimization — Steps 879-888

Implements priority-aware pair-upgrade queue with starvation prevention:
- PairUpgradeQueue: enqueue/dequeue/getActive sorted by priority (step 879)
- UpgradePriorityPolicy: critical/revenue/coverage/experimental classes (step 880)
- ParallelDispatchOptimizer: schedule N pairs with blocked-pair constraints (step 881)
- StarvationPrevention: detect and promote starved pairs (step 882)
- QueueHealthScorer: stall-rate-based health scoring (step 883)
- RetryBackoffStrategy: exponential backoff with max-attempts guard (step 884)
- whetstone_enqueue_pair_upgrade MCP tool (step 885)
- whetstone_get_upgrade_queue MCP tool with priority sorting (step 886)
- UpgradeQueueObservabilityPanel: snapshot model for queue visibility (step 887)
- Sprint65IntegrationSummary (step 888)

All 10 steps passing (94 tests). Routing rule: critical-tier regressions
preempt all experimental upgrades.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-22 18:58:04 -07:00
parent 484d9e3f8c
commit 0282e7ebb7
22 changed files with 932 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
#pragma once
// Step 879: Pair-upgrade queue model.
#include <string>
#include <vector>
#include <algorithm>
#include <nlohmann/json.hpp>
struct UpgradeQueueEntry {
std::string entryId;
std::string pairId;
std::string priority; // "critical","revenue","coverage","experimental"
int priorityLevel = 0; // higher = more urgent
bool active = true;
};
class PairUpgradeQueue {
public:
bool enqueue(const UpgradeQueueEntry& e, std::string* error = nullptr) {
if (e.pairId.empty()) { if (error) *error = "pair_id_missing"; return false; }
if (e.entryId.empty()) { if (error) *error = "entry_id_missing"; return false; }
entries_.push_back(e);
return true;
}
bool dequeue(const std::string& entryId) {
for (auto& e : entries_)
if (e.entryId == entryId && e.active) { e.active = false; return true; }
return false;
}
int activeCount() const {
int n = 0; for (const auto& e : entries_) if (e.active) ++n; return n;
}
std::vector<UpgradeQueueEntry> getActive() const {
std::vector<UpgradeQueueEntry> res;
for (const auto& e : entries_) if (e.active) res.push_back(e);
std::stable_sort(res.begin(), res.end(),
[](const UpgradeQueueEntry& a, const UpgradeQueueEntry& b){
return a.priorityLevel > b.priorityLevel; });
return res;
}
static nlohmann::json toJson(const UpgradeQueueEntry& e) {
return {{"entry_id", e.entryId}, {"pair_id", e.pairId},
{"priority", e.priority}, {"priority_level", e.priorityLevel},
{"active", e.active}};
}
private:
std::vector<UpgradeQueueEntry> entries_;
};

View File

@@ -0,0 +1,48 @@
#pragma once
// Step 881: Parallel dispatch optimizer with dependency constraints.
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
struct DispatchSlot {
std::string slotId;
std::string pairId;
bool scheduled = false;
};
struct DispatchPlan {
std::vector<DispatchSlot> slots;
int maxParallelism = 0;
int scheduledCount = 0;
};
class ParallelDispatchOptimizer {
public:
static DispatchPlan optimize(const std::vector<std::string>& pairIds,
int maxParallelism,
const std::vector<std::string>& blockedPairs = {}) {
DispatchPlan plan;
plan.maxParallelism = maxParallelism;
int idx = 0;
for (const auto& pid : pairIds) {
bool blocked = false;
for (const auto& b : blockedPairs) if (b == pid) { blocked = true; break; }
DispatchSlot slot;
slot.slotId = "slot-" + std::to_string(++idx);
slot.pairId = pid;
slot.scheduled = !blocked && plan.scheduledCount < maxParallelism;
if (slot.scheduled) ++plan.scheduledCount;
plan.slots.push_back(slot);
}
return plan;
}
static nlohmann::json toJson(const DispatchPlan& p) {
nlohmann::json arr = nlohmann::json::array();
for (const auto& s : p.slots)
arr.push_back({{"slot_id", s.slotId}, {"pair_id", s.pairId},
{"scheduled", s.scheduled}});
return {{"max_parallelism", p.maxParallelism},
{"scheduled_count", p.scheduledCount}, {"slots", arr}};
}
};

View File

@@ -0,0 +1,38 @@
#pragma once
// Step 883: Queue health scoring model.
#include <string>
#include <nlohmann/json.hpp>
struct QueueHealthScore {
std::string queueId;
float score = 0.0f; // 0.0 - 1.0
std::string label; // "healthy", "degraded", "critical"
int activeCount = 0;
int stalledCount = 0;
};
class QueueHealthScorer {
public:
static QueueHealthScore score(const std::string& queueId,
int activeCount, int stalledCount,
int totalCapacity) {
QueueHealthScore h;
h.queueId = queueId;
h.activeCount = activeCount;
h.stalledCount = stalledCount;
float stallRate = (activeCount + stalledCount) > 0
? static_cast<float>(stalledCount) / (activeCount + stalledCount) : 0.0f;
float utilRate = totalCapacity > 0
? static_cast<float>(activeCount) / totalCapacity : 0.0f;
h.score = std::max(0.0f, 1.0f - stallRate * 0.7f - (utilRate > 0.9f ? 0.3f : 0.0f));
if (h.score >= 0.7f) h.label = "healthy";
else if (h.score >= 0.4f) h.label = "degraded";
else h.label = "critical";
return h;
}
static nlohmann::json toJson(const QueueHealthScore& h) {
return {{"queue_id", h.queueId}, {"score", h.score}, {"label", h.label},
{"active_count", h.activeCount}, {"stalled_count", h.stalledCount}};
}
};

View File

@@ -0,0 +1,40 @@
#pragma once
// Step 884: Retry/backoff strategy for unstable pairs.
#include <string>
#include <nlohmann/json.hpp>
struct RetryDecision {
std::string pairId;
int attemptNumber = 0;
int backoffMs = 0;
bool shouldRetry = false;
std::string reason;
};
class RetryBackoffStrategy {
public:
static RetryDecision decide(const std::string& pairId, int attemptNumber,
int maxAttempts = 3, int baseBackoffMs = 100) {
RetryDecision d;
d.pairId = pairId;
d.attemptNumber = attemptNumber;
d.shouldRetry = attemptNumber < maxAttempts;
if (d.shouldRetry) {
// Exponential backoff: base * 2^attempt
int exp = 1;
for (int i = 0; i < attemptNumber; ++i) exp *= 2;
d.backoffMs = baseBackoffMs * exp;
d.reason = "will_retry";
} else {
d.backoffMs = 0;
d.reason = "max_attempts_reached";
}
return d;
}
static nlohmann::json toJson(const RetryDecision& d) {
return {{"pair_id", d.pairId}, {"attempt_number", d.attemptNumber},
{"backoff_ms", d.backoffMs}, {"should_retry", d.shouldRetry},
{"reason", d.reason}};
}
};

View File

@@ -0,0 +1,34 @@
#pragma once
// Step 882: Starvation prevention mechanism.
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
struct StarvationRiskEntry {
std::string pairId;
int waitCycles = 0;
bool atRisk = false;
bool promoted = false;
};
class StarvationPrevention {
public:
static StarvationRiskEntry assess(const std::string& pairId, int waitCycles,
int starvationThreshold = 5) {
StarvationRiskEntry e;
e.pairId = pairId;
e.waitCycles = waitCycles;
e.atRisk = waitCycles >= starvationThreshold;
e.promoted = e.atRisk;
return e;
}
static int countAtRisk(const std::vector<StarvationRiskEntry>& entries) {
int n = 0; for (const auto& e : entries) if (e.atRisk) ++n; return n;
}
static nlohmann::json toJson(const StarvationRiskEntry& e) {
return {{"pair_id", e.pairId}, {"wait_cycles", e.waitCycles},
{"at_risk", e.atRisk}, {"promoted", e.promoted}};
}
};

View File

@@ -0,0 +1,41 @@
#pragma once
// Step 880: Priority policy for pair upgrades.
#include <string>
#include <nlohmann/json.hpp>
struct PriorityPolicyResult {
std::string pairId;
std::string priorityClass; // "critical","revenue","coverage","experimental"
int priorityLevel = 0; // critical=4, revenue=3, coverage=2, experimental=1
bool preemptsExperimental = false;
};
class UpgradePriorityPolicy {
public:
static PriorityPolicyResult classify(const std::string& pairId,
const std::string& requestedClass) {
PriorityPolicyResult r;
r.pairId = pairId;
if (requestedClass == "critical") {
r.priorityClass = "critical"; r.priorityLevel = 4; r.preemptsExperimental = true;
} else if (requestedClass == "revenue") {
r.priorityClass = "revenue"; r.priorityLevel = 3; r.preemptsExperimental = false;
} else if (requestedClass == "coverage") {
r.priorityClass = "coverage"; r.priorityLevel = 2; r.preemptsExperimental = false;
} else {
r.priorityClass = "experimental"; r.priorityLevel = 1; r.preemptsExperimental = false;
}
return r;
}
static bool validate(const std::string& priorityClass) {
return priorityClass == "critical" || priorityClass == "revenue" ||
priorityClass == "coverage" || priorityClass == "experimental";
}
static nlohmann::json toJson(const PriorityPolicyResult& r) {
return {{"pair_id", r.pairId}, {"priority_class", r.priorityClass},
{"priority_level", r.priorityLevel},
{"preempts_experimental", r.preemptsExperimental}};
}
};

View File

@@ -0,0 +1,45 @@
#pragma once
// Step 887: Upgrade queue observability panel model.
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
struct QueuePanelSnapshot {
std::string snapshotId;
int totalQueued = 0;
int totalActive = 0;
int totalStalled = 0;
int totalCompleted = 0;
float healthScore = 0.0f;
std::string healthLabel;
};
class UpgradeQueueObservabilityPanel {
public:
static QueuePanelSnapshot snapshot(const std::string& snapshotId,
int queued, int active,
int stalled, int completed,
float healthScore) {
QueuePanelSnapshot s;
s.snapshotId = snapshotId;
s.totalQueued = queued;
s.totalActive = active;
s.totalStalled = stalled;
s.totalCompleted = completed;
s.healthScore = healthScore;
if (healthScore >= 0.7f) s.healthLabel = "healthy";
else if (healthScore >= 0.4f) s.healthLabel = "degraded";
else s.healthLabel = "critical";
return s;
}
static nlohmann::json toJson(const QueuePanelSnapshot& s) {
return {{"snapshot_id", s.snapshotId},
{"total_queued", s.totalQueued},
{"total_active", s.totalActive},
{"total_stalled", s.totalStalled},
{"total_completed", s.totalCompleted},
{"health_score", s.healthScore},
{"health_label", s.healthLabel}};
}
};