queen-mq 0.1.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/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- package/src/websocket/wsServer.js +221 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optimized Continuous Consumer Example
|
|
5
|
+
*
|
|
6
|
+
* This optimized version processes messages in parallel batches
|
|
7
|
+
* with batch acknowledgments for maximum throughput.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createQueenClient } from '../src/client/index.js';
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
// Create client with configuration
|
|
14
|
+
const client = createQueenClient({
|
|
15
|
+
baseUrl: process.env.QUEEN_URL || 'http://localhost:6632',
|
|
16
|
+
timeout: 35000 // Client timeout for non-polling requests
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const namespace = process.env.NS || 'smartchat';
|
|
20
|
+
const task = process.env.TASK || 'translations';
|
|
21
|
+
const queue = process.env.QUEUE || 'email';
|
|
22
|
+
|
|
23
|
+
// Optimized settings for high throughput
|
|
24
|
+
const batchSize = parseInt(process.env.BATCH_SIZE) || 50; // Reasonable batch size
|
|
25
|
+
const parallelWorkers = parseInt(process.env.WORKERS) || 10; // Process in parallel
|
|
26
|
+
const pollTimeout = parseInt(process.env.POLL_TIMEOUT) || 5000; // Shorter timeout for faster response
|
|
27
|
+
|
|
28
|
+
console.log('š Starting OPTIMIZED continuous consumer');
|
|
29
|
+
console.log(`š¦ Queue: ${namespace}/${task}/${queue}`);
|
|
30
|
+
console.log('ā” Performance settings:');
|
|
31
|
+
console.log(` - Batch size: ${batchSize}`);
|
|
32
|
+
console.log(` - Parallel workers: ${parallelWorkers}`);
|
|
33
|
+
console.log(` - Poll timeout: ${pollTimeout}ms`);
|
|
34
|
+
console.log('');
|
|
35
|
+
|
|
36
|
+
let stats = {
|
|
37
|
+
messages: 0,
|
|
38
|
+
errors: 0,
|
|
39
|
+
startTime: Date.now(),
|
|
40
|
+
lastMessageTime: null,
|
|
41
|
+
processingTimes: []
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Process message function
|
|
45
|
+
async function processMessage(message) {
|
|
46
|
+
// Your actual processing logic here
|
|
47
|
+
// For demo, just simulate minimal work
|
|
48
|
+
return new Promise(resolve => setImmediate(resolve));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Optimized consume function with parallel processing
|
|
52
|
+
async function consumeOptimized() {
|
|
53
|
+
let running = true;
|
|
54
|
+
const activeWorkers = new Set();
|
|
55
|
+
|
|
56
|
+
// Handle shutdown
|
|
57
|
+
process.on('SIGINT', () => {
|
|
58
|
+
console.log('\n\nš Shutting down consumer...');
|
|
59
|
+
running = false;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
while (running) {
|
|
63
|
+
try {
|
|
64
|
+
// Wait if we have too many active workers
|
|
65
|
+
while (activeWorkers.size >= parallelWorkers && running) {
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!running) break;
|
|
70
|
+
|
|
71
|
+
// Start a new worker
|
|
72
|
+
const worker = (async () => {
|
|
73
|
+
try {
|
|
74
|
+
// Pop messages with optimized settings
|
|
75
|
+
const result = await client.pop({
|
|
76
|
+
ns: namespace,
|
|
77
|
+
task: task,
|
|
78
|
+
queue: queue,
|
|
79
|
+
wait: true,
|
|
80
|
+
timeout: pollTimeout,
|
|
81
|
+
batch: batchSize
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.messages && result.messages.length > 0) {
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
|
|
87
|
+
// Process messages in parallel within the batch
|
|
88
|
+
const processPromises = result.messages.map(async (message) => {
|
|
89
|
+
try {
|
|
90
|
+
await processMessage(message);
|
|
91
|
+
stats.messages++;
|
|
92
|
+
stats.lastMessageTime = Date.now();
|
|
93
|
+
return { transactionId: message.transactionId, status: 'completed' };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
stats.errors++;
|
|
96
|
+
console.error(`Error processing message ${message.id}:`, error.message);
|
|
97
|
+
return { transactionId: message.transactionId, status: 'failed', error: error.message };
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Wait for all messages in batch to process
|
|
102
|
+
const acknowledgments = await Promise.all(processPromises);
|
|
103
|
+
|
|
104
|
+
// Batch acknowledge all messages at once
|
|
105
|
+
try {
|
|
106
|
+
await client.ackBatch(acknowledgments);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Batch ACK failed:', error);
|
|
109
|
+
// Fall back to individual ACKs if batch fails
|
|
110
|
+
for (const ack of acknowledgments) {
|
|
111
|
+
try {
|
|
112
|
+
await client.ack(ack.transactionId, ack.status, ack.error);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(`Individual ACK failed for ${ack.transactionId}:`, e.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const processingTime = Date.now() - startTime;
|
|
120
|
+
stats.processingTimes.push(processingTime);
|
|
121
|
+
|
|
122
|
+
// Keep only last 100 processing times for average calculation
|
|
123
|
+
if (stats.processingTimes.length > 100) {
|
|
124
|
+
stats.processingTimes.shift();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(`ā
Processed batch of ${result.messages.length} messages in ${processingTime}ms`);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error.name !== 'AbortError') {
|
|
131
|
+
console.error('Worker error:', error);
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
activeWorkers.delete(worker);
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
|
|
139
|
+
activeWorkers.add(worker);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Main loop error:', error);
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wait for all workers to complete
|
|
147
|
+
await Promise.all(Array.from(activeWorkers));
|
|
148
|
+
|
|
149
|
+
// Final stats
|
|
150
|
+
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
|
151
|
+
const avgProcessingTime = stats.processingTimes.length > 0
|
|
152
|
+
? stats.processingTimes.reduce((a, b) => a + b, 0) / stats.processingTimes.length
|
|
153
|
+
: 0;
|
|
154
|
+
|
|
155
|
+
console.log('\nš Final Statistics:');
|
|
156
|
+
console.log(` Total runtime: ${formatUptime(uptime)}`);
|
|
157
|
+
console.log(` Messages processed: ${stats.messages}`);
|
|
158
|
+
console.log(` Throughput: ${(stats.messages / uptime).toFixed(2)} msg/s`);
|
|
159
|
+
console.log(` Average batch processing time: ${avgProcessingTime.toFixed(2)}ms`);
|
|
160
|
+
console.log(` Errors encountered: ${stats.errors}`);
|
|
161
|
+
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Print statistics every 10 seconds
|
|
166
|
+
const statsInterval = setInterval(() => {
|
|
167
|
+
if (stats.messages === 0) return;
|
|
168
|
+
|
|
169
|
+
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
|
170
|
+
const throughput = stats.messages / uptime;
|
|
171
|
+
const avgProcessingTime = stats.processingTimes.length > 0
|
|
172
|
+
? stats.processingTimes.reduce((a, b) => a + b, 0) / stats.processingTimes.length
|
|
173
|
+
: 0;
|
|
174
|
+
|
|
175
|
+
console.log('\nš Statistics:');
|
|
176
|
+
console.log(` Uptime: ${formatUptime(uptime)}`);
|
|
177
|
+
console.log(` Messages processed: ${stats.messages}`);
|
|
178
|
+
console.log(` Throughput: ${throughput.toFixed(2)} msg/s`);
|
|
179
|
+
console.log(` Average batch processing: ${avgProcessingTime.toFixed(2)}ms`);
|
|
180
|
+
console.log(` Errors: ${stats.errors}`);
|
|
181
|
+
|
|
182
|
+
if (stats.lastMessageTime) {
|
|
183
|
+
const idle = Math.floor((Date.now() - stats.lastMessageTime) / 1000);
|
|
184
|
+
if (idle > 2) {
|
|
185
|
+
console.log(` ā ļø Idle time: ${idle}s`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.log('');
|
|
189
|
+
}, 10000);
|
|
190
|
+
|
|
191
|
+
console.log('ā
Consumer started. Press Ctrl+C to stop.\n');
|
|
192
|
+
|
|
193
|
+
// Start consuming
|
|
194
|
+
await consumeOptimized();
|
|
195
|
+
clearInterval(statsInterval);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatUptime(seconds) {
|
|
199
|
+
const hours = Math.floor(seconds / 3600);
|
|
200
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
201
|
+
const secs = seconds % 60;
|
|
202
|
+
|
|
203
|
+
const parts = [];
|
|
204
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
205
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
206
|
+
parts.push(`${secs}s`);
|
|
207
|
+
|
|
208
|
+
return parts.join(' ');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Run the consumer
|
|
212
|
+
main().catch(error => {
|
|
213
|
+
console.error('ā Fatal error:', error);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Continuous Consumer Example for Queen V2
|
|
5
|
+
*
|
|
6
|
+
* This example demonstrates how the Queen client performs continuous
|
|
7
|
+
* long polling without delays when timeouts occur. The consumer will
|
|
8
|
+
* immediately start a new request when the previous one times out,
|
|
9
|
+
* ensuring no messages are missed and minimizing latency.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createQueenClient } from '../src/client/index.js';
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
// Create client with configuration
|
|
16
|
+
const client = createQueenClient({
|
|
17
|
+
baseUrl: process.env.QUEEN_URL || 'http://localhost:6632',
|
|
18
|
+
timeout: 35000 // Client timeout for non-polling requests
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// V2: Using queue and optional partition
|
|
22
|
+
const queue = process.env.QUEUE || 'email-translations';
|
|
23
|
+
const partition = process.env.PARTITION; // Optional: if not set, consumes from any partition
|
|
24
|
+
const namespace = process.env.NAMESPACE; // Optional: for filtering
|
|
25
|
+
const task = process.env.TASK; // Optional: for filtering
|
|
26
|
+
|
|
27
|
+
console.log('š Starting continuous consumer (V2)');
|
|
28
|
+
if (queue) {
|
|
29
|
+
console.log(`š¦ Queue: ${queue}${partition ? '/' + partition : ' (any partition)'}`);
|
|
30
|
+
} else if (namespace || task) {
|
|
31
|
+
console.log(`š Filtering by: ${namespace ? 'namespace=' + namespace : ''} ${task ? 'task=' + task : ''}`);
|
|
32
|
+
}
|
|
33
|
+
console.log('ā±ļø Long polling timeout: 30 seconds');
|
|
34
|
+
console.log('š Will immediately retry on timeout (no delays)\n');
|
|
35
|
+
|
|
36
|
+
let stats = {
|
|
37
|
+
messages: 0,
|
|
38
|
+
errors: 0,
|
|
39
|
+
startTime: Date.now(),
|
|
40
|
+
lastMessageTime: null,
|
|
41
|
+
partitionCounts: {}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Start the consumer
|
|
45
|
+
const stop = client.consume({
|
|
46
|
+
queue: queue,
|
|
47
|
+
partition: partition,
|
|
48
|
+
namespace: namespace,
|
|
49
|
+
task: task,
|
|
50
|
+
handler: async (message) => {
|
|
51
|
+
stats.messages++;
|
|
52
|
+
stats.lastMessageTime = Date.now();
|
|
53
|
+
|
|
54
|
+
// Track messages per partition
|
|
55
|
+
const partitionKey = `${message.queue}/${message.partition}`;
|
|
56
|
+
stats.partitionCounts[partitionKey] = (stats.partitionCounts[partitionKey] || 0) + 1;
|
|
57
|
+
|
|
58
|
+
console.log(`āļø [${new Date().toISOString()}] Message received:`, {
|
|
59
|
+
queue: message.queue,
|
|
60
|
+
partition: message.partition,
|
|
61
|
+
transactionId: message.transactionId,
|
|
62
|
+
retryCount: message.retryCount,
|
|
63
|
+
data: message.data
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Simulate processing
|
|
67
|
+
await processMessage(message);
|
|
68
|
+
},
|
|
69
|
+
options: {
|
|
70
|
+
wait: true, // Enable long polling
|
|
71
|
+
timeout: 30000, // 30 second server timeout
|
|
72
|
+
batch: 10, // Process up to 10 messages at once
|
|
73
|
+
stopOnError: false // Continue processing on errors
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Print statistics every minute
|
|
78
|
+
const statsInterval = setInterval(() => {
|
|
79
|
+
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
|
80
|
+
const rate = stats.messages / (uptime / 60);
|
|
81
|
+
|
|
82
|
+
console.log('\nš Statistics:');
|
|
83
|
+
console.log(` Uptime: ${formatUptime(uptime)}`);
|
|
84
|
+
console.log(` Messages processed: ${stats.messages}`);
|
|
85
|
+
console.log(` Processing rate: ${rate.toFixed(2)} msg/min`);
|
|
86
|
+
console.log(` Errors: ${stats.errors}`);
|
|
87
|
+
|
|
88
|
+
// Show partition distribution
|
|
89
|
+
if (Object.keys(stats.partitionCounts).length > 0) {
|
|
90
|
+
console.log(' Distribution by partition:');
|
|
91
|
+
for (const [partition, count] of Object.entries(stats.partitionCounts)) {
|
|
92
|
+
const percentage = ((count / stats.messages) * 100).toFixed(1);
|
|
93
|
+
console.log(` ${partition}: ${count} messages (${percentage}%)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (stats.lastMessageTime) {
|
|
98
|
+
const idle = Math.floor((Date.now() - stats.lastMessageTime) / 1000);
|
|
99
|
+
console.log(` Idle time: ${idle}s`);
|
|
100
|
+
}
|
|
101
|
+
console.log('');
|
|
102
|
+
}, 60000);
|
|
103
|
+
|
|
104
|
+
// Handle shutdown
|
|
105
|
+
process.on('SIGINT', () => {
|
|
106
|
+
console.log('\n\nš Shutting down consumer...');
|
|
107
|
+
|
|
108
|
+
clearInterval(statsInterval);
|
|
109
|
+
stop();
|
|
110
|
+
|
|
111
|
+
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
|
112
|
+
console.log('\nš Final Statistics:');
|
|
113
|
+
console.log(` Total runtime: ${formatUptime(uptime)}`);
|
|
114
|
+
console.log(` Messages processed: ${stats.messages}`);
|
|
115
|
+
console.log(` Errors encountered: ${stats.errors}`);
|
|
116
|
+
|
|
117
|
+
if (Object.keys(stats.partitionCounts).length > 0) {
|
|
118
|
+
console.log(' Final partition distribution:');
|
|
119
|
+
for (const [partition, count] of Object.entries(stats.partitionCounts)) {
|
|
120
|
+
const percentage = ((count / stats.messages) * 100).toFixed(1);
|
|
121
|
+
console.log(` ${partition}: ${count} messages (${percentage}%)`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
process.exit(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log('ā
Consumer started. Press Ctrl+C to stop.\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function processMessage(message) {
|
|
132
|
+
// Simulate some processing work
|
|
133
|
+
const processingTime = Math.random() * 1000 + 500; // 0.5-1.5 seconds
|
|
134
|
+
await new Promise(resolve => setTimeout(resolve, processingTime));
|
|
135
|
+
|
|
136
|
+
// Randomly simulate occasional processing errors (5% chance)
|
|
137
|
+
if (Math.random() < 0.05) {
|
|
138
|
+
throw new Error('Simulated processing error');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatUptime(seconds) {
|
|
143
|
+
const hours = Math.floor(seconds / 3600);
|
|
144
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
145
|
+
const secs = seconds % 60;
|
|
146
|
+
|
|
147
|
+
const parts = [];
|
|
148
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
149
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
150
|
+
parts.push(`${secs}s`);
|
|
151
|
+
|
|
152
|
+
return parts.join(' ');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Run the consumer
|
|
156
|
+
main().catch(error => {
|
|
157
|
+
console.error('ā Fatal error:', error);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|