node-red-contrib-redis-variable 1.0.0 → 1.3.0
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 +259 -2
- package/package.json +6 -3
- package/redis-variable-config.html +17 -1
- package/redis-variable-config.js +115 -42
- package/redis-variable.html +77 -0
- package/redis-variable.js +410 -289
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ A comprehensive Node-RED node for Redis operations with flexible connection mana
|
|
|
26
26
|
- **SET** - Store value with optional TTL
|
|
27
27
|
- **DEL** - Delete single or multiple keys
|
|
28
28
|
- **EXISTS** - Check if single or multiple keys exist
|
|
29
|
+
- **MATCH** - Find keys by pattern using SCAN
|
|
29
30
|
|
|
30
31
|
#### **TTL Operations**
|
|
31
32
|
- **TTL** - Get remaining time to live in seconds
|
|
@@ -159,6 +160,61 @@ Port: Global Context → redis_config.port
|
|
|
159
160
|
Password: Flow Context → redis_password
|
|
160
161
|
```
|
|
161
162
|
|
|
163
|
+
**Setting up Global Context Variables:**
|
|
164
|
+
|
|
165
|
+
1. **In Node-RED Admin Panel:**
|
|
166
|
+
- Go to **Admin** → **Context** → **Global**
|
|
167
|
+
- Add variables:
|
|
168
|
+
- `redis_config.host` = `your-redis-host`
|
|
169
|
+
- `redis_config.port` = `6379`
|
|
170
|
+
- `redis_config.password` = `your-redis-password`
|
|
171
|
+
|
|
172
|
+
2. **Via Function Node:**
|
|
173
|
+
```javascript
|
|
174
|
+
// Set global context variables
|
|
175
|
+
flow.set("redis_config", {
|
|
176
|
+
host: "your-redis-host",
|
|
177
|
+
port: 6379,
|
|
178
|
+
password: "your-redis-password"
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
3. **Via HTTP API:**
|
|
183
|
+
```bash
|
|
184
|
+
curl -X POST http://localhost:1880/context/global/redis_config \
|
|
185
|
+
-H "Content-Type: application/json" \
|
|
186
|
+
-d '{"host":"your-redis-host","port":6379,"password":"your-redis-password"}'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Troubleshooting Global Context:**
|
|
190
|
+
- Ensure variable names match exactly (case-sensitive)
|
|
191
|
+
- Check Node-RED logs for context lookup messages
|
|
192
|
+
- Verify global context variables are set before Redis operations
|
|
193
|
+
|
|
194
|
+
**Testing Global Context Setup:**
|
|
195
|
+
|
|
196
|
+
1. **Set up test variables:**
|
|
197
|
+
```javascript
|
|
198
|
+
// In a Function node
|
|
199
|
+
flow.set("redis_config", {
|
|
200
|
+
host: "localhost",
|
|
201
|
+
port: 6379,
|
|
202
|
+
password: "your-password"
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
2. **Test connection:**
|
|
207
|
+
```javascript
|
|
208
|
+
// In another Function node
|
|
209
|
+
msg.payload = "test_key";
|
|
210
|
+
return msg;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
3. **Check logs:**
|
|
214
|
+
- Enable debug mode: `NODE_RED_DEBUG=1 node-red`
|
|
215
|
+
- Look for context lookup messages in Node-RED logs
|
|
216
|
+
- Verify connection parameters are correct
|
|
217
|
+
|
|
162
218
|
## Operations
|
|
163
219
|
|
|
164
220
|
**Universal Payload Interface**: All Redis operations use a unified `msg.payload` interface. Parameters can be passed as simple strings (for single keys) or as objects with specific properties. This provides flexibility while maintaining simplicity.
|
|
@@ -230,6 +286,82 @@ msg.payload = {
|
|
|
230
286
|
// Returns: { payload: { exists: true, count: 2, keys: ["key1", "key2", "key3"] } }
|
|
231
287
|
```
|
|
232
288
|
|
|
289
|
+
#### MATCH - Find Keys by Pattern
|
|
290
|
+
```javascript
|
|
291
|
+
// Simple pattern
|
|
292
|
+
msg.payload = "user:*";
|
|
293
|
+
// Returns: { payload: { pattern: "user:*", keys: ["user:123", "user:456"], count: 2, scanned: true } }
|
|
294
|
+
|
|
295
|
+
// Pattern with custom count
|
|
296
|
+
msg.payload = {
|
|
297
|
+
pattern: "session:*",
|
|
298
|
+
count: 50
|
|
299
|
+
};
|
|
300
|
+
// Returns: { payload: { pattern: "session:*", keys: ["session:abc123", "session:def456"], count: 2, limit: 50, scanned: true } }
|
|
301
|
+
|
|
302
|
+
// Pattern with pagination (cursor)
|
|
303
|
+
msg.payload = {
|
|
304
|
+
pattern: "user:*",
|
|
305
|
+
count: 30,
|
|
306
|
+
cursor: "12345"
|
|
307
|
+
};
|
|
308
|
+
// Returns: { payload: { pattern: "user:*", keys: ["user:31", "user:32"], count: 2, limit: 30, cursor: "67890", startCursor: "12345", scanned: true, truncated: false } }
|
|
309
|
+
|
|
310
|
+
// Pattern with skip (skip first N keys)
|
|
311
|
+
msg.payload = {
|
|
312
|
+
pattern: "session:*",
|
|
313
|
+
count: 30,
|
|
314
|
+
skip: 100
|
|
315
|
+
};
|
|
316
|
+
// Returns: { payload: { pattern: "session:*", keys: ["session:101", "session:102"], count: 2, limit: 30, cursor: "67890", startCursor: 0, scanned: true, truncated: false } }
|
|
317
|
+
|
|
318
|
+
// Complex patterns
|
|
319
|
+
msg.payload = "cache:page:*"; // All cache pages
|
|
320
|
+
msg.payload = "temp:*:data"; // Temporary data keys
|
|
321
|
+
msg.payload = "user:*:profile"; // User profiles
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Advanced MATCH Features:**
|
|
325
|
+
|
|
326
|
+
- **Pattern Matching**: Uses Redis SCAN with pattern matching for efficient key discovery
|
|
327
|
+
- **Count Limit**: Limit the number of keys returned (default: 100)
|
|
328
|
+
- **Cursor Pagination**: Use `cursor` parameter for efficient pagination through large datasets
|
|
329
|
+
- **Skip Keys**: Use `skip` parameter to skip the first N matching keys
|
|
330
|
+
- **Performance Optimized**: Uses Redis SCAN for non-blocking operation on large datasets
|
|
331
|
+
|
|
332
|
+
**Response Format:**
|
|
333
|
+
```javascript
|
|
334
|
+
{
|
|
335
|
+
"pattern": "user:*",
|
|
336
|
+
"keys": ["user:1", "user:2", "user:3"],
|
|
337
|
+
"count": 3, // Number of keys returned
|
|
338
|
+
"limit": 50, // Requested limit
|
|
339
|
+
"cursor": "67890", // Next cursor for pagination
|
|
340
|
+
"startCursor": "0", // Starting cursor
|
|
341
|
+
"scanned": true, // Operation completed
|
|
342
|
+
"truncated": false // true if results were limited by count
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Pagination Example:**
|
|
347
|
+
```javascript
|
|
348
|
+
// First request
|
|
349
|
+
msg.payload = {
|
|
350
|
+
pattern: "session:*",
|
|
351
|
+
count: 30
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Response contains cursor for next page
|
|
355
|
+
// Use that cursor in next request
|
|
356
|
+
msg.payload = {
|
|
357
|
+
pattern: "session:*",
|
|
358
|
+
count: 30,
|
|
359
|
+
cursor: "67890" // from previous response
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Continue until cursor becomes "0" (end of results)
|
|
363
|
+
```
|
|
364
|
+
|
|
233
365
|
#### TTL - Get Time To Live
|
|
234
366
|
```javascript
|
|
235
367
|
msg.payload = "mykey";
|
|
@@ -566,6 +698,24 @@ msg.payload = {
|
|
|
566
698
|
// Subscriber automatically receives notifications
|
|
567
699
|
```
|
|
568
700
|
|
|
701
|
+
### Key Discovery and Cleanup
|
|
702
|
+
```javascript
|
|
703
|
+
// Find all temporary keys
|
|
704
|
+
msg.payload = "temp:*";
|
|
705
|
+
// Returns: { pattern: "temp:*", keys: ["temp:cache1", "temp:cache2", "temp:session123"], count: 3, scanned: true }
|
|
706
|
+
|
|
707
|
+
// Find expired session keys
|
|
708
|
+
msg.payload = {
|
|
709
|
+
pattern: "session:*:expired",
|
|
710
|
+
count: 50
|
|
711
|
+
};
|
|
712
|
+
// Returns: { pattern: "session:*:expired", keys: ["session:abc:expired", "session:def:expired"], count: 2, scanned: true }
|
|
713
|
+
|
|
714
|
+
// Clean up old cache entries
|
|
715
|
+
msg.payload = "cache:old:*";
|
|
716
|
+
// Use returned keys with DEL operation for cleanup
|
|
717
|
+
```
|
|
718
|
+
|
|
569
719
|
## 📖 Usage Examples
|
|
570
720
|
|
|
571
721
|
### Basic Operations
|
|
@@ -613,6 +763,24 @@ msg.payload = {
|
|
|
613
763
|
// Returns: { success: true, deleted: 3, keys: [...] }
|
|
614
764
|
```
|
|
615
765
|
|
|
766
|
+
#### MATCH Operations
|
|
767
|
+
```javascript
|
|
768
|
+
// Find all user keys
|
|
769
|
+
msg.payload = "user:*";
|
|
770
|
+
// Returns: { pattern: "user:*", keys: ["user:123", "user:456", "user:789"], count: 3, scanned: true }
|
|
771
|
+
|
|
772
|
+
// Find session keys with custom scan count
|
|
773
|
+
msg.payload = {
|
|
774
|
+
pattern: "session:*",
|
|
775
|
+
count: 25
|
|
776
|
+
};
|
|
777
|
+
// Returns: { pattern: "session:*", keys: ["session:abc123", "session:def456"], count: 2, scanned: true }
|
|
778
|
+
|
|
779
|
+
// Find cache keys
|
|
780
|
+
msg.payload = "cache:*";
|
|
781
|
+
// Returns: { pattern: "cache:*", keys: ["cache:page1", "cache:page2", "cache:api"], count: 3, scanned: true }
|
|
782
|
+
```
|
|
783
|
+
|
|
616
784
|
### TTL Operations
|
|
617
785
|
|
|
618
786
|
#### Check TTL
|
|
@@ -738,19 +906,55 @@ msg.payload = {
|
|
|
738
906
|
|
|
739
907
|
## Troubleshooting
|
|
740
908
|
|
|
909
|
+
### SSL/TLS Connection Issues
|
|
910
|
+
|
|
911
|
+
If you encounter the error `"Protocol error, got "\u0015" as reply type byte"`, this indicates an SSL/TLS configuration problem:
|
|
912
|
+
|
|
913
|
+
**Solution**: Disable certificate verification in the SSL/TLS configuration:
|
|
914
|
+
1. Enable SSL/TLS in the configuration node
|
|
915
|
+
2. **Uncheck** "Verify Certificate" (Reject unauthorized certificates)
|
|
916
|
+
3. This allows connections to servers with self-signed or invalid certificates
|
|
917
|
+
|
|
918
|
+
**Common scenarios where this is needed:**
|
|
919
|
+
- Self-signed certificates in development environments
|
|
920
|
+
- Local Redis servers with SSL enabled
|
|
921
|
+
- Cloud Redis services with custom certificates
|
|
922
|
+
- Test environments with temporary certificates
|
|
923
|
+
|
|
924
|
+
**Security Note**: Disabling certificate verification reduces security. Only use this in trusted environments or when you're certain about the server's identity.
|
|
925
|
+
|
|
741
926
|
### Common Issues
|
|
742
927
|
|
|
743
928
|
1. **Connection Refused**: Check Redis server is running and accessible
|
|
744
|
-
2. **Authentication Failed**: Verify username/password configuration
|
|
929
|
+
2. **Authentication Failed**: Verify username/password configuration
|
|
745
930
|
3. **Timeout Errors**: Increase connection timeout in advanced options
|
|
746
931
|
4. **Memory Issues**: Monitor Redis memory usage and configure appropriate limits
|
|
747
932
|
|
|
748
933
|
### Debug Mode
|
|
934
|
+
|
|
749
935
|
Enable Node-RED debug mode to see detailed connection and operation logs:
|
|
750
936
|
```bash
|
|
751
937
|
DEBUG=redis* node-red
|
|
752
938
|
```
|
|
753
939
|
|
|
940
|
+
**Context Debugging:**
|
|
941
|
+
Enable context debugging by setting the environment variable:
|
|
942
|
+
```bash
|
|
943
|
+
NODE_RED_DEBUG=1 node-red
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
Check Node-RED logs for messages like:
|
|
947
|
+
```
|
|
948
|
+
Context lookup - Type: global, Path: redis_config.host, Result: your-redis-host
|
|
949
|
+
Redis connection config - Host: your-redis-host, Port: 6379, Database: 0, Username: not set, Password: set
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
**Common Context Issues:**
|
|
953
|
+
1. **Variable not found**: Check exact variable name spelling
|
|
954
|
+
2. **Nested objects**: Use dot notation (e.g., `redis_config.host`)
|
|
955
|
+
3. **Context type mismatch**: Ensure correct context type is selected
|
|
956
|
+
4. **Timing issues**: Set context variables before Redis operations
|
|
957
|
+
|
|
754
958
|
## Contributing
|
|
755
959
|
|
|
756
960
|
Contributions are welcome! Please read the contributing guidelines and submit pull requests to the GitHub repository.
|
|
@@ -761,9 +965,62 @@ MIT License - see LICENSE file for details.
|
|
|
761
965
|
|
|
762
966
|
## Changelog
|
|
763
967
|
|
|
968
|
+
### v1.1.0
|
|
969
|
+
- **Enhanced MATCH Operation**: Added advanced pattern matching with pagination support
|
|
970
|
+
- **Cursor Pagination**: Efficient pagination through large datasets using Redis SCAN cursors
|
|
971
|
+
- **Skip Functionality**: Skip first N matching keys for offset-based pagination
|
|
972
|
+
- **Count Limiting**: Improved count parameter handling for precise result limiting
|
|
973
|
+
- **Performance Optimization**: Better SCAN integration for non-blocking operations
|
|
974
|
+
- **Improved Response Format**: Enhanced MATCH response with pagination metadata
|
|
975
|
+
- Added `cursor`, `startCursor`, `limit`, and `truncated` fields
|
|
976
|
+
- Better error handling and validation
|
|
977
|
+
- **Production Ready**: Removed debug logging and optimized for production use
|
|
978
|
+
- **Updated Documentation**: Comprehensive examples for all MATCH features
|
|
979
|
+
- **Enhanced Error Handling**: Better validation and error messages
|
|
980
|
+
|
|
764
981
|
### v1.0.0
|
|
765
982
|
- Initial release
|
|
766
983
|
- Complete Redis operations support
|
|
767
984
|
- Flexible connection management
|
|
768
985
|
- Modern ioredis integration
|
|
769
|
-
- Comprehensive documentation
|
|
986
|
+
- Comprehensive documentation
|
|
987
|
+
|
|
988
|
+
### Pattern Matching (MATCH)
|
|
989
|
+
|
|
990
|
+
Find keys by pattern using Redis SCAN:
|
|
991
|
+
|
|
992
|
+
```javascript
|
|
993
|
+
// Find all keys starting with "user:"
|
|
994
|
+
msg.payload = {
|
|
995
|
+
operation: "match",
|
|
996
|
+
pattern: "user:*"
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// Find keys with specific pattern and custom scan count
|
|
1000
|
+
msg.payload = {
|
|
1001
|
+
operation: "match",
|
|
1002
|
+
pattern: "session:*:active",
|
|
1003
|
+
count: 50 // Number of keys to scan per iteration
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// Simple pattern matching
|
|
1007
|
+
msg.payload = "temp:*"; // Find all keys starting with "temp:"
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**Response format:**
|
|
1011
|
+
```javascript
|
|
1012
|
+
{
|
|
1013
|
+
pattern: "user:*",
|
|
1014
|
+
keys: ["user:1", "user:2", "user:admin"],
|
|
1015
|
+
count: 3,
|
|
1016
|
+
scanned: true
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**Pattern examples:**
|
|
1021
|
+
- `user:*` - All keys starting with "user:"
|
|
1022
|
+
- `*:active` - All keys ending with ":active"
|
|
1023
|
+
- `session:*:data` - Keys with "session:" prefix and ":data" suffix
|
|
1024
|
+
- `temp_*` - Keys starting with "temp_"
|
|
1025
|
+
|
|
1026
|
+
### Hash Operations
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-redis-variable",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A comprehensive Node-RED node for Redis operations with universal payload-based configuration, automatic JSON handling,
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "A comprehensive Node-RED node for Redis operations with universal payload-based configuration, automatic JSON handling, SSL/TLS support, and advanced pattern matching with pagination",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|
|
7
7
|
"redis",
|
|
8
8
|
"database",
|
|
9
9
|
"cache",
|
|
10
10
|
"variable",
|
|
11
|
-
"storage"
|
|
11
|
+
"storage",
|
|
12
|
+
"pattern-matching",
|
|
13
|
+
"pagination",
|
|
14
|
+
"json-handling"
|
|
12
15
|
],
|
|
13
16
|
"homepage": "https://github.com/lotockii/node-red-contrib-redis-variable#readme",
|
|
14
17
|
"bugs": {
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
tlsCaContext: { value: "" },
|
|
29
29
|
options: { value: "{}" },
|
|
30
30
|
optionsType: { value: "json" },
|
|
31
|
-
optionsContext: { value: "" }
|
|
31
|
+
optionsContext: { value: "" },
|
|
32
|
+
debugNoSSL: { value: false }
|
|
32
33
|
},
|
|
33
34
|
credentials: {
|
|
34
35
|
password: { type: "password" },
|
|
@@ -290,6 +291,18 @@
|
|
|
290
291
|
$("#node-config-input-tlsCa-visible").on("input", function() {
|
|
291
292
|
$("#node-config-input-tlsCa").val($(this).val());
|
|
292
293
|
});
|
|
294
|
+
|
|
295
|
+
// Debug option
|
|
296
|
+
$("#node-config-input-debugNoSSL").prop('checked', this.debugNoSSL === true);
|
|
297
|
+
|
|
298
|
+
// Show/hide TLS options based on debug setting
|
|
299
|
+
$("#node-config-input-debugNoSSL").change(function() {
|
|
300
|
+
if ($(this).is(':checked')) {
|
|
301
|
+
$("#ssl-config-section").hide();
|
|
302
|
+
} else {
|
|
303
|
+
$("#ssl-config-section").show();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
293
306
|
},
|
|
294
307
|
oneditsave: function() {
|
|
295
308
|
// Save basic configuration
|
|
@@ -431,6 +444,9 @@
|
|
|
431
444
|
this.tlsCaContext = tlsCaValue || '';
|
|
432
445
|
$("#node-config-input-tlsCa").val('');
|
|
433
446
|
}
|
|
447
|
+
|
|
448
|
+
// Debug option
|
|
449
|
+
this.debugNoSSL = $("#node-config-input-debugNoSSL").is(':checked');
|
|
434
450
|
}
|
|
435
451
|
});
|
|
436
452
|
</script>
|
package/redis-variable-config.js
CHANGED
|
@@ -4,14 +4,6 @@ module.exports = function (RED) {
|
|
|
4
4
|
let connections = {};
|
|
5
5
|
let usedConn = {};
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Helper function to get value from different contexts
|
|
9
|
-
* @param {Object} node - Node instance
|
|
10
|
-
* @param {string} value - Value to get
|
|
11
|
-
* @param {string} type - Type of value (str, flow, global, env)
|
|
12
|
-
* @param {Object} msg - Message object
|
|
13
|
-
* @returns {string} Retrieved value
|
|
14
|
-
*/
|
|
15
7
|
function getValueFromContext(node, value, type, msg) {
|
|
16
8
|
if (value === null || value === undefined) return null;
|
|
17
9
|
|
|
@@ -55,14 +47,21 @@ module.exports = function (RED) {
|
|
|
55
47
|
if (path.includes('.')) {
|
|
56
48
|
const parts = path.split('.');
|
|
57
49
|
let result = context.get(parts[0]);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
|
|
51
|
+
// If the first part returns an object, traverse it
|
|
52
|
+
if (result && typeof result === 'object') {
|
|
53
|
+
for (let i = 1; i < parts.length; i++) {
|
|
54
|
+
if (result && typeof result === 'object' && result[parts[i]] !== undefined) {
|
|
55
|
+
result = result[parts[i]];
|
|
56
|
+
} else {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
63
59
|
}
|
|
60
|
+
return result;
|
|
61
|
+
} else {
|
|
62
|
+
// If first part is not an object, try to get the full path as a single value
|
|
63
|
+
return context.get(path);
|
|
64
64
|
}
|
|
65
|
-
return result;
|
|
66
65
|
} else {
|
|
67
66
|
return context.get(path);
|
|
68
67
|
}
|
|
@@ -129,6 +128,9 @@ module.exports = function (RED) {
|
|
|
129
128
|
break;
|
|
130
129
|
case 'global':
|
|
131
130
|
result = getValueFromContext(executingNode || this, value, 'global', msg);
|
|
131
|
+
if (executingNode && process.env.NODE_RED_DEBUG) {
|
|
132
|
+
executingNode.log(`Context lookup - Type: global, Path: ${value}, Result: ${result}`);
|
|
133
|
+
}
|
|
132
134
|
break;
|
|
133
135
|
case 'env':
|
|
134
136
|
result = process.env[value] || null;
|
|
@@ -158,29 +160,29 @@ module.exports = function (RED) {
|
|
|
158
160
|
// Get Redis connection options
|
|
159
161
|
this.getConnectionOptions = function(msg, executingNode) {
|
|
160
162
|
try {
|
|
161
|
-
// Parse host
|
|
162
|
-
let host
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
|
|
166
|
-
executingNode
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Parse port
|
|
170
|
-
let port
|
|
171
|
-
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
executingNode
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Parse database
|
|
178
|
-
let database
|
|
179
|
-
|
|
180
|
-
this.
|
|
181
|
-
|
|
182
|
-
executingNode
|
|
183
|
-
|
|
163
|
+
// Parse host - handle both typedInput and direct context
|
|
164
|
+
let host;
|
|
165
|
+
if (this.hostType === 'str') {
|
|
166
|
+
host = this.host || 'localhost';
|
|
167
|
+
} else {
|
|
168
|
+
host = this.parseCredentialValue(this.hostContext, this.hostType, msg, executingNode) || 'localhost';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse port - handle both typedInput and direct context
|
|
172
|
+
let port;
|
|
173
|
+
if (this.portType === 'str') {
|
|
174
|
+
port = this.port || 6379;
|
|
175
|
+
} else {
|
|
176
|
+
port = this.parseCredentialValue(this.portContext, this.portType, msg, executingNode) || 6379;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Parse database - handle both typedInput and direct context
|
|
180
|
+
let database;
|
|
181
|
+
if (this.databaseType === 'str') {
|
|
182
|
+
database = this.database || 0;
|
|
183
|
+
} else {
|
|
184
|
+
database = this.parseCredentialValue(this.databaseContext, this.databaseType, msg, executingNode) || 0;
|
|
185
|
+
}
|
|
184
186
|
|
|
185
187
|
// Parse password
|
|
186
188
|
let password = null;
|
|
@@ -273,6 +275,9 @@ module.exports = function (RED) {
|
|
|
273
275
|
if (!this.tlsRejectUnauthorized) {
|
|
274
276
|
connectionOptions.tls.rejectUnauthorized = false;
|
|
275
277
|
}
|
|
278
|
+
} else {
|
|
279
|
+
// Explicitly disable TLS for non-SSL connections
|
|
280
|
+
connectionOptions.tls = false;
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
return connectionOptions;
|
|
@@ -298,22 +303,76 @@ module.exports = function (RED) {
|
|
|
298
303
|
|
|
299
304
|
const options = this.getConnectionOptions(msg, executingNode);
|
|
300
305
|
|
|
306
|
+
// Add connection limits to prevent infinite retry loops
|
|
307
|
+
const connectionOptions = {
|
|
308
|
+
...options,
|
|
309
|
+
maxRetriesPerRequest: 1, // Limit retries per request
|
|
310
|
+
retryDelayOnFailover: 100,
|
|
311
|
+
enableReadyCheck: false,
|
|
312
|
+
lazyConnect: true, // Don't connect immediately
|
|
313
|
+
connectTimeout: 5000, // 5 second timeout
|
|
314
|
+
commandTimeout: 3000, // 3 second command timeout
|
|
315
|
+
// Disable automatic reconnection to prevent error loops
|
|
316
|
+
retryDelayOnClusterDown: 0,
|
|
317
|
+
retryDelayOnFailover: 0,
|
|
318
|
+
maxRetriesPerRequest: 0
|
|
319
|
+
};
|
|
320
|
+
|
|
301
321
|
// Create Redis client
|
|
302
322
|
let client;
|
|
303
323
|
if (this.cluster) {
|
|
304
324
|
// For cluster mode, options should be an array of nodes
|
|
305
|
-
const clusterNodes = Array.isArray(
|
|
325
|
+
const clusterNodes = Array.isArray(connectionOptions) ? connectionOptions : [connectionOptions];
|
|
306
326
|
client = new Redis.Cluster(clusterNodes);
|
|
307
327
|
} else {
|
|
308
|
-
client = new Redis(
|
|
328
|
+
client = new Redis(connectionOptions);
|
|
309
329
|
}
|
|
310
330
|
|
|
331
|
+
// Track error state to prevent spam
|
|
332
|
+
let errorReported = false;
|
|
333
|
+
let lastErrorTime = 0;
|
|
334
|
+
const ERROR_REPORT_INTERVAL = 30000; // Report errors only once per 30 seconds
|
|
335
|
+
|
|
311
336
|
// Handle connection errors
|
|
312
337
|
client.on("error", (e) => {
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
|
|
340
|
+
// Only report errors once per interval to prevent spam
|
|
341
|
+
if (!errorReported || (now - lastErrorTime) > ERROR_REPORT_INTERVAL) {
|
|
342
|
+
let errorMsg = `Redis connection error: ${e.message}`;
|
|
343
|
+
|
|
344
|
+
// Add specific diagnostics for common SSL issues
|
|
345
|
+
if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
|
|
346
|
+
errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
|
|
347
|
+
errorMsg += "\n1. Disable SSL/TLS if your Redis server doesn't support it";
|
|
348
|
+
errorMsg += "\n2. Use port 6380 for Redis SSL instead of 6379";
|
|
349
|
+
errorMsg += "\n3. Check if your Redis server is configured for SSL";
|
|
350
|
+
errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (executingNode) {
|
|
354
|
+
executingNode.error(errorMsg, {});
|
|
355
|
+
} else {
|
|
356
|
+
this.error(errorMsg, {});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
errorReported = true;
|
|
360
|
+
lastErrorTime = now;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Handle successful connection
|
|
365
|
+
client.on("connect", () => {
|
|
366
|
+
errorReported = false;
|
|
313
367
|
if (executingNode) {
|
|
314
|
-
executingNode.
|
|
315
|
-
}
|
|
316
|
-
|
|
368
|
+
executingNode.status({ fill: "green", shape: "dot", text: "connected" });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Handle disconnection
|
|
373
|
+
client.on("disconnect", () => {
|
|
374
|
+
if (executingNode) {
|
|
375
|
+
executingNode.status({ fill: "red", shape: "ring", text: "disconnected" });
|
|
317
376
|
}
|
|
318
377
|
});
|
|
319
378
|
|
|
@@ -348,6 +407,20 @@ module.exports = function (RED) {
|
|
|
348
407
|
}
|
|
349
408
|
};
|
|
350
409
|
|
|
410
|
+
// Force disconnect method for error recovery
|
|
411
|
+
this.forceDisconnect = function(nodeId) {
|
|
412
|
+
const id = nodeId || this.id;
|
|
413
|
+
if (connections[id]) {
|
|
414
|
+
try {
|
|
415
|
+
connections[id].disconnect();
|
|
416
|
+
} catch (e) {
|
|
417
|
+
// Ignore disconnect errors
|
|
418
|
+
}
|
|
419
|
+
delete connections[id];
|
|
420
|
+
delete usedConn[id];
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
351
424
|
// Clean up on node close
|
|
352
425
|
this.on('close', function() {
|
|
353
426
|
this.disconnect();
|