@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.
@@ -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 { exec } = require('child_process');
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 = "/etc/security/limits.conf";
8
+ const LIMITS_CONF_FILE = '/etc/security/limits.d/colyseus.conf';
9
+ const SYSCTL_FILE = '/etc/sysctl.d/local.conf'
9
10
 
10
- // update file descriptor limits systemwide + nginx worker connections
11
+ const MAX_CCU_PER_CPU = 8000;
11
12
 
12
- function bailOnErr(err) {
13
- if (err) {
14
- console.error(err);
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
- // exit with error!
17
- process.exit(1);
18
- }
19
- }
26
+ const numCPU = os.cpus().length;
27
+ const maxCCU = numCPU * MAX_CCU_PER_CPU;
20
28
 
21
- function updateNOFileConfig(cb) {
22
- // const numCPU = os.cpus().length;
23
- const totalmemMB = os.totalmem() / 1024 / 1024;
24
- const estimatedCCUPerGB = 5000;
29
+ // Calculate limits
30
+ const workerConnections = maxCCU * connectionsMultiplier;
31
+ const maxFileDescriptors = maxCCU * fileDescriptorMultiplier;
25
32
 
26
- const maxCCU = Math.floor((totalmemMB / 1024) * estimatedCCUPerGB);
27
- const workerConnections = maxCCU * 2; // 2x because of proxy_pass
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
- // immediatelly apply new nofile limit
32
- // (apparently this has no effect)
33
- exec(`ulimit -n ${systemMaxNOFileLimit}`, bailOnErr);
36
+ // Validation checks
37
+ if (workerConnections > 65535) {
38
+ console.warn(`Warning: worker_connections (${workerConnections}) exceeds typical max_socket_backlog (65535)`);
39
+ }
34
40
 
35
- // update "/etc/security/limits.conf" file.
36
- fs.writeFileSync(LIMITS_CONF_FILE, `
37
- * - nofile ${systemMaxNOFileLimit}
38
- `, bailOnErr);
41
+ if (maxFileDescriptors > 1048576) {
42
+ console.warn(`Warning: Very high file descriptor limit (${maxFileDescriptors}). Verify system capabilities.`);
43
+ }
39
44
 
40
- if (fs.existsSync(NGINX_LIMITS_CONFIG_FILE)) {
41
- fs.writeFileSync(NGINX_LIMITS_CONFIG_FILE, `
42
- worker_rlimit_nofile ${nginxMaxNOFileLimit};
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
- # multi_accept on;
64
+ multi_accept on;
47
65
  }
48
- `, cb);
49
- console.log("new nofile limit:", { workerConnections, systemMaxNOFileLimit, nginxMaxNOFileLimit });
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
- } else {
52
- console.warn(NGINX_LIMITS_CONFIG_FILE, "not found.");
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();