@cocreate/server 1.6.1 → 1.6.3
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 +435 -55
- package/package.json +29 -10
- package/src/index.js +245 -393
- package/src/ipc.js +157 -0
- package/src/log.js +207 -0
- package/src/state.js +436 -0
- package/src/storage.js +104 -0
- package/src/getIp.js +0 -113
package/src/ipc.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
********************************************************************************/
|
|
18
|
+
|
|
19
|
+
import cluster from 'node:cluster';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initializes Inter-Process Communication (IPC) transport routing between
|
|
23
|
+
* the Master orchestrator and clustered Worker processes.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} Server - The CoCreateServer instance reference
|
|
26
|
+
* @param {Map} pendingPromises - Tracking map for async worker RPC promises
|
|
27
|
+
* @returns {Function} Context-aware send function for dispatching cluster messages
|
|
28
|
+
*/
|
|
29
|
+
export function init(Server, pendingPromises) {
|
|
30
|
+
if (Server.isMaster) {
|
|
31
|
+
cluster.on('message', async (worker, message) => {
|
|
32
|
+
if (!message || typeof message !== 'object') return;
|
|
33
|
+
|
|
34
|
+
// 1. Resolve pending RPC promise if messageId matches
|
|
35
|
+
if (message.messageId && pendingPromises.has(message.messageId)) {
|
|
36
|
+
const promiseHandler = pendingPromises.get(message.messageId);
|
|
37
|
+
pendingPromises.delete(message.messageId);
|
|
38
|
+
if (message.success) {
|
|
39
|
+
promiseHandler.resolve(message.result);
|
|
40
|
+
} else {
|
|
41
|
+
promiseHandler.reject(new Error(message.error || 'IPC RPC Request Failed'));
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Emit event locally on Master for subscribers (e.g. log, state, storage)
|
|
47
|
+
if (message.method) {
|
|
48
|
+
Server.emit(message.method, message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Relay broadcast or targeted messages to other workers
|
|
52
|
+
if (message.broadcast || message.worker) {
|
|
53
|
+
send(message);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function send(payload = {}) {
|
|
58
|
+
if (!payload || typeof payload !== 'object') return;
|
|
59
|
+
payload.id = Server.id;
|
|
60
|
+
|
|
61
|
+
const targetWorkers = [];
|
|
62
|
+
|
|
63
|
+
if (payload.worker) {
|
|
64
|
+
if (Array.isArray(payload.worker)) {
|
|
65
|
+
for (const wId of payload.worker) {
|
|
66
|
+
const w = Server.workers.get(wId);
|
|
67
|
+
if (w) targetWorkers.push(w);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
const w = Server.workers.get(payload.worker);
|
|
71
|
+
if (w) targetWorkers.push(w);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// Broadcast to all workers by default if no specific worker specified
|
|
75
|
+
for (const w of Server.workers.values()) {
|
|
76
|
+
targetWorkers.push(w);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check edge listener fallback conditions for cordon/drain commands
|
|
81
|
+
if (payload.method === 'cordon' || payload.method === 'drain') {
|
|
82
|
+
const isEnabling = typeof payload.value === 'boolean' ? payload.value : (payload.value !== false);
|
|
83
|
+
if (isEnabling) {
|
|
84
|
+
const targetedIds = new Set(targetWorkers.map(w => w.id));
|
|
85
|
+
let activeRemaining = 0;
|
|
86
|
+
|
|
87
|
+
for (const w of Server.workers.values()) {
|
|
88
|
+
if (!targetedIds.has(w.id) && w.isListening && !w.isCordoned && !w.isDraining) {
|
|
89
|
+
activeRemaining++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (activeRemaining === 0) {
|
|
94
|
+
Server.evaluateMasterFallback();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const workerObj of targetWorkers) {
|
|
100
|
+
const workerProcess = workerObj.process || workerObj;
|
|
101
|
+
if (workerProcess && typeof workerProcess.send === 'function' && workerProcess.isConnected()) {
|
|
102
|
+
try {
|
|
103
|
+
workerProcess.send(payload);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error(`[@cocreate/server] [MASTER] Failed to send IPC payload to worker ${workerObj.id}:`, err.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fire event locally on Master process unless explicitly turned off via master: false
|
|
111
|
+
if (payload.master !== false && payload.method) {
|
|
112
|
+
Server.emit(payload.method, payload);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return send;
|
|
117
|
+
} else {
|
|
118
|
+
process.on('message', async (message) => {
|
|
119
|
+
if (!message || typeof message !== 'object') return;
|
|
120
|
+
|
|
121
|
+
if (message.messageId && pendingPromises.has(message.messageId)) {
|
|
122
|
+
const promiseHandler = pendingPromises.get(message.messageId);
|
|
123
|
+
pendingPromises.delete(message.messageId);
|
|
124
|
+
|
|
125
|
+
if (message.success) {
|
|
126
|
+
promiseHandler.resolve(message.result);
|
|
127
|
+
} else {
|
|
128
|
+
promiseHandler.reject(new Error(message.error || 'IPC RPC Call Failed'));
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (message.method) {
|
|
134
|
+
Server.emit(message.method, message);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return function send(payload = {}) {
|
|
139
|
+
if (!payload || typeof payload !== 'object') return;
|
|
140
|
+
payload.id = Server.id;
|
|
141
|
+
|
|
142
|
+
if (typeof process.send === 'function' && process.connected) {
|
|
143
|
+
try {
|
|
144
|
+
process.send(payload);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`[@cocreate/server] [WORKER ${process.pid}] Failed to dispatch IPC message to master:`, err.message);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
if (payload.method) {
|
|
150
|
+
Server.emit(payload.method, payload);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default { init };
|
package/src/log.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
********************************************************************************/
|
|
18
|
+
|
|
19
|
+
import util from 'node:util';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LogManager captures console logs and manual server log dispatches across
|
|
23
|
+
* cluster workers, aggregates them on the master orchestrator, and periodically
|
|
24
|
+
* flushes a capped ring buffer of logs to the server object in the database.
|
|
25
|
+
*/
|
|
26
|
+
export class LogManager {
|
|
27
|
+
constructor(Server) {
|
|
28
|
+
this.server = Server;
|
|
29
|
+
this.logQueue = [];
|
|
30
|
+
this.recentLogs = [];
|
|
31
|
+
this.maxLogs = 100; // Capped ring buffer size on server document
|
|
32
|
+
this.flushIntervalMs = 1500; // Time-window flush in milliseconds
|
|
33
|
+
this.maxBatchSize = 20; // Trigger immediate flush if queue exceeds size
|
|
34
|
+
this.isLoggingInternal = false; // Recursion guard against infinite loops
|
|
35
|
+
this.flushTimer = null;
|
|
36
|
+
|
|
37
|
+
// Preserve native console methods for uninhibited terminal stdout/stderr
|
|
38
|
+
this.originalConsole = {
|
|
39
|
+
log: console.log.bind(console),
|
|
40
|
+
info: console.info.bind(console),
|
|
41
|
+
warn: console.warn.bind(console),
|
|
42
|
+
error: console.error.bind(console)
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.bindMethods();
|
|
46
|
+
this.setupIPCListeners();
|
|
47
|
+
this.patchConsole();
|
|
48
|
+
this.setupFlushTimer();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bindMethods() {
|
|
52
|
+
this.server.log = this.log.bind(this);
|
|
53
|
+
this.server.flushLogs = this.flushLogs.bind(this);
|
|
54
|
+
this.server.handleIncomingWorkerLog = this.handleIncomingWorkerLog.bind(this);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setupIPCListeners() {
|
|
58
|
+
// Listen for 'server.log' events on master process dispatched across IPC
|
|
59
|
+
if (this.server.isMaster) {
|
|
60
|
+
this.server.on('server.log', (message) => {
|
|
61
|
+
if (message && message.log) {
|
|
62
|
+
this.handleIncomingWorkerLog(message.log);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
patchConsole() {
|
|
69
|
+
const self = this;
|
|
70
|
+
|
|
71
|
+
const createConsoleProxy = (level, originalFn) => {
|
|
72
|
+
return function (...args) {
|
|
73
|
+
// Always execute original console method to print to standard stdout/stderr
|
|
74
|
+
originalFn.apply(console, args);
|
|
75
|
+
|
|
76
|
+
// Skip log capture if internal logging recursion flag is active
|
|
77
|
+
if (self.isLoggingInternal) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const message = args.map(arg => {
|
|
81
|
+
if (typeof arg === 'string') return arg;
|
|
82
|
+
if (arg instanceof Error) return arg.stack || arg.message;
|
|
83
|
+
return util.inspect(arg, { depth: 3, colors: false });
|
|
84
|
+
}).join(' ');
|
|
85
|
+
|
|
86
|
+
self.log(level, message);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// Fail gracefully to ensure native console execution is never disrupted
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
console.log = createConsoleProxy('info', this.originalConsole.log);
|
|
94
|
+
console.info = createConsoleProxy('info', this.originalConsole.info);
|
|
95
|
+
console.warn = createConsoleProxy('warn', this.originalConsole.warn);
|
|
96
|
+
console.error = createConsoleProxy('error', this.originalConsole.error);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
log(level = 'info', message = '', metadata = null) {
|
|
100
|
+
if (this.isLoggingInternal) return;
|
|
101
|
+
|
|
102
|
+
const sourceWorker = this.server.isMaster ? 'master' : (this.server.workerId || 1);
|
|
103
|
+
|
|
104
|
+
const logEntry = {
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
workerId: sourceWorker,
|
|
107
|
+
level,
|
|
108
|
+
message,
|
|
109
|
+
...(metadata && typeof metadata === 'object' ? { metadata } : {})
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (!this.server.isMaster) {
|
|
113
|
+
// Worker Process: Relay log entry to Master Orchestrator over IPC
|
|
114
|
+
if (typeof this.server.send === 'function') {
|
|
115
|
+
this.server.send({
|
|
116
|
+
method: 'server.log',
|
|
117
|
+
log: logEntry
|
|
118
|
+
});
|
|
119
|
+
} else if (process.send && process.connected) {
|
|
120
|
+
try {
|
|
121
|
+
process.send({
|
|
122
|
+
method: 'server.log',
|
|
123
|
+
log: logEntry
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// Fallback to original console error if IPC fails
|
|
127
|
+
this.originalConsole.error('[@cocreate/server] [WORKER LOG IPC ERROR]', err.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Master Process: Append directly to local log queue
|
|
132
|
+
this.enqueueLog(logEntry);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
handleIncomingWorkerLog(logEntry) {
|
|
137
|
+
if (!this.server.isMaster || !logEntry) return;
|
|
138
|
+
this.enqueueLog(logEntry);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
enqueueLog(logEntry) {
|
|
142
|
+
this.logQueue.push(logEntry);
|
|
143
|
+
|
|
144
|
+
// Immediately flush if queue hits batch limit or if an error level log is emitted
|
|
145
|
+
if (this.logQueue.length >= this.maxBatchSize || logEntry.level === 'error') {
|
|
146
|
+
this.flushLogs();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
setupFlushTimer() {
|
|
151
|
+
if (!this.server.isMaster) return;
|
|
152
|
+
|
|
153
|
+
this.flushTimer = setInterval(() => {
|
|
154
|
+
this.flushLogs();
|
|
155
|
+
}, this.flushIntervalMs);
|
|
156
|
+
|
|
157
|
+
// Ensure flush timer doesn't keep node process alive during shutdown
|
|
158
|
+
if (this.flushTimer.unref) {
|
|
159
|
+
this.flushTimer.unref();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async flushLogs() {
|
|
164
|
+
if (!this.server.isMaster || this.logQueue.length === 0 || this.isLoggingInternal) return;
|
|
165
|
+
|
|
166
|
+
this.isLoggingInternal = true;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const pendingLogs = [...this.logQueue];
|
|
170
|
+
this.logQueue = [];
|
|
171
|
+
|
|
172
|
+
// Maintain capped ring buffer of recent logs on Master
|
|
173
|
+
this.recentLogs = [...this.recentLogs, ...pendingLogs].slice(-this.maxLogs);
|
|
174
|
+
|
|
175
|
+
// Persist updated log ring buffer to the server object in DB
|
|
176
|
+
if (this.server.crud && typeof this.server.getServerObject === 'function') {
|
|
177
|
+
const payload = this.server.getServerObject({
|
|
178
|
+
logs: this.recentLogs
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await this.server.crud.send(payload);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
this.originalConsole.error('[@cocreate/server] [LOG FLUSH ERROR]', err.message);
|
|
185
|
+
} finally {
|
|
186
|
+
this.isLoggingInternal = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
destroy() {
|
|
191
|
+
if (this.flushTimer) {
|
|
192
|
+
clearInterval(this.flushTimer);
|
|
193
|
+
this.flushTimer = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log = this.originalConsole.log;
|
|
197
|
+
console.info = this.originalConsole.info;
|
|
198
|
+
console.warn = this.originalConsole.warn;
|
|
199
|
+
console.error = this.originalConsole.error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export default {
|
|
204
|
+
init(Server) {
|
|
205
|
+
return new LogManager(Server);
|
|
206
|
+
}
|
|
207
|
+
};
|