feat(pool): no machine-wide width - 128 passes per available account

Jorijn: "there is no need to do a global cap anymore, the cap should be
128 per available account". `--concurrency` no longer defaults to 128:
the limits are each OAuth account's 128 (and the local login's), every
driver's counted, an API seat's own REVIEW_GATES_API_CONCURRENCY, and
free memory. `--concurrency N` still caps the machine when given; a value
below 1 is a usage error.

The dump's args.concurrency is null then. The board's driver settings
read `128 per account`, which every driver's `seats` shows as that
driver's width, and the `seats` header says a width is that driver's cap.
The Scheduler test runs 300 passes on two accounts: 256 at once, 128 each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 17:33:11 +01:00
commit 61ee4602f4
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
12 changed files with 98 additions and 19 deletions

View file

@ -341,11 +341,21 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\"
// Stopped by `crafter-review stop`'s file, and by SIGTERM, while the
// first gate's whole pass is under way: the pass is killed, no later
// gate starts, nothing is left running.
std::vector<std::string> widths;
for (const bool bySignal : {false, true}) {
before = calls();
const std::string name = bySignal ? "term" : "file";
const Process::Child child = start({"--no-reuse", "--no-salvage"}, {{"FAKE_SLEEP", "573"}}, name);
const bool running = WaitFor([&] { return calls() > before; }, std::chrono::seconds(30));
// While it runs, the board shows its width: per account, there
// being no machine-wide cap.
std::string width;
try {
const Json drivers = Dyn::LoadJsonFile(base / "board.json").at("drivers");
for (auto it = drivers.begin(); it != drivers.end(); ++it) width = Text(Dyn::Get(Dyn::Get(it.value(), "settings"), "concurrency"));
} catch (const std::exception&) {
}
widths.push_back(width);
std::string scratch;
for (const std::string& line : Lines(base / std::format("{}.out", name))) {
if (const std::size_t at = line.rfind("; scratch "); at != std::string::npos) scratch = line.substr(at + 10);
@ -368,6 +378,15 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\"
dump.is_object() ? Text(dump.at("stoppedAt")) : Read(base / std::format("{}.err", name))));
}
// No machine-wide width by default (2026-10-31): the dump's args record
// none, a live run's board entry shows the per-account one, and a
// width below 1 is refused.
const Process::Captured noWidth = run({"--concurrency", "0"});
Check("no global cap by default: the dump records none, a live run shows `128 per account` on the board; --concurrency below 1 is a usage error",
dumpB.is_object() && dumpB.at("args").at("concurrency").is_null() && widths == std::vector<std::string>{"128 per account", "128 per account"} && noWidth.returnCode == 3
&& noWidth.err.contains("argument --concurrency: a number from 1 up"),
std::format("{} | {}", Dyn::Join(widths, ", "), noWidth.err));
// An env file that is not UTF-8: the preflight stops the run.
Write(base / "bad.env", std::string("\xff\n", 2));
const Process::Captured badEnv = run({"--no-reuse", "--no-salvage", "--env-file", (base / "bad.env").string()});

View file

@ -145,6 +145,43 @@ namespace {
Check("a no-room wait ends on the next completion, not at the window reset",
r.DoneList().size() == 3 && Seconds(t0) < 5 && AnyContains(waited, "no seat has room") && AnyContains(waited, "seat room is re-checked") && r.PauseUntil() == 0, Dyn::Join(waited, " | "));
// 2026-10-31: no machine-wide width by default - each account takes at
// most 128 passes at once, every driver's counted.
{
std::vector<Json> two;
for (const std::string_view id : {std::string_view("1"), std::string_view("2")}) {
Json s = SeatPool::NewSeat(std::string(id), "oauth", std::format("t{}", id));
s["state"] = "ok";
s["windows"] = {{"five_hour", {{"utilization", 10}, {"resetsAt", now + 3600}}}, {"seven_day", {{"utilization", 10}, {"resetsAt", now + 86400}}}};
two.push_back(std::move(s));
}
std::mutex lock;
std::map<std::string, std::int64_t> inSeat;
std::map<std::string, std::int64_t> peakSeat;
std::int64_t inFlight = 0;
std::int64_t peak = 0;
Pool::Scheduler wide({.log = [](const std::string&) {}, .seats = &two, .seatConfig = SeatPool::PoolConfig({}), .apiConfig = SeatPool::ApiConfig({})});
std::vector<Json> many;
for (std::int32_t i = 0; i < 300; ++i) many.push_back({{"label", std::format("w{}", i)}});
wide.Run(many, [&](const Json& j, std::int64_t) {
const std::string seat = Dyn::Str(j.at("seat"));
{
const std::lock_guard guard(lock);
peakSeat[seat] = std::max(peakSeat[seat], ++inSeat[seat]);
peak = std::max(peak, ++inFlight);
}
std::this_thread::sleep_for(300ms);
{
const std::lock_guard guard(lock);
--inSeat[seat];
--inFlight;
}
return std::pair<Json, Json>({{"label", j.at("label")}, {"status", "ok"}}, nullptr);
}, NoFollowUp);
Check("no machine-wide width by default: two accounts take 128 passes each, 256 at once, and all 300 finish",
wide.DoneList().size() == 300 && peak == 256 && peakSeat["1"] == 128 && peakSeat["2"] == 128, std::format("peak {}, per seat {} and {}", peak, peakSeat["1"], peakSeat["2"]));
}
std::map<std::string, double> started;
std::map<std::string, double> answered;
logs.Clear();