@colyseus/tools 0.16.0-preview.2 → 0.16.0-preview.4
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/README.md +5 -5
- package/build/index.d.ts +1 -1
- package/build/index.js +42 -38
- package/build/index.js.map +3 -3
- package/build/index.mjs +42 -38
- package/build/index.mjs.map +2 -2
- package/package.json +28 -13
- package/pm2/post-deploy-agent.js +315 -0
- package/pm2/shared.js +73 -0
- package/post-deploy.js +97 -94
- package/report-stats.js +178 -0
- package/system-boot.js +89 -35
package/report-stats.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const pm2 = require('pm2');
|
|
6
|
+
|
|
7
|
+
const COLYSEUS_CLOUD_URL = `${process.env.ENDPOINT}/vultr/stats`;
|
|
8
|
+
|
|
9
|
+
const FAILED_ATTEMPS_FILE = "/var/tmp/pm2-stats-attempts.txt";
|
|
10
|
+
const FETCH_TIMEOUT = 7000;
|
|
11
|
+
|
|
12
|
+
async function retryFailedAttempts() {
|
|
13
|
+
/**
|
|
14
|
+
* Retry cached failed attempts
|
|
15
|
+
*/
|
|
16
|
+
if (!fs.existsSync(FAILED_ATTEMPS_FILE)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Retry up to last 30 attempts
|
|
21
|
+
const failedAttempts = fs.readFileSync(FAILED_ATTEMPS_FILE, "utf8").split("\n").slice(-30);
|
|
22
|
+
|
|
23
|
+
for (const body of failedAttempts) {
|
|
24
|
+
// skip if empty
|
|
25
|
+
if (!body) { continue; }
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await fetch(COLYSEUS_CLOUD_URL, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"Authorization": `Bearer ${process.env.COLYSEUS_SECRET}`,
|
|
33
|
+
"ngrok-skip-browser-warning": "yes",
|
|
34
|
+
},
|
|
35
|
+
body,
|
|
36
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT)
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.unlinkSync(FAILED_ATTEMPS_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchRetry(url, options, retries = 3) {
|
|
47
|
+
try {
|
|
48
|
+
return await fetch(url, options)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (retries === 1) throw err;
|
|
51
|
+
return await fetchRetry(url, options, retries - 1);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
pm2.Client.executeRemote('getMonitorData', {}, async function(err, list) {
|
|
56
|
+
if (err) { return console.error(err); }
|
|
57
|
+
|
|
58
|
+
//
|
|
59
|
+
// Filter out:
|
|
60
|
+
// - @colyseus/tools module (PM2 agent)
|
|
61
|
+
// - "stopped" processes (gracefully shut down)
|
|
62
|
+
//
|
|
63
|
+
list = list.filter((item) => {
|
|
64
|
+
return (
|
|
65
|
+
item.name !== '@colyseus/tools' &&
|
|
66
|
+
item.pm2_env.status !== 'stopped'
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const aggregate = { ccu: 0, roomcount: 0, };
|
|
71
|
+
const apps = {};
|
|
72
|
+
|
|
73
|
+
await Promise.all(list.map(async (item) => {
|
|
74
|
+
const env = item.pm2_env;
|
|
75
|
+
const app_id = env.pm_id;
|
|
76
|
+
const uptime = new Date(env.pm_uptime); // uptime in milliseconds
|
|
77
|
+
const axm_monitor = env.axm_monitor;
|
|
78
|
+
const restart_time = env.restart_time;
|
|
79
|
+
const status = env.status; // 'online', 'stopped', 'stopping', 'waiting restart', 'launching', 'errored'
|
|
80
|
+
const node_version = env.node_version;
|
|
81
|
+
|
|
82
|
+
const monit = {
|
|
83
|
+
cpu: item.monit.cpu, // 0 ~ 100 (%)
|
|
84
|
+
memory: item.monit.memory / 1024 / 1024, // in MB
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const version = {};
|
|
88
|
+
if (env.versioning) {
|
|
89
|
+
version.revision = env.versioning.revision;
|
|
90
|
+
version.comment = env.versioning.comment;
|
|
91
|
+
version.branch = env.versioning.branch;
|
|
92
|
+
version.update_time = env.versioning.update_time;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
aggregate.ccu += axm_monitor.ccu?.value ?? 0;
|
|
96
|
+
aggregate.roomcount += axm_monitor.roomcount?.value ?? 0;
|
|
97
|
+
|
|
98
|
+
const custom_monitor = {};
|
|
99
|
+
for (const originalKey in axm_monitor) {
|
|
100
|
+
const key = (originalKey.indexOf(" ") !== -1)
|
|
101
|
+
? axm_monitor[originalKey].type
|
|
102
|
+
: originalKey;
|
|
103
|
+
custom_monitor[key] = Number(axm_monitor[originalKey].value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// check if process .sock file is active
|
|
107
|
+
const socket_is_active = await checkSocketIsActive(`/run/colyseus/${(2567 + env.NODE_APP_INSTANCE)}.sock`);
|
|
108
|
+
|
|
109
|
+
apps[app_id] = {
|
|
110
|
+
pid: item.pid,
|
|
111
|
+
uptime,
|
|
112
|
+
app_id,
|
|
113
|
+
status,
|
|
114
|
+
restart_time,
|
|
115
|
+
...monit,
|
|
116
|
+
...custom_monitor,
|
|
117
|
+
version,
|
|
118
|
+
node_version,
|
|
119
|
+
socket_is_active,
|
|
120
|
+
};
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const fetchipv4 = await fetch("http://169.254.169.254/v1.json");
|
|
124
|
+
const ip = (await fetchipv4.json()).interfaces[0].ipv4.address;
|
|
125
|
+
|
|
126
|
+
const body = {
|
|
127
|
+
version: 1,
|
|
128
|
+
ip,
|
|
129
|
+
time: new Date(),
|
|
130
|
+
aggregate,
|
|
131
|
+
apps,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
console.log(body);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetchRetry(COLYSEUS_CLOUD_URL, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
"Authorization": `Bearer ${process.env.COLYSEUS_SECRET}`,
|
|
142
|
+
"ngrok-skip-browser-warning": "yes",
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(body),
|
|
145
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT)
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (response.status !== 200) {
|
|
149
|
+
throw new Error(`Failed to send stats to Colyseus Cloud. Status: ${response.status}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log("OK");
|
|
153
|
+
|
|
154
|
+
// Only retry failed attempts if the current attempt was successful
|
|
155
|
+
await retryFailedAttempts();
|
|
156
|
+
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error("Failed to send stats to Colyseus Cloud. ");
|
|
159
|
+
|
|
160
|
+
// cache failed attempts
|
|
161
|
+
fs.appendFileSync(FAILED_ATTEMPS_FILE, JSON.stringify(body) + "\n");
|
|
162
|
+
|
|
163
|
+
} finally {
|
|
164
|
+
process.exit();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
function checkSocketIsActive(sockFilePath) {
|
|
169
|
+
return new Promise((resolve, _) => {
|
|
170
|
+
const client = net.createConnection({ path: sockFilePath, timeout: 5000 })
|
|
171
|
+
.on('connect', () => {
|
|
172
|
+
client.end(); // close the connection
|
|
173
|
+
resolve(true);
|
|
174
|
+
})
|
|
175
|
+
.on('error', () => resolve(false))
|
|
176
|
+
.on('timeout', () => resolve(false));
|
|
177
|
+
});
|
|
178
|
+
}
|
package/system-boot.js
CHANGED
|
@@ -2,56 +2,110 @@
|
|
|
2
2
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
const {
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
6
|
|
|
7
7
|
const NGINX_LIMITS_CONFIG_FILE = '/etc/nginx/colyseus_limits.conf';
|
|
8
|
-
const LIMITS_CONF_FILE =
|
|
8
|
+
const LIMITS_CONF_FILE = '/etc/security/limits.d/colyseus.conf';
|
|
9
|
+
const SYSCTL_FILE = '/etc/sysctl.d/local.conf'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const MAX_CCU_PER_CPU = 8000;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* System-wide limits configuration for high-CCU environments
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {number} options.maxCCUPerCPU - Maximum concurrent users per CPU core
|
|
17
|
+
* @param {number} options.connectionsMultiplier - Multiplier for worker connections (default: 3)
|
|
18
|
+
* @param {number} options.fileDescriptorMultiplier - Multiplier for max file descriptors (default: 4)
|
|
19
|
+
*/
|
|
20
|
+
function configureSystemLimits(options = {}) {
|
|
21
|
+
const {
|
|
22
|
+
connectionsMultiplier = 3,
|
|
23
|
+
fileDescriptorMultiplier = 4
|
|
24
|
+
} = options;
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
}
|
|
26
|
+
const numCPU = os.cpus().length;
|
|
27
|
+
const maxCCU = numCPU * MAX_CCU_PER_CPU;
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const estimatedCCUPerGB = 5000;
|
|
29
|
+
// Calculate limits
|
|
30
|
+
const workerConnections = maxCCU * connectionsMultiplier;
|
|
31
|
+
const maxFileDescriptors = maxCCU * fileDescriptorMultiplier;
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const nginxMaxNOFileLimit = workerConnections * 2; // 2x
|
|
29
|
-
const systemMaxNOFileLimit = workerConnections * 3; // 3x for other system operations
|
|
33
|
+
// Nginx-specific calculations
|
|
34
|
+
const workerRlimitNofile = (workerConnections / numCPU) * 2;
|
|
30
35
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// Validation checks
|
|
37
|
+
if (workerConnections > 65535) {
|
|
38
|
+
console.warn(`Warning: worker_connections (${workerConnections}) exceeds typical max_socket_backlog (65535)`);
|
|
39
|
+
}
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`, bailOnErr);
|
|
41
|
+
if (maxFileDescriptors > 1048576) {
|
|
42
|
+
console.warn(`Warning: Very high file descriptor limit (${maxFileDescriptors}). Verify system capabilities.`);
|
|
43
|
+
}
|
|
39
44
|
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
if (process.argv.includes('--dry-run')) {
|
|
46
|
+
console.log({
|
|
47
|
+
maxCCU,
|
|
48
|
+
workerConnections,
|
|
49
|
+
maxFileDescriptors,
|
|
50
|
+
workerRlimitNofile
|
|
51
|
+
})
|
|
52
|
+
process.exit();
|
|
53
|
+
}
|
|
43
54
|
|
|
55
|
+
// Configuration updates
|
|
56
|
+
try {
|
|
57
|
+
// Update Nginx limits
|
|
58
|
+
if (fs.existsSync(NGINX_LIMITS_CONFIG_FILE)) {
|
|
59
|
+
fs.writeFileSync(NGINX_LIMITS_CONFIG_FILE, `
|
|
60
|
+
worker_rlimit_nofile ${workerRlimitNofile};
|
|
44
61
|
events {
|
|
62
|
+
use epoll;
|
|
45
63
|
worker_connections ${workerConnections};
|
|
46
|
-
|
|
64
|
+
multi_accept on;
|
|
47
65
|
}
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update system-wide limits
|
|
70
|
+
fs.writeFileSync(LIMITS_CONF_FILE, `
|
|
71
|
+
# System-wide file descriptor limits
|
|
72
|
+
* - nofile ${maxFileDescriptors}
|
|
73
|
+
nginx soft nofile ${maxFileDescriptors}
|
|
74
|
+
nginx hard nofile ${maxFileDescriptors}
|
|
75
|
+
`);
|
|
50
76
|
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
// Update sysctl with doubled file-max for safety margin
|
|
78
|
+
fs.writeFileSync(SYSCTL_FILE, `
|
|
79
|
+
# System-wide file descriptor limit
|
|
80
|
+
fs.file-max = ${maxFileDescriptors * 2}
|
|
81
|
+
|
|
82
|
+
# TCP buffer optimization
|
|
83
|
+
net.core.rmem_max = 16777216
|
|
84
|
+
net.core.wmem_max = 16777216
|
|
85
|
+
net.ipv4.tcp_rmem = 4096 87380 16777216
|
|
86
|
+
net.ipv4.tcp_wmem = 4096 87380 16777216
|
|
87
|
+
|
|
88
|
+
# Connection handling optimization
|
|
89
|
+
net.core.netdev_max_backlog = 50000
|
|
90
|
+
net.core.somaxconn = 65535
|
|
91
|
+
net.ipv4.tcp_tw_reuse = 1
|
|
92
|
+
net.ipv4.ip_local_port_range = 1024 65535
|
|
93
|
+
|
|
94
|
+
# TCP timeout optimization
|
|
95
|
+
net.ipv4.tcp_fin_timeout = 30
|
|
96
|
+
net.ipv4.tcp_keepalive_time = 30
|
|
97
|
+
net.ipv4.tcp_keepalive_intvl = 10
|
|
98
|
+
net.ipv4.tcp_keepalive_probes = 3
|
|
99
|
+
`);
|
|
100
|
+
|
|
101
|
+
// Apply sysctl changes
|
|
102
|
+
execSync("sysctl -p", { stdio: 'inherit' });
|
|
103
|
+
console.log(`System limits configured successfully for ${maxCCU} CCU (${MAX_CCU_PER_CPU}/CPU)`);
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Failed to update system limits:', error);
|
|
107
|
+
process.exit(1);
|
|
53
108
|
}
|
|
54
109
|
}
|
|
55
110
|
|
|
56
|
-
|
|
57
|
-
updateNOFileConfig(bailOnErr);
|
|
111
|
+
configureSystemLimits();
|