feat: add sub2api sentinel trust cadence
This commit is contained in:
@@ -32,6 +32,8 @@ export interface CodexPoolSentinelConfig {
|
||||
cadence: {
|
||||
successInitialIntervalMinutes: number;
|
||||
successMaxIntervalMinutes: number;
|
||||
trustedSuccessMaxIntervalMinutes: number;
|
||||
untrustedSuccessMaxIntervalMinutes: number;
|
||||
successBackoffMultiplier: number;
|
||||
jitterPercent: number;
|
||||
};
|
||||
@@ -61,6 +63,7 @@ export interface CodexPoolSentinelProfileSecret {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
upstreamUserAgent: string | null;
|
||||
trustUpstream: boolean;
|
||||
}
|
||||
|
||||
export interface CodexPoolSentinelManifestOptions {
|
||||
@@ -103,6 +106,8 @@ export function defaultCodexPoolSentinelConfig(): CodexPoolSentinelConfig {
|
||||
cadence: {
|
||||
successInitialIntervalMinutes: 1,
|
||||
successMaxIntervalMinutes: 20,
|
||||
trustedSuccessMaxIntervalMinutes: 20,
|
||||
untrustedSuccessMaxIntervalMinutes: 2,
|
||||
successBackoffMultiplier: 2,
|
||||
jitterPercent: 10,
|
||||
},
|
||||
@@ -145,6 +150,9 @@ export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolS
|
||||
const cadence = isRecord(value.cadence) ? value.cadence : {};
|
||||
const freeze = isRecord(value.freeze) ? value.freeze : {};
|
||||
const pricing = isRecord(value.pricing) ? value.pricing : {};
|
||||
const legacySuccessMax = readInt(valueAt(cadence, "successMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.successMaxIntervalMinutes`, defaults.cadence.successMaxIntervalMinutes, 1, 1440);
|
||||
const trustedSuccessMax = readInt(valueAt(cadence, "trustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.trustedSuccessMaxIntervalMinutes`, legacySuccessMax, 1, 1440);
|
||||
const untrustedSuccessMax = readInt(valueAt(cadence, "untrustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.untrustedSuccessMaxIntervalMinutes`, legacySuccessMax, 1, 1440);
|
||||
const config: CodexPoolSentinelConfig = {
|
||||
monitor: {
|
||||
enabled: readBoolean(valueAt(monitor, "enabled"), `${sourcePath}.sentinel.monitor.enabled`, defaults.monitor.enabled),
|
||||
@@ -176,7 +184,9 @@ export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolS
|
||||
},
|
||||
cadence: {
|
||||
successInitialIntervalMinutes: readInt(valueAt(cadence, "successInitialIntervalMinutes"), `${sourcePath}.sentinel.cadence.successInitialIntervalMinutes`, defaults.cadence.successInitialIntervalMinutes, 1, 1440),
|
||||
successMaxIntervalMinutes: readInt(valueAt(cadence, "successMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.successMaxIntervalMinutes`, defaults.cadence.successMaxIntervalMinutes, 1, 1440),
|
||||
successMaxIntervalMinutes: legacySuccessMax,
|
||||
trustedSuccessMaxIntervalMinutes: trustedSuccessMax,
|
||||
untrustedSuccessMaxIntervalMinutes: untrustedSuccessMax,
|
||||
successBackoffMultiplier: readInt(valueAt(cadence, "successBackoffMultiplier"), `${sourcePath}.sentinel.cadence.successBackoffMultiplier`, defaults.cadence.successBackoffMultiplier, 1, 10),
|
||||
jitterPercent: readInt(valueAt(cadence, "jitterPercent"), `${sourcePath}.sentinel.cadence.jitterPercent`, defaults.cadence.jitterPercent, 0, 50),
|
||||
},
|
||||
@@ -198,6 +208,12 @@ export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolS
|
||||
if (config.cadence.successMaxIntervalMinutes < config.cadence.successInitialIntervalMinutes) {
|
||||
throw new Error(`${sourcePath}.sentinel.cadence.successMaxIntervalMinutes must be >= successInitialIntervalMinutes`);
|
||||
}
|
||||
if (config.cadence.trustedSuccessMaxIntervalMinutes < config.cadence.successInitialIntervalMinutes) {
|
||||
throw new Error(`${sourcePath}.sentinel.cadence.trustedSuccessMaxIntervalMinutes must be >= successInitialIntervalMinutes`);
|
||||
}
|
||||
if (config.cadence.untrustedSuccessMaxIntervalMinutes < config.cadence.successInitialIntervalMinutes) {
|
||||
throw new Error(`${sourcePath}.sentinel.cadence.untrustedSuccessMaxIntervalMinutes must be >= successInitialIntervalMinutes`);
|
||||
}
|
||||
if (config.freeze.maxTtlMinutes < config.freeze.initialTtlMinutes) {
|
||||
throw new Error(`${sourcePath}.sentinel.freeze.maxTtlMinutes must be >= initialTtlMinutes`);
|
||||
}
|
||||
@@ -1032,6 +1048,7 @@ def probe_account(profile, config, purpose):
|
||||
return {
|
||||
"accountName": profile["accountName"],
|
||||
"profile": profile.get("profile"),
|
||||
"trustUpstream": profile.get("trustUpstream") is True,
|
||||
"purpose": purpose,
|
||||
"ok": ok,
|
||||
"markerMatched": marker_matched,
|
||||
@@ -1115,11 +1132,17 @@ def choose_forced_profiles(profiles, state, config, now, names):
|
||||
missing = sorted(name for name in names if name not in set(found))
|
||||
return due, {"selected": len(due), "due": len(due), "limit": "forced-accounts", "budgetMode": "record-only", "ledger": ledger_for(state, now)[1], "requestedAccounts": sorted(names), "missingAccounts": missing}
|
||||
|
||||
def next_success_interval(account_state, config):
|
||||
def success_max_interval(profile, config):
|
||||
cadence = config["cadence"]
|
||||
if profile.get("trustUpstream") is True:
|
||||
return int(cadence.get("trustedSuccessMaxIntervalMinutes") or cadence.get("successMaxIntervalMinutes"))
|
||||
return int(cadence.get("untrustedSuccessMaxIntervalMinutes") or cadence.get("successMaxIntervalMinutes"))
|
||||
|
||||
def next_success_interval(account_state, config, profile):
|
||||
streak = int(account_state.get("successStreak") or 0)
|
||||
previous = int(account_state.get("successIntervalMinutes") or 0)
|
||||
initial = int(config["cadence"]["successInitialIntervalMinutes"])
|
||||
maximum = int(config["cadence"]["successMaxIntervalMinutes"])
|
||||
maximum = success_max_interval(profile, config)
|
||||
multiplier = int(config["cadence"]["successBackoffMultiplier"])
|
||||
return initial if streak <= 0 or previous <= 0 else min(maximum, max(initial, previous * multiplier))
|
||||
|
||||
@@ -1133,7 +1156,7 @@ def next_freeze_interval(account_state, config, was_recovery):
|
||||
return min(maximum, max(initial, previous * multiplier))
|
||||
return initial
|
||||
|
||||
def apply_result(result, state, config, now, admin):
|
||||
def apply_result(result, state, config, now, admin, profile):
|
||||
name = result["accountName"]
|
||||
account_state = state.setdefault("accounts", {}).setdefault(name, {})
|
||||
add_usage(state, account_state, now, result.get("usage") or {})
|
||||
@@ -1154,9 +1177,10 @@ def apply_result(result, state, config, now, admin):
|
||||
account_state["qualityGate"] = {**quality_gate, "pending": False, "clearedAt": iso(now)}
|
||||
account_state["successStreak"] = 0
|
||||
account_state["successIntervalMinutes"] = 0
|
||||
interval = next_success_interval(account_state, config)
|
||||
interval = next_success_interval(account_state, config, profile)
|
||||
account_state["successStreak"] = int(account_state.get("successStreak") or 0) + 1
|
||||
account_state["successIntervalMinutes"] = interval
|
||||
account_state["successMaxIntervalMinutes"] = success_max_interval(profile, config)
|
||||
account_state["nextProbeAfter"] = iso(add_minutes(now, interval, int(config["cadence"]["jitterPercent"])))
|
||||
account_state["lastOkAt"] = iso(now)
|
||||
account_state["lastStatus"] = "ok"
|
||||
@@ -1190,6 +1214,7 @@ def apply_result(result, state, config, now, admin):
|
||||
account_state["nextProbeAfter"] = iso(until)
|
||||
account_state["successStreak"] = 0
|
||||
account_state["successIntervalMinutes"] = 0
|
||||
account_state["successMaxIntervalMinutes"] = success_max_interval(profile, config)
|
||||
account_state["lastStatus"] = "quarantined"
|
||||
else:
|
||||
retry = int(config["probe"]["transportRetryMinutes"])
|
||||
@@ -1197,9 +1222,12 @@ def apply_result(result, state, config, now, admin):
|
||||
account_state["lastStatus"] = "marker-not-matched-no-freeze"
|
||||
account_state["lastFailureAt"] = iso(now)
|
||||
account_state["lastProbeAt"] = iso(now)
|
||||
account_state["trustUpstream"] = profile.get("trustUpstream") is True
|
||||
account_state["lastProbe"] = {
|
||||
"ok": result.get("ok"),
|
||||
"purpose": result.get("purpose"),
|
||||
"trustUpstream": result.get("trustUpstream"),
|
||||
"successMaxIntervalMinutes": success_max_interval(profile, config),
|
||||
"httpStatus": result.get("httpStatus"),
|
||||
"durationMs": result.get("durationMs"),
|
||||
"markerMatched": result.get("markerMatched"),
|
||||
@@ -1263,11 +1291,12 @@ def main():
|
||||
actions = []
|
||||
if (config["monitor"]["enabled"] or forced_names) and due:
|
||||
with ThreadPoolExecutor(max_workers=max(1, len(due))) as executor:
|
||||
futures = [executor.submit(probe_account, item["profile"], config, item["purpose"]) for item in due]
|
||||
futures = {executor.submit(probe_account, item["profile"], config, item["purpose"]): item["profile"] for item in due}
|
||||
for future in as_completed(futures):
|
||||
result = future.result()
|
||||
results.append(result)
|
||||
actions.append({"accountName": result["accountName"], **apply_result(result, state, config, now, admin)})
|
||||
profile = futures[future]
|
||||
actions.append({"accountName": result["accountName"], **apply_result(result, state, config, now, admin, profile)})
|
||||
history = state.setdefault("history", [])
|
||||
run_summary = {
|
||||
"at": iso(now),
|
||||
@@ -1293,6 +1322,7 @@ def main():
|
||||
"results": [{
|
||||
"accountName": item.get("accountName"),
|
||||
"purpose": item.get("purpose"),
|
||||
"trustUpstream": item.get("trustUpstream"),
|
||||
"ok": item.get("ok"),
|
||||
"markerMatched": item.get("markerMatched"),
|
||||
"httpStatus": item.get("httpStatus"),
|
||||
|
||||
Reference in New Issue
Block a user