nexus-fca 3.1.4 → 3.1.6
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/docs/memory-usage.md +50 -0
- package/package.json +1 -1
- package/src/listenMqtt.js +205 -22
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Nexus-FCA Memory Usage Guide
|
|
2
|
+
|
|
3
|
+
## Built-in guardrails
|
|
4
|
+
|
|
5
|
+
- **Outbound MQTT buffers** (`src/sendMessageMqtt.js`, `src/listenMqtt.js`)
|
|
6
|
+
- `_pendingOutbound` tracks only outstanding message IDs and is pruned immediately when an ACK lands.
|
|
7
|
+
- Health metrics (`lib/health/HealthMetrics.js`) cap ACK samples at 50 entries and keep averages via exponential smoothing, so telemetry never balloons in RAM.
|
|
8
|
+
- **Pending edit queue** (`src/editMessage.js`)
|
|
9
|
+
- `globalOptions.editSettings.maxPendingEdits` defaults to 200; oldest edits are dropped once the cap is hit, and TTL checks clear stale entries.
|
|
10
|
+
- Every ACK removes the corresponding entry, and `HealthMetrics` mirrors the size so you can alert if it starts climbing.
|
|
11
|
+
- **Group send queue** (`index.js`)
|
|
12
|
+
- Each group’s queue is capped (default 100 messages) and sweeps run every 5 minutes to drop idle or over-capacity queues.
|
|
13
|
+
- Sweeper stats surface through `ctx.health.recordGroupQueuePrune`, so you can confirm that cleanup is happening.
|
|
14
|
+
- **Performance caches** (`lib/performance/PerformanceManager.js`)
|
|
15
|
+
- Cache maps are bounded by `cacheSize` (default 1000) and enforce TTL, while request time windows keep only the last 100 samples.
|
|
16
|
+
- `PerformanceOptimizer` trims its request history to 1000 entries and halves the buffer on each cleanup pass.
|
|
17
|
+
- **Database write queue** (`lib/database/EnhancedDatabase.js`)
|
|
18
|
+
- Writes batch in chunks of 100 and the queue processor re-runs every second. Long outages are the only way to accumulate large queues.
|
|
19
|
+
- **Safety timers** (`lib/safety/FacebookSafety.js`)
|
|
20
|
+
- All recurring timers are stored and cleared before new ones are scheduled, preventing runaway intervals during reconnect churn.
|
|
21
|
+
|
|
22
|
+
## Situations that can increase memory
|
|
23
|
+
|
|
24
|
+
| Area | Why it grows | Mitigation |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Database write queue | Target SQLite/SQL server offline → `writeQueue` keeps buffering | Monitor `writeQueue.length` or add alerts around `EnhancedDatabase.processQueue`; if storage is optional, disable DB integration entirely.
|
|
27
|
+
| Multiple PerformanceManager / PerformanceOptimizer instances | Each instance spawns its own metrics intervals and caches | Treat both managers as singletons; share them via dependency injection instead of `new`ing per feature.
|
|
28
|
+
| Elevated group queue caps | Setting `setGroupQueueCapacity` >> 100 multiplies per-thread memory | Keep caps small; rely on `_flushGroupQueue` for bursts instead of raising the ceiling.
|
|
29
|
+
| Pending edit saturation | Frequent edit retries without ACKs hit the 200-item cap | Investigate upstream failures (usually edit rights or MQTT drops). `api.getMemoryMetrics()` will show `pendingEditsDropped` climbing when this happens.
|
|
30
|
+
| Large custom caches | If you override `cacheSize` or TTLs to very large values, the Map will grow | Pick realistic TTLs; if the workload is mostly transient, disable cache (`enableCache: false`).
|
|
31
|
+
|
|
32
|
+
## Monitoring checklist
|
|
33
|
+
|
|
34
|
+
1. **Runtime snapshot** – call `api.getMemoryMetrics()` to read pending edit counts, outbound depth, and memory guard actions.
|
|
35
|
+
2. **Health dashboard** – `ctx.health.snapshot()` (or the API wrapper `getHealthMetrics`) exposes ACK latency samples and queue stats.
|
|
36
|
+
3. **Performance events** – `PerformanceManager` emits `metricsUpdate` every 30s; attach a listener and pipe to your logger or Prometheus bridge.
|
|
37
|
+
4. **Node heap checks** – pair the built-in metrics with `process.memoryUsage()` or `--inspect` tooling if you suspect leaks from user code.
|
|
38
|
+
|
|
39
|
+
## Configuration tips
|
|
40
|
+
|
|
41
|
+
- Tune `globalOptions.editSettings` if your bot edits aggressively; lower `maxPendingEdits` to 100 and `editTTLms` to 2–3 minutes for tighter control.
|
|
42
|
+
- Use `api.setGroupQueueCapacity(n)` to keep per-thread queues bounded; the sweeper already limits idle queues to 30 minutes, but lower values (10–20) reduce burst memory further.
|
|
43
|
+
- If you don’t need database analytics, avoid initializing `EnhancedDatabase`/`DatabaseManager`; the rest of the stack runs without it.
|
|
44
|
+
- Disable extra instrumentation when running on low-memory hardware:
|
|
45
|
+
```js
|
|
46
|
+
const perfManager = new PerformanceManager({ enableMetrics: false, enableCache: false });
|
|
47
|
+
```
|
|
48
|
+
- Always reuse the same `PerformanceOptimizer`/`PerformanceManager` instead of instantiating per request handler, so their intervals remain singular.
|
|
49
|
+
|
|
50
|
+
Following the defaults keeps Nexus-FCA comfortably under a few hundred megabytes even on small VPS nodes. When memory spikes appear, start by sampling `api.getMemoryMetrics()` and check the table above to see which subsystem is accumulating work. Adjust the related caps or temporarily disable the optional feature until the upstream issue (DB outage, repeated edit failures, etc.) is resolved.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-fca",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.6",
|
|
4
4
|
"description": "Nexus-FCA 3.1 – THE BEST, SAFEST, MOST STABLE Facebook Messenger API! Email/password + appState login, proxy support (HTTP/HTTPS/SOCKS5), random user agent, proactive cookie refresh, MQTT stability, session protection, and TypeScript support.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
package/src/listenMqtt.js
CHANGED
|
@@ -93,19 +93,34 @@ function fetchSeqID(defaultFuncs, api, ctx, callback) {
|
|
|
93
93
|
|
|
94
94
|
// Adaptive backoff state (per-process singleton like) - tie to ctx
|
|
95
95
|
function getBackoffState(ctx){
|
|
96
|
+
const envBase = parseInt(process.env.NEXUS_MQTT_BACKOFF_BASE, 10) || 1000;
|
|
97
|
+
const envMax = parseInt(process.env.NEXUS_MQTT_BACKOFF_MAX, 10) || (5 * 60 * 1000);
|
|
98
|
+
const envFactor = parseFloat(process.env.NEXUS_MQTT_BACKOFF_FACTOR) || 2;
|
|
99
|
+
const backoffOverrides = (ctx.globalOptions && ctx.globalOptions.backoff) || {};
|
|
100
|
+
const resolved = {
|
|
101
|
+
base: Number.isFinite(backoffOverrides.baseMs) ? backoffOverrides.baseMs : envBase,
|
|
102
|
+
max: Number.isFinite(backoffOverrides.maxMs) ? backoffOverrides.maxMs : envMax,
|
|
103
|
+
factor: Number.isFinite(backoffOverrides.factor) ? backoffOverrides.factor : envFactor,
|
|
104
|
+
jitter: typeof backoffOverrides.jitter === 'number' ? backoffOverrides.jitter : 0.25,
|
|
105
|
+
resetAfterMs: Number.isFinite(backoffOverrides.resetAfterMs) ? backoffOverrides.resetAfterMs : (3 * 60 * 1000)
|
|
106
|
+
};
|
|
96
107
|
if(!ctx._adaptiveReconnect){
|
|
97
|
-
const envBase = parseInt(process.env.NEXUS_MQTT_BACKOFF_BASE, 10) || 1000;
|
|
98
|
-
const envMax = parseInt(process.env.NEXUS_MQTT_BACKOFF_MAX, 10) || (5 * 60 * 1000);
|
|
99
|
-
const envFactor = parseFloat(process.env.NEXUS_MQTT_BACKOFF_FACTOR) || 2;
|
|
100
108
|
ctx._adaptiveReconnect = {
|
|
101
|
-
base:
|
|
102
|
-
max:
|
|
103
|
-
factor:
|
|
104
|
-
jitter:
|
|
109
|
+
base: resolved.base,
|
|
110
|
+
max: resolved.max,
|
|
111
|
+
factor: resolved.factor,
|
|
112
|
+
jitter: resolved.jitter,
|
|
113
|
+
resetAfterMs: resolved.resetAfterMs,
|
|
105
114
|
current: 0,
|
|
106
115
|
lastResetTs: 0,
|
|
107
116
|
consecutiveFails: 0 // Track consecutive failures
|
|
108
117
|
};
|
|
118
|
+
} else {
|
|
119
|
+
ctx._adaptiveReconnect.base = resolved.base;
|
|
120
|
+
ctx._adaptiveReconnect.max = resolved.max;
|
|
121
|
+
ctx._adaptiveReconnect.factor = resolved.factor;
|
|
122
|
+
ctx._adaptiveReconnect.jitter = resolved.jitter;
|
|
123
|
+
ctx._adaptiveReconnect.resetAfterMs = resolved.resetAfterMs;
|
|
109
124
|
}
|
|
110
125
|
return ctx._adaptiveReconnect;
|
|
111
126
|
}
|
|
@@ -121,6 +136,126 @@ function resetBackoff(state){
|
|
|
121
136
|
state.current = 0;
|
|
122
137
|
state.lastResetTs = Date.now();
|
|
123
138
|
}
|
|
139
|
+
function getForegroundState(ctx){
|
|
140
|
+
if (ctx && ctx.globalOptions) {
|
|
141
|
+
if (ctx.globalOptions.foreground === false) return false;
|
|
142
|
+
if (ctx.globalOptions.online === false) return false;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
function startForegroundRefresh(ctx){
|
|
147
|
+
if (ctx._foregroundRefreshInterval) {
|
|
148
|
+
clearInterval(ctx._foregroundRefreshInterval);
|
|
149
|
+
ctx._foregroundRefreshInterval = null;
|
|
150
|
+
}
|
|
151
|
+
const options = ctx.globalOptions || {};
|
|
152
|
+
if (options.foregroundRefreshEnabled === false) return;
|
|
153
|
+
const minutes = Number.isFinite(options.foregroundRefreshMinutes)
|
|
154
|
+
? Math.max(1, options.foregroundRefreshMinutes)
|
|
155
|
+
: 15;
|
|
156
|
+
ctx._foregroundRefreshInterval = setInterval(() => {
|
|
157
|
+
if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
|
|
158
|
+
try {
|
|
159
|
+
const foreground = getForegroundState(ctx);
|
|
160
|
+
ctx.mqttClient.publish("/foreground_state", JSON.stringify({ foreground }), { qos: 0, retain: false });
|
|
161
|
+
ctx.mqttClient.publish(
|
|
162
|
+
"/set_client_settings",
|
|
163
|
+
JSON.stringify({ make_user_available_when_in_foreground: true }),
|
|
164
|
+
{ qos: 0, retain: false }
|
|
165
|
+
);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (typeof log.verbose === 'function') {
|
|
168
|
+
log.verbose("listenMqtt", `Foreground refresh publish failed: ${err.message || err}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}, minutes * 60 * 1000);
|
|
172
|
+
}
|
|
173
|
+
function stopForegroundRefresh(ctx){
|
|
174
|
+
if (ctx && ctx._foregroundRefreshInterval) {
|
|
175
|
+
clearInterval(ctx._foregroundRefreshInterval);
|
|
176
|
+
ctx._foregroundRefreshInterval = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function getStormGuard(ctx){
|
|
180
|
+
if(!ctx._mqttStorm){
|
|
181
|
+
ctx._mqttStorm = {
|
|
182
|
+
events: [],
|
|
183
|
+
windowMs: 30 * 60 * 1000,
|
|
184
|
+
threshold: 12,
|
|
185
|
+
quietMs: 90 * 1000,
|
|
186
|
+
extraDelayMs: 120 * 1000,
|
|
187
|
+
quietUntil: 0,
|
|
188
|
+
active: false,
|
|
189
|
+
lastLogTs: 0
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return ctx._mqttStorm;
|
|
193
|
+
}
|
|
194
|
+
function noteStormEvent(ctx){
|
|
195
|
+
const guard = getStormGuard(ctx);
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
guard.events.push(now);
|
|
198
|
+
guard.events = guard.events.filter(ts => now - ts <= guard.windowMs);
|
|
199
|
+
if (guard.events.length >= guard.threshold) {
|
|
200
|
+
guard.active = true;
|
|
201
|
+
guard.quietUntil = now + guard.quietMs;
|
|
202
|
+
} else if (guard.active && now > guard.quietUntil) {
|
|
203
|
+
guard.active = false;
|
|
204
|
+
}
|
|
205
|
+
return guard;
|
|
206
|
+
}
|
|
207
|
+
function resetStormGuard(ctx){
|
|
208
|
+
if(ctx._mqttStorm){
|
|
209
|
+
ctx._mqttStorm.events = [];
|
|
210
|
+
ctx._mqttStorm.active = false;
|
|
211
|
+
ctx._mqttStorm.quietUntil = 0;
|
|
212
|
+
ctx._mqttStorm.lastLogTs = 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function shouldLogStorm(guard){
|
|
216
|
+
if(!guard || !guard.active) return true;
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
if(!guard.lastLogTs || now - guard.lastLogTs > guard.quietMs){
|
|
219
|
+
guard.lastLogTs = now;
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
async function runStormRecovery(ctx, api, defaultFuncs){
|
|
225
|
+
if(ctx._stormRecoveryRunning) return;
|
|
226
|
+
ctx._stormRecoveryRunning = true;
|
|
227
|
+
try {
|
|
228
|
+
log.warn('listenMqtt', 'Storm recovery triggered: validating session & refreshing tokens.');
|
|
229
|
+
try {
|
|
230
|
+
await utils.validateSession(ctx, defaultFuncs, { retries: 1, delayMs: 1000 });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
log.warn('listenMqtt', `Storm validateSession failed: ${err.message || err}`);
|
|
233
|
+
}
|
|
234
|
+
if (api && typeof api.refreshFb_dtsg === 'function') {
|
|
235
|
+
try {
|
|
236
|
+
await api.refreshFb_dtsg();
|
|
237
|
+
} catch (err) {
|
|
238
|
+
log.warn('listenMqtt', `Storm fb_dtsg refresh failed: ${err.message || err}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (ctx.globalSafety && typeof ctx.globalSafety.refreshSafeSession === 'function') {
|
|
242
|
+
try {
|
|
243
|
+
await ctx.globalSafety.refreshSafeSession();
|
|
244
|
+
} catch (err) {
|
|
245
|
+
log.warn('listenMqtt', `Storm safe session refresh failed: ${err.message || err}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
ctx._stormRecoveryRunning = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function scheduleStormRecovery(ctx, api, defaultFuncs, guard){
|
|
253
|
+
if(!guard || !guard.active) return;
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
if(ctx._nextStormRecovery && now < ctx._nextStormRecovery) return;
|
|
256
|
+
ctx._nextStormRecovery = now + guard.quietMs;
|
|
257
|
+
runStormRecovery(ctx, api, defaultFuncs);
|
|
258
|
+
}
|
|
124
259
|
// Build lazy preflight gating
|
|
125
260
|
function shouldRunPreflight(ctx){
|
|
126
261
|
if(ctx.globalOptions.disablePreflight) return false;
|
|
@@ -272,8 +407,8 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
272
407
|
}
|
|
273
408
|
})();
|
|
274
409
|
}
|
|
275
|
-
const chatOn = ctx.globalOptions.online;
|
|
276
|
-
const foreground =
|
|
410
|
+
const chatOn = (ctx.globalOptions && ctx.globalOptions.online === false) ? false : true;
|
|
411
|
+
const foreground = getForegroundState(ctx);
|
|
277
412
|
const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
|
|
278
413
|
const GUID = utils.getGUID();
|
|
279
414
|
const username = {
|
|
@@ -388,7 +523,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
388
523
|
fetchSeqID(defaultFuncs, api, ctx, (err) => {
|
|
389
524
|
if (err) {
|
|
390
525
|
log.warn("listenMqtt", "Failed to refresh SeqID on error, falling back to adaptive reconnect...");
|
|
391
|
-
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
526
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, 'seqid-refresh-failed');
|
|
392
527
|
} else {
|
|
393
528
|
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
394
529
|
}
|
|
@@ -403,48 +538,87 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
403
538
|
mqttClient.on('close', function () {
|
|
404
539
|
ctx.health.onDisconnect();
|
|
405
540
|
if (ctx.health && typeof ctx.health.incFailure === 'function') { ctx.health.incFailure(); }
|
|
541
|
+
stopForegroundRefresh(ctx);
|
|
406
542
|
|
|
407
543
|
const duration = Date.now() - attemptStartTs;
|
|
408
544
|
const seconds = Math.floor(duration / 1000);
|
|
409
545
|
|
|
546
|
+
const shortWindowMs = 5 * 60 * 1000;
|
|
547
|
+
const guard = duration < shortWindowMs ? noteStormEvent(ctx) : getStormGuard(ctx);
|
|
548
|
+
const allowLog = shouldLogStorm(guard);
|
|
549
|
+
const stormSuffix = guard && guard.active ? ` [storm:${guard.events.length}/${Math.round(guard.windowMs/60000)}m]` : '';
|
|
550
|
+
if (guard && guard.active) {
|
|
551
|
+
scheduleStormRecovery(ctx, api, defaultFuncs, guard);
|
|
552
|
+
}
|
|
410
553
|
// Treat long-lived connections as normal lifecycle, keep logs calm
|
|
411
554
|
if (duration >= 30 * 60 * 1000) { // >= 30 minutes
|
|
412
|
-
log.info('listenMqtt', `MQTT connection closed after ${seconds}s (normal lifecycle). Reconnecting
|
|
555
|
+
if (allowLog) log.info('listenMqtt', `MQTT connection closed after ${seconds}s (normal lifecycle). Reconnecting...${stormSuffix}`);
|
|
413
556
|
} else if (duration >= 5 * 60 * 1000) { // 5-30 minutes
|
|
414
|
-
log.info('listenMqtt', `MQTT connection closed after ${seconds}s (remote close). Reconnecting with backoff
|
|
557
|
+
if (allowLog) log.info('listenMqtt', `MQTT connection closed after ${seconds}s (remote close). Reconnecting with backoff...${stormSuffix}`);
|
|
558
|
+
} else if (duration >= 60 * 1000) {
|
|
559
|
+
if (allowLog) log.info('listenMqtt', `MQTT connection closed after ${seconds}s (short remote close). Reconnecting with backoff...${stormSuffix}`);
|
|
415
560
|
} else {
|
|
416
|
-
log.warn('listenMqtt', `MQTT bridge socket closed quickly after ${duration}ms (attempt=${ctx._mqttDiag.attempts})
|
|
561
|
+
if (allowLog) log.warn('listenMqtt', `MQTT bridge socket closed quickly after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).${stormSuffix}`);
|
|
417
562
|
}
|
|
418
563
|
|
|
419
564
|
if (!ctx.loggedIn) return; // avoid loops if logged out
|
|
420
565
|
if (ctx.globalOptions.autoReconnect) {
|
|
421
|
-
|
|
566
|
+
const backoffState = getBackoffState(ctx);
|
|
567
|
+
const resetThreshold = backoffState.resetAfterMs || (3 * 60 * 1000);
|
|
568
|
+
let reconnectReason = 'close';
|
|
569
|
+
if (duration >= resetThreshold) {
|
|
570
|
+
log.info('listenMqtt', `Resetting MQTT backoff after ${seconds}s healthy session.`);
|
|
571
|
+
resetBackoff(backoffState);
|
|
572
|
+
resetStormGuard(ctx);
|
|
573
|
+
reconnectReason = 'close-long';
|
|
574
|
+
}
|
|
575
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, reconnectReason);
|
|
422
576
|
}
|
|
423
577
|
});
|
|
424
578
|
|
|
425
579
|
mqttClient.on('disconnect', function(){
|
|
426
580
|
ctx.health.onDisconnect();
|
|
427
581
|
if (ctx.health && typeof ctx.health.incFailure === 'function') { ctx.health.incFailure(); }
|
|
582
|
+
stopForegroundRefresh(ctx);
|
|
428
583
|
|
|
429
584
|
const duration = Date.now() - attemptStartTs;
|
|
430
585
|
const seconds = Math.floor(duration / 1000);
|
|
431
586
|
|
|
587
|
+
const shortWindowMs = 5 * 60 * 1000;
|
|
588
|
+
const guard = duration < shortWindowMs ? noteStormEvent(ctx) : getStormGuard(ctx);
|
|
589
|
+
const allowLog = shouldLogStorm(guard);
|
|
590
|
+
const stormSuffix = guard && guard.active ? ` [storm:${guard.events.length}/${Math.round(guard.windowMs/60000)}m]` : '';
|
|
591
|
+
if (guard && guard.active) {
|
|
592
|
+
scheduleStormRecovery(ctx, api, defaultFuncs, guard);
|
|
593
|
+
}
|
|
432
594
|
if (duration >= 30 * 60 * 1000) {
|
|
433
|
-
log.info('listenMqtt', `MQTT disconnected after ${seconds}s (normal lifecycle). Reconnecting
|
|
595
|
+
if (allowLog) log.info('listenMqtt', `MQTT disconnected after ${seconds}s (normal lifecycle). Reconnecting...${stormSuffix}`);
|
|
434
596
|
} else if (duration >= 5 * 60 * 1000) {
|
|
435
|
-
log.info('listenMqtt', `MQTT disconnected after ${seconds}s (remote close). Reconnecting with backoff
|
|
597
|
+
if (allowLog) log.info('listenMqtt', `MQTT disconnected after ${seconds}s (remote close). Reconnecting with backoff...${stormSuffix}`);
|
|
598
|
+
} else if (duration >= 60 * 1000) {
|
|
599
|
+
if (allowLog) log.info('listenMqtt', `MQTT disconnected after ${seconds}s (short remote close). Reconnecting with backoff...${stormSuffix}`);
|
|
436
600
|
} else {
|
|
437
|
-
log.warn('listenMqtt', `MQTT bridge disconnect event after ${duration}ms (attempt=${ctx._mqttDiag.attempts})
|
|
601
|
+
if (allowLog) log.warn('listenMqtt', `MQTT bridge disconnect event after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).${stormSuffix}`);
|
|
438
602
|
}
|
|
439
603
|
|
|
440
604
|
if (!ctx.loggedIn) return;
|
|
441
605
|
if (ctx.globalOptions.autoReconnect) {
|
|
442
|
-
|
|
606
|
+
const backoffState = getBackoffState(ctx);
|
|
607
|
+
const resetThreshold = backoffState.resetAfterMs || (3 * 60 * 1000);
|
|
608
|
+
let reconnectReason = 'disconnect';
|
|
609
|
+
if (duration >= resetThreshold) {
|
|
610
|
+
log.info('listenMqtt', `Resetting MQTT backoff after ${seconds}s healthy session.`);
|
|
611
|
+
resetBackoff(backoffState);
|
|
612
|
+
resetStormGuard(ctx);
|
|
613
|
+
reconnectReason = 'disconnect-long';
|
|
614
|
+
}
|
|
615
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, reconnectReason);
|
|
443
616
|
}
|
|
444
617
|
});
|
|
445
618
|
|
|
446
619
|
mqttClient.on("connect", function () {
|
|
447
620
|
resetBackoff(backoff);
|
|
621
|
+
resetStormGuard(ctx);
|
|
448
622
|
backoff.consecutiveFails = 0; // Reset consecutive failures on successful connect
|
|
449
623
|
// Reset or wrap MQTT attempt counter so long-lived sessions don't look scary
|
|
450
624
|
ctx._mqttDiag = ctx._mqttDiag || {};
|
|
@@ -518,11 +692,12 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
518
692
|
JSON.stringify({ make_user_available_when_in_foreground: true }),
|
|
519
693
|
{ qos: 1 }
|
|
520
694
|
);
|
|
695
|
+
startForegroundRefresh(ctx);
|
|
521
696
|
// Replace fixed rTimeout reconnect with health-driven logic
|
|
522
697
|
const rTimeout = setTimeout(function () {
|
|
523
698
|
ctx.health.onError('timeout_no_t_ms');
|
|
524
699
|
mqttClient.end();
|
|
525
|
-
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
700
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, 'tms-timeout');
|
|
526
701
|
}, 5000);
|
|
527
702
|
ctx.tmsWait = function () {
|
|
528
703
|
clearTimeout(rTimeout);
|
|
@@ -619,11 +794,19 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
619
794
|
}, 55000 + Math.floor(Math.random()*20000));
|
|
620
795
|
}
|
|
621
796
|
}
|
|
622
|
-
function scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback){
|
|
797
|
+
function scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, reason){
|
|
623
798
|
const state = getBackoffState(ctx);
|
|
624
|
-
const
|
|
799
|
+
const guard = getStormGuard(ctx);
|
|
800
|
+
let delay = computeNextDelay(state);
|
|
801
|
+
if (guard && guard.active) {
|
|
802
|
+
delay = Math.min(state.max, delay + guard.extraDelayMs);
|
|
803
|
+
}
|
|
625
804
|
ctx.health.onReconnectScheduled(delay);
|
|
626
|
-
|
|
805
|
+
const tags = [];
|
|
806
|
+
if (reason) tags.push(reason);
|
|
807
|
+
if (guard && guard.active) tags.push('storm');
|
|
808
|
+
const suffix = tags.length ? ` (${tags.join(', ')})` : '';
|
|
809
|
+
log.warn('listenMqtt', `Reconnecting in ${delay} ms (adaptive backoff)${suffix}`);
|
|
627
810
|
setTimeout(()=>listenMqtt(defaultFuncs, api, ctx, globalCallback), delay);
|
|
628
811
|
}
|
|
629
812
|
function getTaskResponseData(taskType, payload) {
|