Skip to content

Commit 018ea4a

Browse files
authored
Support running PS in a single process again (#11331)
1 parent f891287 commit 018ea4a

File tree

15 files changed

+140
-67
lines changed

15 files changed

+140
-67
lines changed

config/config-example.js

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,6 @@ exports.port = 8000;
1515
*/
1616
exports.bindaddress = '0.0.0.0';
1717

18-
/**
19-
* workers - the number of networking child processes to spawn
20-
* This should be no greater than the number of threads available on your
21-
* server's CPU. If you're not sure how many you have, you can check from a
22-
* terminal by running:
23-
*
24-
* $ node -e "console.log(require('os').cpus().length)"
25-
*
26-
* Using more workers than there are available threads will cause performance
27-
* issues. Keeping a couple threads available for use for OS-related work and
28-
* other PS processes will likely give you the best performance, if your
29-
* server's CPU is capable of multithreading. If you don't know what any of
30-
* this means or you are unfamiliar with PS' networking code, leave this set
31-
* to 1.
32-
*/
33-
exports.workers = 1;
34-
3518
/**
3619
* wsdeflate - compresses WebSocket messages
3720
* Toggles use of the Sec-WebSocket-Extension permessage-deflate extension.
@@ -92,6 +75,50 @@ Main's SSL deploy script from Let's Encrypt looks like:
9275
*/
9376
exports.proxyip = false;
9477

78+
// subprocesses - the number of child processes to use for various tasks.
79+
// Can be set to `0` instead of `{...}` to stop using subprocesses, if you're running out of RAM.
80+
exports.subprocesses = {
81+
/**
82+
* network - the number of networking child processes to spawn
83+
* This should be no greater than the number of threads available on your
84+
* server's CPU. If you're not sure how many you have, you can check from a
85+
* terminal by running:
86+
*
87+
* $ node -e "console.log(require('os').cpus().length)"
88+
*
89+
* Using more workers than there are available threads will cause performance
90+
* issues. Keeping a couple threads available for use for OS-related work and
91+
* other PS processes will likely give you the best performance, if your
92+
* server's CPU is capable of multithreading. If you don't know what any of
93+
* this means or you are unfamiliar with PS' networking code, leave this set
94+
* to 1.
95+
*/
96+
network: 1,
97+
/**
98+
* for simulating battles
99+
* You should leave this at 1 unless your server has a very large
100+
* amount of traffic (i.e. hundreds of concurrent battles).
101+
*/
102+
simulator: 1,
103+
104+
// beyond this point, it'd be very weird if you needed more than one of each of these
105+
106+
/** for validating teams */
107+
validator: 1,
108+
/** for user authentication */
109+
verifier: 1,
110+
localartemis: 1,
111+
remoteartemis: 1,
112+
friends: 1,
113+
chatdb: 1,
114+
modlog: 1,
115+
pm: 1,
116+
/** for the battlesearch chat plugin */
117+
battlesearch: 1,
118+
/** datasearch - for the datasearch chat plugin */
119+
datasearch: 1,
120+
};
121+
95122
/**
96123
* Various debug options
97124
*
@@ -401,15 +428,6 @@ exports.logchallenges = false;
401428
*/
402429
exports.loguserstats = 1000 * 60 * 10; // 10 minutes
403430

404-
/**
405-
* validatorprocesses - the number of processes to use for validating teams
406-
* simulatorprocesses - the number of processes to use for handling battles
407-
* You should leave both of these at 1 unless your server has a very large
408-
* amount of traffic (i.e. hundreds of concurrent battles).
409-
*/
410-
exports.validatorprocesses = 1;
411-
exports.simulatorprocesses = 1;
412-
413431
/**
414432
* inactiveuserthreshold - how long a user must be inactive before being pruned
415433
* from the `users` array. The default is 1 hour.

server/artemis/local.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,5 @@ if (require.main === module) {
173173
// eslint-disable-next-line no-eval
174174
Repl.start(`abusemonitor-local-${process.pid}`, cmd => eval(cmd));
175175
} else if (!process.send) {
176-
PM.spawn(Config.localartemisprocesses || 1);
176+
PM.spawn(global.Config?.subprocessescache?.localartemis ?? 1);
177177
}

server/artemis/remote.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ if (require.main === module) {
123123
// eslint-disable-next-line no-eval
124124
Repl.start(`abusemonitor-remote-${process.pid}`, cmd => eval(cmd));
125125
} else if (!process.send) {
126-
PM.spawn(Config.remoteartemisprocesses || 1);
126+
PM.spawn(global.Config?.subprocessescache?.remoteartemis ?? 1);
127127
}
128128

129129
export class RemoteClassifier {

server/chat-plugins/battlesearch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ interface BattleSearchResults {
2626
timesBattled: { [k: string]: number };
2727
}
2828

29-
const MAX_BATTLESEARCH_PROCESSES = 1;
3029
export async function runBattleSearch(userids: ID[], month: string, tierid: ID, turnLimit?: number) {
3130
const useRipgrep = await checkRipgrepAvailability();
3231
const pathString = `${month}/${tierid}/`;
@@ -499,5 +498,5 @@ if (!PM.isParentProcess) {
499498
// eslint-disable-next-line no-eval
500499
Repl.start('battlesearch', cmd => eval(cmd));
501500
} else {
502-
PM.spawn(MAX_BATTLESEARCH_PROCESSES);
501+
PM.spawn(global.Config?.subprocessescache?.battlesearch ?? 1);
503502
}

server/chat-plugins/datasearch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ interface MoveOrGroup {
5050

5151
type Direction = 'less' | 'greater' | 'equal';
5252

53-
const MAX_PROCESSES = 1;
5453
const RESULTS_MAX_LENGTH = 10;
5554
const MAX_RANDOM_RESULTS = 30;
5655
const dexesHelpMods = Object.keys((global.Dex?.dexes || {})).filter(x => x !== 'sourceMaps').join('</code>, <code>');
@@ -3119,7 +3118,7 @@ if (!PM.isParentProcess) {
31193118
// eslint-disable-next-line no-eval
31203119
require('../../lib/repl').Repl.start('dexsearch', (cmd: string) => eval(cmd));
31213120
} else {
3122-
PM.spawn(MAX_PROCESSES);
3121+
PM.spawn(global.Config?.subprocessescache?.datasearch ?? 1);
31233122
}
31243123

31253124
export const testables = {

server/chat.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,7 +1821,7 @@ export const Chat = new class {
18211821
*/
18221822
database = SQL(module, {
18231823
file: global.Config?.nofswriting ? ':memory:' : PLUGIN_DATABASE_PATH,
1824-
processes: global.Config?.chatdbprocesses,
1824+
processes: global.Config?.subprocessescache?.chatdb ?? 1,
18251825
});
18261826
databaseReadyPromise: Promise<void> | null = null;
18271827

@@ -2708,7 +2708,7 @@ export interface Monitor {
27082708

27092709
// explicitly check this so it doesn't happen in other child processes
27102710
if (!process.send) {
2711-
Chat.database.spawn(Config.chatdbprocesses || 1);
2711+
Chat.database.spawn(global.Config?.subprocessescache?.chatdb ?? 1);
27122712
Chat.databaseReadyPromise = Chat.prepareDatabase();
27132713
// we need to make sure it is explicitly JUST the child of the original parent db process
27142714
// no other child processes

server/config-loader.ts

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,36 @@ import * as defaults from '../config/config-example';
99
import type { GroupInfo, EffectiveGroupSymbol } from './user-groups';
1010
import { ProcessManager, FS } from '../lib';
1111

12-
export type ConfigType = typeof defaults & {
12+
type DefaultConfig = typeof defaults;
13+
14+
type InputConfig = Omit<DefaultConfig, 'subprocesses'> & {
15+
subprocesses: null | 0 | 1 | SubProcessesConfig,
16+
};
17+
18+
type ProcessType = (
19+
'localartemis' | 'remoteartemis' | 'battlesearch' | 'datasearch' | 'friends' |
20+
'chatdb' | 'pm' | 'modlog' | 'network' | 'simulator' | 'validator' | 'verifier'
21+
);
22+
23+
type SubProcessesConfig = Partial<Record<ProcessType, number>>;
24+
25+
export type ConfigType = InputConfig & {
1326
groups: { [symbol: string]: GroupInfo },
1427
groupsranking: EffectiveGroupSymbol[],
1528
greatergroupscache: { [combo: string]: GroupSymbol },
29+
subprocessescache: SubProcessesConfig,
1630
[k: string]: any,
1731
};
1832
/** Map<process flag, config settings for it to turn on> */
1933
const FLAG_PRESETS = new Map([
2034
['--no-security', ['nothrottle', 'noguestsecurity', 'noipchecks']],
2135
]);
2236

37+
const processTypes: ProcessType[] = [
38+
'localartemis', 'remoteartemis', 'battlesearch', 'datasearch', 'friends',
39+
'chatdb', 'pm', 'modlog', 'network', 'simulator', 'validator', 'verifier',
40+
];
41+
2342
const CONFIG_PATH = FS('./config/config.js').path;
2443

2544
export function load(invalidate = false) {
@@ -28,12 +47,25 @@ export function load(invalidate = false) {
2847
// config.routes is nested - we need to ensure values are set for its keys as well.
2948
config.routes = { ...defaults.routes, ...config.routes };
3049

31-
// Automatically stop startup if better-sqlite3 isn't installed and SQLite is enabled
32-
if (config.usesqlite) {
33-
try {
34-
require('better-sqlite3');
35-
} catch {
36-
throw new Error(`better-sqlite3 is not installed or could not be loaded, but Config.usesqlite is enabled.`);
50+
if (!process.send) {
51+
// Automatically stop startup if optional dependencies are enabled yet missing
52+
if (config.usesqlite) {
53+
try {
54+
require.resolve('better-sqlite3');
55+
} catch {
56+
throw new Error(`better-sqlite3 is not installed or could not be loaded, but Config.usesqlite is enabled.`);
57+
}
58+
}
59+
60+
if (config.ofemain) {
61+
try {
62+
require.resolve('node-oom-heapdump');
63+
} catch {
64+
throw new Error(
65+
`node-oom-heapdump is not installed, but it is a required dependency if Config.ofemain is set to true! ` +
66+
`Run npm install node-oom-heapdump and restart the server.`
67+
);
68+
}
3769
}
3870
}
3971

@@ -43,18 +75,57 @@ export function load(invalidate = false) {
4375
}
4476
}
4577

78+
cacheSubProcesses(config);
4679
cacheGroupData(config);
4780
return config;
4881
}
4982

83+
function cacheSubProcesses(config: ConfigType) {
84+
if (config.subprocesses !== undefined) {
85+
// Leniently accept all other falsy values, including `null`.
86+
const value = config.subprocesses || 0;
87+
if (value === 0 || value === 1) {
88+
// https://github.com/microsoft/TypeScript/issues/35745
89+
config.subprocessescache = (Object.fromEntries(
90+
processTypes.map(k => [k, value])
91+
) as Record<ProcessType, number>);
92+
} else if (typeof value === 'object') {
93+
config.subprocessescache = value;
94+
} else {
95+
reportError(`Invalid \`subprocesses\` specification. Use any of 0, 1, or a plain old object.`);
96+
}
97+
}
98+
config.subprocessescache ??= {};
99+
const deprecatedKeys = [];
100+
if ('workers' in config) {
101+
deprecatedKeys.push('workers');
102+
config.subprocessescache.network = config.workers;
103+
}
104+
for (const processType of processTypes) {
105+
if (processType === 'network') continue;
106+
const compatKey = `${processType}processes`;
107+
if (compatKey in config) {
108+
deprecatedKeys.push(compatKey);
109+
config.subprocessescache[processType] = config[compatKey];
110+
}
111+
}
112+
for (const compatKey of deprecatedKeys) {
113+
reportError(
114+
`You are using \`${compatKey}\`, which is deprecated\n` +
115+
`Support for this may be removed.\n` +
116+
`Please ensure that you update your config.js to use \`subprocesses\` (see config-example.js, line 80).\n`
117+
);
118+
}
119+
}
120+
50121
export function cacheGroupData(config: ConfigType) {
51122
if (config.groups) {
52123
// Support for old config groups format.
53124
// Should be removed soon.
54125
reportError(
55126
`You are using a deprecated version of user group specification in config.\n` +
56-
`Support for this will be removed soon.\n` +
57-
`Please ensure that you update your config.js to the new format (see config-example.js, line 457).\n`
127+
`Support for this may be removed.\n` +
128+
`Please ensure that you update your config.js to the new format (see config-example.js, line 521).\n`
58129
);
59130
} else {
60131
config.punishgroups = Object.create(null);
@@ -162,6 +233,6 @@ function reportError(msg: string) {
162233
// This module generally loads before Monitor, so we put this in a setImmediate to wait for it to load.
163234
// Most child processes don't have Monitor.error, but the main process should always have them, and Config
164235
// errors should always be the same across processes, so this is a neat way to avoid unnecessary logging.
165-
setImmediate(() => global.Monitor?.error?.(msg));
236+
setImmediate(() => global.Monitor?.error?.(`[CONFIG] ${msg}`));
166237
}
167238
export const Config = load();

server/friends.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,5 +454,5 @@ if (require.main === module) {
454454
Repl.start(`friends-${process.pid}`, cmd => eval(cmd));
455455
}
456456
} else if (!process.send) {
457-
PM.spawn(Config.friendsprocesses || 1);
457+
PM.spawn(global.Config?.subprocessescache?.friends ?? 1);
458458
}

server/index.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ function setupGlobals() {
131131

132132
const Verifier = require('./verifier');
133133
global.Verifier = Verifier;
134-
Verifier.PM.spawn();
135134

136135
const { Tournaments } = require('./tournaments');
137136
global.Tournaments = Tournaments;
@@ -157,7 +156,7 @@ if (Config.crashguard) {
157156
* Start networking processes to be connected to
158157
*********************************************************/
159158

160-
import { Sockets } from './sockets';
159+
const { Sockets } = require('./sockets');
161160
global.Sockets = Sockets;
162161

163162
export function listen(port: number, bindAddress: string, workerCount: number) {
@@ -182,9 +181,8 @@ if (require.main === module) {
182181
* Set up our last global
183182
*********************************************************/
184183

185-
import * as TeamValidatorAsync from './team-validator-async';
184+
const { TeamValidatorAsync } = require('./team-validator-async');
186185
global.TeamValidatorAsync = TeamValidatorAsync;
187-
TeamValidatorAsync.PM.spawn();
188186

189187
/*********************************************************
190188
* Start up the REPL server
@@ -202,16 +200,6 @@ if (Config.startuphook) {
202200
}
203201

204202
if (Config.ofemain) {
205-
try {
206-
require.resolve('node-oom-heapdump');
207-
} catch (e: any) {
208-
if (e.code !== 'MODULE_NOT_FOUND') throw e; // should never happen
209-
throw new Error(
210-
'node-oom-heapdump is not installed, but it is a required dependency if Config.ofe is set to true! ' +
211-
'Run npm install node-oom-heapdump and restart the server.'
212-
);
213-
}
214-
215203
// Create a heapdump if the process runs out of memory.
216204
global.nodeOomHeapdump = (require as any)('node-oom-heapdump')({
217205
addTimestamp: true,

server/modlog/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export class Modlog {
113113

114114
if (Config.usesqlite) {
115115
if (this.database.isParentProcess) {
116-
this.database.spawn(Config.modlogprocesses || 1);
116+
this.database.spawn(global.Config?.subprocessescache?.modlog ?? 1);
117117
} else {
118118
global.Monitor = {
119119
crashlog(error: Error, source = 'A modlog child process', details: AnyObject | null = null) {

0 commit comments

Comments
 (0)