Release Healer Man 0.1.56

This commit is contained in:
phenom committed 2026-09-19 13:16:17 -04:00
1 parent 38b3d4d160
commit a05a79eb83
16 files changed
+388 -116

No files matched your search

+98 -52
View File
@@ -17,6 +17,7 @@ const SOURCES = new Set(["wow", "coa", "rom", "all"]);
const BOSS_CATEGORIES = new Set(["all", "wow", "rom"]);
const SPEC_SELECTIONS = new Set(["random", "all", "specific"]);
const JOB_STATUSES = new Set(["queued", "running", "complete", "failed"]);
const MAX_BATCH_JOBS = 80;
const JOB_METADATA_NAME = "job.json";
const JOB_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const ARTIFACTS = new Map([
@@ -103,6 +104,18 @@ export function normalizeBalanceLabJobSpec(input) {
return spec;
}
export function normalizeBalanceLabJobBatch(input) {
const rawSpecs = Array.isArray(input?.specs) ? input.specs : [];
if (rawSpecs.length < 1 || rawSpecs.length > MAX_BATCH_JOBS) {
throw new GameDatabaseError(
`A Balance Lab batch must contain 1 through ${MAX_BATCH_JOBS} level jobs.`,
400,
"invalid_balance_lab_batch",
);
}
return Object.freeze(rawSpecs.map((spec) => normalizeBalanceLabJobSpec(spec)));
}
export function balanceLabCommand(spec, outputDir, options = {}) {
const projectRoot = path.resolve(options.projectRoot ?? fileURLToPath(new URL("..", import.meta.url)));
const executable = String(options.pythonExecutable
@@ -218,14 +231,14 @@ export function createBalanceLabJobService(options = {}) {
const projectRoot = path.resolve(options.projectRoot ?? fileURLToPath(new URL("..", import.meta.url)));
const dataDir = path.resolve(options.dataDir ?? process.env.DATA_DIR ?? "data/runtime");
const jobsDir = path.join(dataDir, "balance-lab", "jobs");
const configuredConcurrency = Number(options.maxConcurrent ?? process.env.BALANCE_LAB_MAX_CONCURRENT ?? 1);
const configuredConcurrency = Number(options.maxConcurrent ?? process.env.BALANCE_LAB_MAX_CONCURRENT ?? 4);
const maxConcurrent = Number.isFinite(configuredConcurrency) && configuredConcurrency >= 1
? Math.min(4, Math.floor(configuredConcurrency))
: 1;
const configuredQueueLimit = Number(options.maxQueued ?? process.env.BALANCE_LAB_MAX_QUEUED ?? 8);
? Math.min(16, Math.floor(configuredConcurrency))
: 4;
const configuredQueueLimit = Number(options.maxQueued ?? process.env.BALANCE_LAB_MAX_QUEUED ?? 100);
const maxQueued = Number.isFinite(configuredQueueLimit) && configuredQueueLimit >= 1
? Math.min(50, Math.floor(configuredQueueLimit))
: 8;
? Math.min(200, Math.floor(configuredQueueLimit))
: 100;
const launch = options.spawnProcess ?? spawn;
const pythonExecutable = String(options.pythonExecutable
?? process.env.BALANCE_LAB_PYTHON
@@ -239,8 +252,14 @@ export function createBalanceLabJobService(options = {}) {
const children = new Map();
let running = 0;
let closed = false;
let inspectedRuntime = null;
mkdirSync(jobsDir, { recursive: true });
function currentRuntime() {
inspectedRuntime ??= runtimeProbe();
return inspectedRuntime;
}
function persistJob(job) {
const record = publicJob(job);
writeFileSync(path.join(job.outputDir, JOB_METADATA_NAME), `${JSON.stringify(record, null, 2)}\n`, "utf8");
@@ -414,57 +433,78 @@ export function createBalanceLabJobService(options = {}) {
return job;
}
function requireAvailableRuntime() {
if (closed) throw new GameDatabaseError("Balance Lab runner is shutting down.", 503, "balance_lab_unavailable");
const runtime = currentRuntime();
if (!runtime.available) {
throw new GameDatabaseError(
`${runtime.error} Install Python 3 in the server container and set BALANCE_LAB_PYTHON=python3, then restart the app.`,
503,
"balance_lab_runtime_unavailable",
);
}
}
function requireQueueCapacity(count) {
const pending = [...jobs.values()].filter((job) => job.status === "queued" || job.status === "running").length;
if (pending + count > maxQueued) {
throw new GameDatabaseError(
`The Balance Lab server queue is full for this request; it has room for ${Math.max(0, maxQueued - pending)} more jobs. Wait for running jobs to finish or submit a smaller level batch.`,
429,
"balance_lab_queue_full",
);
}
}
function queueJob(spec, account) {
const id = randomUUID();
const outputDir = path.join(jobsDir, id);
mkdirSync(outputDir, { recursive: true });
const totalParties = expectedPartyConfigurations(spec);
const job = {
id,
status: "queued",
createdAt: new Date().toISOString(),
startedAt: null,
completedAt: null,
requestedBy: account.username,
spec,
progress: {
party: 0,
partiesCompleted: 0,
partiesTotal: totalParties,
simulationsCompleted: 0,
simulationsTotal: 0,
},
artifacts: [],
error: null,
logTail: [],
summary: null,
outputDir,
};
jobs.set(id, job);
persistJob(job);
queue.push(job);
return job;
}
return {
start(input, account) {
if (closed) throw new GameDatabaseError("Balance Lab runner is shutting down.", 503, "balance_lab_unavailable");
const runtime = runtimeProbe();
if (!runtime.available) {
throw new GameDatabaseError(
`${runtime.error} Install Python 3 in the server container and set BALANCE_LAB_PYTHON=python3, then restart the app.`,
503,
"balance_lab_runtime_unavailable",
);
}
const pending = [...jobs.values()].filter((job) => job.status === "queued" || job.status === "running").length;
if (pending >= maxQueued) {
throw new GameDatabaseError(
"The Balance Lab server queue is full. Wait for a running job to finish.",
429,
"balance_lab_queue_full",
);
}
const spec = normalizeBalanceLabJobSpec(input);
const id = randomUUID();
const outputDir = path.join(jobsDir, id);
mkdirSync(outputDir, { recursive: true });
const totalParties = expectedPartyConfigurations(spec);
const job = {
id,
status: "queued",
createdAt: new Date().toISOString(),
startedAt: null,
completedAt: null,
requestedBy: account.username,
spec,
progress: {
party: 0,
partiesCompleted: 0,
partiesTotal: totalParties,
simulationsCompleted: 0,
simulationsTotal: 0,
},
artifacts: [],
error: null,
logTail: [],
summary: null,
outputDir,
};
jobs.set(id, job);
persistJob(job);
queue.push(job);
requireAvailableRuntime();
requireQueueCapacity(1);
const job = queueJob(spec, account);
pump();
return publicJob(job);
},
startMany(input, account) {
const specs = normalizeBalanceLabJobBatch(input);
requireAvailableRuntime();
requireQueueCapacity(specs.length);
const batch = specs.map((spec) => queueJob(spec, account));
pump();
return batch.map(publicJob);
},
list() {
return [...jobs.values()]
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
@@ -472,7 +512,13 @@ export function createBalanceLabJobService(options = {}) {
.map(publicJob);
},
runtime() {
return { ...runtimeProbe() };
return {
...currentRuntime(),
maxConcurrent,
running,
queued: queue.length,
maxQueued,
};
},
get(id) {
return publicJob(requireJob(id));