forked from SmartDropLabs/smartdrop-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcache.js
More file actions
165 lines (150 loc) · 4.48 KB
/
Copy pathcache.js
File metadata and controls
165 lines (150 loc) · 4.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
const Redis = require('ioredis');
const config = require('../config');
const logger = require('../logger');
const Semaphore = require('../utils/semaphore');
const MAX_RETRIES = 10;
const RETRY_DELAY_MS = 1000;
const CONNECT_TIMEOUT_MS = 5000;
const COMMAND_TIMEOUT_MS = 3000;
const COMMAND_QUEUE_WARN_THRESHOLD = parseInt(process.env.REDIS_COMMAND_QUEUE_WARN_THRESHOLD, 10) || 100;
const COMMAND_QUEUE_BACKPRESSURE_THRESHOLD = parseInt(process.env.REDIS_COMMAND_QUEUE_BACKPRESSURE_THRESHOLD, 10) || 500;
let client = null;
let reconnectAttempts = 0;
// Concurrency limiter to prevent Redis connection pool exhaustion (issue #249).
// Limits concurrent in-flight Redis commands to prevent queue buildup.
const MAX_CONCURRENT_OPS = parseInt(process.env.REDIS_MAX_CONCURRENT_OPS, 10) || 50;
const operationSemaphore = new Semaphore(MAX_CONCURRENT_OPS);
let consecutiveQueueWarnings = 0;
function _checkQueueBackpressure(caller) {
const queueLen = getCommandQueueLength();
if (queueLen > COMMAND_QUEUE_BACKPRESSURE_THRESHOLD) {
consecutiveQueueWarnings++;
if (consecutiveQueueWarnings % 10 === 1) {
logger.error('Redis command queue critically deep — backpressure active', {
queue_length: queueLen,
threshold: COMMAND_QUEUE_BACKPRESSURE_THRESHOLD,
caller,
consecutive_warnings: consecutiveQueueWarnings,
});
}
return true;
}
if (queueLen > COMMAND_QUEUE_WARN_THRESHOLD) {
consecutiveQueueWarnings++;
if (consecutiveQueueWarnings % 5 === 1) {
logger.warn('Redis command queue depth high', {
queue_length: queueLen,
threshold: COMMAND_QUEUE_WARN_THRESHOLD,
caller,
});
}
return false;
}
if (consecutiveQueueWarnings > 0) {
logger.info('Redis command queue depth recovered', { queue_length: queueLen, caller });
consecutiveQueueWarnings = 0;
}
return false;
}
function getClient() {
if (!client) {
client = new Redis(config.redis.url, {
lazyConnect: true,
enableOfflineQueue: true,
connectTimeout: CONNECT_TIMEOUT_MS,
commandTimeout: COMMAND_TIMEOUT_MS,
retryStrategy(times) {
if (times > MAX_RETRIES) {
logger.error('Redis max reconnection attempts reached', { attempts: times });
return null;
}
const delay = Math.min(times * RETRY_DELAY_MS, 30000);
logger.warn('Redis reconnecting', { attempt: times, delayMs: delay });
return delay;
},
maxRetriesPerRequest: 3,
});
client.on('error', (err) => {
reconnectAttempts++;
logger.error('Redis connection error', { error: err.message, reconnectAttempts });
});
client.on('connect', () => {
reconnectAttempts = 0;
logger.info('Redis connected');
});
client.on('ready', () => {
reconnectAttempts = 0;
logger.info('Redis ready');
});
client.on('close', () => {
logger.warn('Redis connection closed');
});
client.connect().catch(() => {});
}
return client;
}
function isConnected() {
return client !== null && client.status === 'ready';
}
function getCommandQueueLength() {
if (!client) return 0;
return client.commandQueue ? client.commandQueue.length : 0;
}
function getConcurrencyStats() {
return {
active: operationSemaphore.active,
waiting: operationSemaphore.waiting,
available: operationSemaphore.available,
max: MAX_CONCURRENT_OPS,
};
}
async function get(key) {
const release = await operationSemaphore.acquire(5000);
try {
_checkQueueBackpressure('get');
const redis = getClient();
const data = await redis.get(key);
if (!data) return null;
try {
return JSON.parse(data);
} catch {
return data;
}
} finally {
release();
}
}
async function set(key, value, ttlSeconds) {
const release = await operationSemaphore.acquire(5000);
try {
_checkQueueBackpressure('set');
const redis = getClient();
const serialized = JSON.stringify(value);
if (ttlSeconds) {
await redis.setex(key, ttlSeconds, serialized);
} else {
await redis.set(key, serialized);
}
} finally {
release();
}
}
async function del(key) {
const release = await operationSemaphore.acquire(5000);
try {
const redis = getClient();
await redis.del(key);
} finally {
release();
}
}
async function disconnect() {
if (client) {
await client.quit();
client = null;
}
}
module.exports = {
get, set, del, disconnect, getClient, isConnected,
getCommandQueueLength, getConcurrencyStats,
};