forked from phenom/healer-man
Release Healer Man 0.1.56
This commit is contained in:
1 parent
38b3d4d160
commit
a05a79eb83
16 files changed
+388
-116
No files matched your search
+98
-52
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user