maxpool 1.5.52 → 1.5.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +19 -0
- package/src/server.js +15 -3
- package/src/tui.js +5 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import net from 'node:net';
|
|
3
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
5
|
import { createInterface } from 'node:readline';
|
|
5
6
|
import { loadOrCreateConfig, loadConfig, saveConfig, atomicConfigUpdate, getConfigPath, loadState, saveState, getStatePath, getLogPath, readGeneration, flushConfigWrites, flushStateWrites } from './config.js';
|
|
@@ -7,6 +8,24 @@ import { setEventLogPath, installConsoleMirror, setConsoleStdoutSuppressed } fro
|
|
|
7
8
|
import { SleepGuard } from './sleep-guard.js';
|
|
8
9
|
import { AccountManager } from './account-manager.js';
|
|
9
10
|
import { createProxyServer, REQUEST_IDLE_MAX_MS } from './server.js';
|
|
11
|
+
|
|
12
|
+
// ── Happy Eyeballs: stop racing a dead IPv6 leg ────────────────────────────────
|
|
13
|
+
// Node 20 resolves both families and, 250ms after starting the IPv4 connect, races an
|
|
14
|
+
// IPv6 one (autoSelectFamily, autoSelectFamilyAttemptTimeout=250). On a machine whose
|
|
15
|
+
// resolver returns an AAAA that BLACKHOLES — a VPN advertising IPv6 it cannot carry is
|
|
16
|
+
// the common case — that second leg never completes and takes the whole connect down
|
|
17
|
+
// with it, surfacing as UND_ERR_CONNECT_TIMEOUT / ETIMEDOUT on a network that is
|
|
18
|
+
// otherwise fine (curl, which falls back cleanly, succeeds in the same second).
|
|
19
|
+
//
|
|
20
|
+
// Measured 2026-08-01 on this fleet: 6,848 connect failures in one day; A/B over 14
|
|
21
|
+
// requests gave 12/14 at the 250ms default vs 14/14 with the timeout raised. Raising it
|
|
22
|
+
// is preferred over disabling the race outright: real IPv6 still wins where it works,
|
|
23
|
+
// and a blackholed leg simply loses instead of poisoning the attempt. `dns
|
|
24
|
+
// setDefaultResultOrder('ipv4first')` was ALSO measured and is NOT reliable (6 of 14 in a
|
|
25
|
+
// degraded window) — it reorders preference but still starts the doomed leg.
|
|
26
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(
|
|
27
|
+
Math.max(1000, Number(process.env.MAXPOOL_FAMILY_ATTEMPT_TIMEOUT_MS) || 5000),
|
|
28
|
+
);
|
|
10
29
|
import { Prober } from './prober.js';
|
|
11
30
|
import { loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon, tokenFingerprint } from './oauth.js';
|
|
12
31
|
import { TUI } from './tui.js';
|
package/src/server.js
CHANGED
|
@@ -256,7 +256,7 @@ export function createProxyServer(accountManager, config, hooks = {}) {
|
|
|
256
256
|
let requestInfo = {};
|
|
257
257
|
// Backstop reaper (defense-in-depth for any UNKNOWN future hang the specific
|
|
258
258
|
// TTFB/idle/body/drain guards don't cover). See startIdleRequestReaper.
|
|
259
|
-
const reaperTimer = startIdleRequestReaper(res, reqId, REQUEST_IDLE_MAX_MS);
|
|
259
|
+
const reaperTimer = startIdleRequestReaper(res, reqId, REQUEST_IDLE_MAX_MS, { getRequestInfo: () => requestInfo });
|
|
260
260
|
try {
|
|
261
261
|
// Buffer request body (needed for retry on 429)
|
|
262
262
|
const bodyChunks = [];
|
|
@@ -2407,12 +2407,24 @@ function mappedModel(originalModel, account) {
|
|
|
2407
2407
|
* (which has no idempotency guard). Clock/timer injectable for tests. Returns the
|
|
2408
2408
|
* interval handle (caller clearInterval()s it in the request's finally).
|
|
2409
2409
|
*/
|
|
2410
|
-
function startIdleRequestReaper(res, reqId, idleMs, { now = Date.now, setIntervalFn = setInterval } = {}) {
|
|
2410
|
+
function startIdleRequestReaper(res, reqId, idleMs, { now = Date.now, setIntervalFn = setInterval, getRequestInfo = null } = {}) {
|
|
2411
2411
|
let lastBytes = res.socket?.bytesWritten ?? 0;
|
|
2412
2412
|
let lastProgressAt = now();
|
|
2413
2413
|
const timer = setIntervalFn(() => {
|
|
2414
|
+
// Count only REAL progress. `bytesWritten` includes maxpool's OWN queue keepalive,
|
|
2415
|
+
// which pings every 10s — so a request stuck forever kept resetting this watchdog with
|
|
2416
|
+
// our own noise and could never be reaped. Observed 2026-07-29: 50 requests pinned
|
|
2417
|
+
// "in-flight" on one account for up to 6.7h, serving zero, which distorted the load
|
|
2418
|
+
// balancer into avoiding a healthy account. A held request is progressing only if the
|
|
2419
|
+
// UPSTREAM produced something; heartbeat bytes prove nothing.
|
|
2420
|
+
const heldOnHeartbeat = Boolean(getRequestInfo?.()?.queueHeartbeatActive);
|
|
2414
2421
|
const bytes = res.socket?.bytesWritten ?? lastBytes;
|
|
2415
|
-
if (bytes !== lastBytes) {
|
|
2422
|
+
if (bytes !== lastBytes) {
|
|
2423
|
+
lastBytes = bytes;
|
|
2424
|
+
if (!heldOnHeartbeat) { lastProgressAt = now(); return; }
|
|
2425
|
+
// Held: those bytes were OUR keepalive, so they are not progress — deliberately
|
|
2426
|
+
// fall through to the staleness check rather than returning.
|
|
2427
|
+
}
|
|
2416
2428
|
if (now() - lastProgressAt >= idleMs && !res.writableEnded && !res.destroyed) {
|
|
2417
2429
|
console.error(`[Maxpool] Request ${reqId} — no write progress for ${Math.round(idleMs / 1000)}s (backstop reaper); force-aborting a stuck request to free its account slot`);
|
|
2418
2430
|
res.destroy();
|
package/src/tui.js
CHANGED
|
@@ -1214,7 +1214,11 @@ export class TUI {
|
|
|
1214
1214
|
|
|
1215
1215
|
// Active requests
|
|
1216
1216
|
const now = Date.now();
|
|
1217
|
-
|
|
1217
|
+
// NEWEST FIRST, matching the completed log directly below (which is `unshift`ed).
|
|
1218
|
+
// A Map iterates in INSERTION order, so this block used to run oldest-first while the
|
|
1219
|
+
// log under it ran newest-first — the panel disagreed with itself and the newest thing
|
|
1220
|
+
// that happened was buried in the middle of the screen.
|
|
1221
|
+
for (const [, r] of [...this.active].reverse()) {
|
|
1218
1222
|
const el = ((now - r.started) / 1000).toFixed(1);
|
|
1219
1223
|
const sp = cyan(SPINNER[this.frame]);
|
|
1220
1224
|
const a = r.account ? ` → ${r.account}` : '';
|