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,333 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="queues-view">
|
|
3
|
+
<Card class="queues-card">
|
|
4
|
+
<template #content>
|
|
5
|
+
<DataTable
|
|
6
|
+
:value="queues"
|
|
7
|
+
:loading="loading"
|
|
8
|
+
:paginator="true"
|
|
9
|
+
:rows="10"
|
|
10
|
+
:rowsPerPageOptions="[10, 20, 50]"
|
|
11
|
+
responsiveLayout="scroll"
|
|
12
|
+
filterDisplay="row"
|
|
13
|
+
:globalFilterFields="['name', 'namespace', 'task']"
|
|
14
|
+
class="dark-table-v3"
|
|
15
|
+
>
|
|
16
|
+
<template #header>
|
|
17
|
+
<div class="table-header">
|
|
18
|
+
<h2>Queue Management</h2>
|
|
19
|
+
<div class="table-actions">
|
|
20
|
+
<span class="p-input-icon-left">
|
|
21
|
+
<i class="pi pi-search" />
|
|
22
|
+
<InputText v-model="filters.global" placeholder="Search queues..." class="search-input" />
|
|
23
|
+
</span>
|
|
24
|
+
<Button icon="pi pi-refresh" @click="fetchQueues" :loading="loading" class="p-button-text" />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<Column field="name" header="Queue Name" :sortable="true">
|
|
30
|
+
<template #body="{ data }">
|
|
31
|
+
<div class="queue-name-cell">
|
|
32
|
+
<div class="queue-icon">Q</div>
|
|
33
|
+
<router-link :to="`/queues/${data.name}`" class="queue-link">
|
|
34
|
+
{{ data.name }}
|
|
35
|
+
</router-link>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
</Column>
|
|
39
|
+
|
|
40
|
+
<Column field="namespace" header="Namespace" :sortable="true">
|
|
41
|
+
<template #body="{ data }">
|
|
42
|
+
<Tag v-if="data.namespace" :value="data.namespace" class="namespace-tag" />
|
|
43
|
+
<span v-else class="text-muted">-</span>
|
|
44
|
+
</template>
|
|
45
|
+
</Column>
|
|
46
|
+
|
|
47
|
+
<Column field="task" header="Task" :sortable="true">
|
|
48
|
+
<template #body="{ data }">
|
|
49
|
+
<Tag v-if="data.task" :value="data.task" class="task-tag" />
|
|
50
|
+
<span v-else class="text-muted">-</span>
|
|
51
|
+
</template>
|
|
52
|
+
</Column>
|
|
53
|
+
|
|
54
|
+
<Column header="Status">
|
|
55
|
+
<template #body="{ data }">
|
|
56
|
+
<div class="status-badges">
|
|
57
|
+
<span class="status-pending">{{ data.stats?.pending || 0 }} pending</span>
|
|
58
|
+
<span class="status-processing">{{ data.stats?.processing || 0 }} processing</span>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
</Column>
|
|
62
|
+
|
|
63
|
+
<Column field="stats.completed" header="Completed" :sortable="true">
|
|
64
|
+
<template #body="{ data }">
|
|
65
|
+
<span class="status-completed">{{ data.stats?.completed || 0 }}</span>
|
|
66
|
+
</template>
|
|
67
|
+
</Column>
|
|
68
|
+
|
|
69
|
+
<Column field="stats.failed" header="Failed" :sortable="true">
|
|
70
|
+
<template #body="{ data }">
|
|
71
|
+
<span class="status-failed">{{ data.stats?.failed || 0 }}</span>
|
|
72
|
+
</template>
|
|
73
|
+
</Column>
|
|
74
|
+
|
|
75
|
+
<Column header="Actions" :exportable="false" style="min-width: 8rem">
|
|
76
|
+
<template #body="{ data }">
|
|
77
|
+
<Button
|
|
78
|
+
icon="pi pi-cog"
|
|
79
|
+
class="p-button-text p-button-sm action-btn"
|
|
80
|
+
@click="configureQueue(data)"
|
|
81
|
+
v-tooltip="'Configure'"
|
|
82
|
+
/>
|
|
83
|
+
<Button
|
|
84
|
+
icon="pi pi-trash"
|
|
85
|
+
class="p-button-text p-button-sm p-button-danger action-btn"
|
|
86
|
+
@click="deleteQueue(data)"
|
|
87
|
+
v-tooltip="'Delete'"
|
|
88
|
+
/>
|
|
89
|
+
</template>
|
|
90
|
+
</Column>
|
|
91
|
+
</DataTable>
|
|
92
|
+
</template>
|
|
93
|
+
</Card>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<script setup>
|
|
98
|
+
import { ref, onMounted } from 'vue'
|
|
99
|
+
import { useRouter } from 'vue-router'
|
|
100
|
+
import { useToast } from 'primevue/usetoast'
|
|
101
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
102
|
+
import DataTable from 'primevue/datatable'
|
|
103
|
+
import Column from 'primevue/column'
|
|
104
|
+
import InputText from 'primevue/inputtext'
|
|
105
|
+
import Button from 'primevue/button'
|
|
106
|
+
import Tag from 'primevue/tag'
|
|
107
|
+
import Card from 'primevue/card'
|
|
108
|
+
import api from '../services/api.js'
|
|
109
|
+
|
|
110
|
+
const router = useRouter()
|
|
111
|
+
const toast = useToast()
|
|
112
|
+
const confirm = useConfirm()
|
|
113
|
+
|
|
114
|
+
const loading = ref(false)
|
|
115
|
+
const queues = ref([])
|
|
116
|
+
const filters = ref({
|
|
117
|
+
global: ''
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const fetchQueues = async () => {
|
|
121
|
+
try {
|
|
122
|
+
loading.value = true
|
|
123
|
+
const data = await api.getQueues()
|
|
124
|
+
|
|
125
|
+
if (data?.queues && Array.isArray(data.queues)) {
|
|
126
|
+
// Process the actual API format: {queues: [{name, namespace, task, messages: {total, pending, processing}}]}
|
|
127
|
+
queues.value = data.queues.map(queue => ({
|
|
128
|
+
id: queue.id,
|
|
129
|
+
name: queue.name,
|
|
130
|
+
namespace: queue.namespace,
|
|
131
|
+
task: queue.task,
|
|
132
|
+
createdAt: queue.createdAt,
|
|
133
|
+
partitions: queue.partitions,
|
|
134
|
+
stats: {
|
|
135
|
+
pending: queue.messages?.pending || 0,
|
|
136
|
+
processing: queue.messages?.processing || 0,
|
|
137
|
+
completed: (queue.messages?.total || 0) - (queue.messages?.pending || 0) - (queue.messages?.processing || 0),
|
|
138
|
+
failed: queue.messages?.failed || 0,
|
|
139
|
+
total: queue.messages?.total || 0
|
|
140
|
+
}
|
|
141
|
+
}))
|
|
142
|
+
} else {
|
|
143
|
+
// Fallback to empty array
|
|
144
|
+
queues.value = []
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('Failed to fetch queues:', error)
|
|
148
|
+
toast.add({
|
|
149
|
+
severity: 'error',
|
|
150
|
+
summary: 'Error',
|
|
151
|
+
detail: 'Failed to load queues',
|
|
152
|
+
life: 3000
|
|
153
|
+
})
|
|
154
|
+
queues.value = []
|
|
155
|
+
} finally {
|
|
156
|
+
loading.value = false
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const configureQueue = (queue) => {
|
|
161
|
+
router.push(`/queues/${queue.name}/configure`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const deleteQueue = (queue) => {
|
|
165
|
+
confirm.require({
|
|
166
|
+
message: `Are you sure you want to delete queue "${queue.name}"?`,
|
|
167
|
+
header: 'Delete Queue',
|
|
168
|
+
icon: 'pi pi-exclamation-triangle',
|
|
169
|
+
acceptClass: 'p-button-danger',
|
|
170
|
+
accept: async () => {
|
|
171
|
+
try {
|
|
172
|
+
await api.deleteQueue(queue.name)
|
|
173
|
+
toast.add({
|
|
174
|
+
severity: 'success',
|
|
175
|
+
summary: 'Success',
|
|
176
|
+
detail: `Queue "${queue.name}" deleted`,
|
|
177
|
+
life: 3000
|
|
178
|
+
})
|
|
179
|
+
fetchQueues()
|
|
180
|
+
} catch (error) {
|
|
181
|
+
toast.add({
|
|
182
|
+
severity: 'error',
|
|
183
|
+
summary: 'Error',
|
|
184
|
+
detail: 'Failed to delete queue',
|
|
185
|
+
life: 3000
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onMounted(() => {
|
|
193
|
+
fetchQueues()
|
|
194
|
+
})
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
<style scoped>
|
|
198
|
+
.queues-view {
|
|
199
|
+
padding: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.queues-card {
|
|
203
|
+
background: transparent !important;
|
|
204
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
205
|
+
border-radius: 16px;
|
|
206
|
+
box-shadow: none !important;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
:deep(.p-card-content) {
|
|
210
|
+
padding: 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.table-header {
|
|
214
|
+
display: flex;
|
|
215
|
+
justify-content: space-between;
|
|
216
|
+
align-items: center;
|
|
217
|
+
padding: 1.5rem;
|
|
218
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.table-header h2 {
|
|
222
|
+
font-size: 1.25rem;
|
|
223
|
+
font-weight: 600;
|
|
224
|
+
color: var(--surface-700);
|
|
225
|
+
margin: 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.table-actions {
|
|
229
|
+
display: flex;
|
|
230
|
+
gap: 1rem;
|
|
231
|
+
align-items: center;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.search-input {
|
|
235
|
+
background: rgba(255, 255, 255, 0.05);
|
|
236
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
237
|
+
color: var(--surface-600);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.queue-name-cell {
|
|
241
|
+
display: flex;
|
|
242
|
+
align-items: center;
|
|
243
|
+
gap: 0.75rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.queue-icon {
|
|
247
|
+
width: 32px;
|
|
248
|
+
height: 32px;
|
|
249
|
+
border-radius: 8px;
|
|
250
|
+
background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
|
|
251
|
+
display: flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: center;
|
|
254
|
+
color: white;
|
|
255
|
+
font-weight: 600;
|
|
256
|
+
font-size: 0.875rem;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.queue-link {
|
|
260
|
+
color: var(--primary-500);
|
|
261
|
+
text-decoration: none;
|
|
262
|
+
font-weight: 500;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.queue-link:hover {
|
|
266
|
+
text-decoration: underline;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.namespace-tag,
|
|
270
|
+
.task-tag {
|
|
271
|
+
background: rgba(236, 72, 153, 0.15);
|
|
272
|
+
color: var(--primary-500);
|
|
273
|
+
border: 1px solid rgba(236, 72, 153, 0.3);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.status-badges {
|
|
277
|
+
display: flex;
|
|
278
|
+
gap: 0.5rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.text-muted {
|
|
282
|
+
color: var(--surface-400);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.action-btn {
|
|
286
|
+
color: var(--surface-500) !important;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.action-btn:hover {
|
|
290
|
+
background: rgba(236, 72, 153, 0.1) !important;
|
|
291
|
+
color: var(--primary-500) !important;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.action-btn.p-button-danger:hover {
|
|
295
|
+
background: rgba(239, 68, 68, 0.1) !important;
|
|
296
|
+
color: var(--danger-color) !important;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* DataTable dark theme overrides */
|
|
300
|
+
:deep(.dark-table-v3) {
|
|
301
|
+
background: transparent !important;
|
|
302
|
+
border: none !important;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
:deep(.dark-table-v3 .p-datatable-header) {
|
|
306
|
+
background: transparent !important;
|
|
307
|
+
border: none !important;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
:deep(.dark-table-v3 .p-datatable-thead > tr > th) {
|
|
311
|
+
background: transparent !important;
|
|
312
|
+
color: var(--surface-400) !important;
|
|
313
|
+
border-color: rgba(255, 255, 255, 0.1) !important;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
:deep(.dark-table-v3 .p-datatable-tbody > tr) {
|
|
317
|
+
background: transparent !important;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
:deep(.dark-table-v3 .p-datatable-tbody > tr:hover) {
|
|
321
|
+
background: rgba(236, 72, 153, 0.05) !important;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
:deep(.dark-table-v3 .p-datatable-tbody > tr > td) {
|
|
325
|
+
color: var(--surface-600) !important;
|
|
326
|
+
border-color: rgba(255, 255, 255, 0.05) !important;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
:deep(.p-paginator) {
|
|
330
|
+
background: transparent !important;
|
|
331
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
332
|
+
}
|
|
333
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
|
|
4
|
+
// https://vite.dev/config/
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [vue()],
|
|
7
|
+
server: {
|
|
8
|
+
port: 4000,
|
|
9
|
+
proxy: {
|
|
10
|
+
'/api': {
|
|
11
|
+
target: 'http://localhost:6632',
|
|
12
|
+
changeOrigin: true
|
|
13
|
+
},
|
|
14
|
+
'/health': {
|
|
15
|
+
target: 'http://localhost:6632',
|
|
16
|
+
changeOrigin: true
|
|
17
|
+
},
|
|
18
|
+
'/metrics': {
|
|
19
|
+
target: 'http://localhost:6632',
|
|
20
|
+
changeOrigin: true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
base: '/dashboard/',
|
|
25
|
+
build: {
|
|
26
|
+
outDir: 'dist',
|
|
27
|
+
assetsDir: 'assets',
|
|
28
|
+
sourcemap: false
|
|
29
|
+
}
|
|
30
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import config from './src/config.js';
|
|
3
|
+
|
|
4
|
+
const pool = new pg.Pool({
|
|
5
|
+
host: config.DATABASE.HOST,
|
|
6
|
+
port: config.DATABASE.PORT,
|
|
7
|
+
database: config.DATABASE.NAME,
|
|
8
|
+
user: config.DATABASE.USER,
|
|
9
|
+
password: config.DATABASE.PASSWORD
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
async function debug() {
|
|
13
|
+
try {
|
|
14
|
+
// Check if test queues have namespace/task set
|
|
15
|
+
console.log('=== Checking queues with namespace ===');
|
|
16
|
+
const queues = await pool.query(`
|
|
17
|
+
SELECT name, namespace, task
|
|
18
|
+
FROM queen.queues
|
|
19
|
+
WHERE namespace IS NOT NULL OR task IS NOT NULL
|
|
20
|
+
`);
|
|
21
|
+
console.log('Queues with namespace/task:', queues.rows);
|
|
22
|
+
|
|
23
|
+
// Check messages in those queues
|
|
24
|
+
console.log('\n=== Checking messages in namespaced queues ===');
|
|
25
|
+
const messages = await pool.query(`
|
|
26
|
+
SELECT m.id, m.transaction_id, q.name as queue_name, q.namespace, q.task, p.name as partition_name
|
|
27
|
+
FROM queen.messages m
|
|
28
|
+
JOIN queen.partitions p ON m.partition_id = p.id
|
|
29
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
30
|
+
WHERE q.namespace IS NOT NULL
|
|
31
|
+
LIMIT 10
|
|
32
|
+
`);
|
|
33
|
+
console.log('Messages in namespaced queues:', messages.rows);
|
|
34
|
+
|
|
35
|
+
// Test the actual query used in popMessagesWithFilters
|
|
36
|
+
console.log('\n=== Testing popMessagesWithFilters query ===');
|
|
37
|
+
const actualConsumerGroup = '__QUEUE_MODE__';
|
|
38
|
+
const namespace = 'test-namespace';
|
|
39
|
+
const batch = 3;
|
|
40
|
+
|
|
41
|
+
const testQuery = `
|
|
42
|
+
WITH available_messages AS (
|
|
43
|
+
SELECT m.id, m.transaction_id, m.trace_id, m.payload, m.is_encrypted, m.created_at,
|
|
44
|
+
p.id as partition_id, p.name as partition_name, q.name as queue_name,
|
|
45
|
+
q.priority, q.lease_time, q.retry_limit, q.delayed_processing,
|
|
46
|
+
q.window_buffer, q.max_wait_time_seconds, q.namespace, q.task
|
|
47
|
+
FROM queen.messages m
|
|
48
|
+
JOIN queen.partitions p ON m.partition_id = p.id
|
|
49
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
50
|
+
LEFT JOIN queen.messages_status ms ON m.id = ms.message_id AND ms.consumer_group = $1
|
|
51
|
+
WHERE (ms.id IS NULL OR ms.status = 'pending')
|
|
52
|
+
AND m.created_at <= NOW() - INTERVAL '1 second' * q.delayed_processing
|
|
53
|
+
AND (q.max_wait_time_seconds = 0 OR
|
|
54
|
+
m.created_at > NOW() - INTERVAL '1 second' * q.max_wait_time_seconds)
|
|
55
|
+
AND (q.window_buffer = 0 OR NOT EXISTS (
|
|
56
|
+
SELECT 1 FROM queen.messages m2
|
|
57
|
+
WHERE m2.partition_id = p.id
|
|
58
|
+
AND m2.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
|
|
59
|
+
))
|
|
60
|
+
AND NOT EXISTS (
|
|
61
|
+
-- Check for active partition leases
|
|
62
|
+
SELECT 1 FROM queen.partition_leases pl
|
|
63
|
+
WHERE pl.partition_id = p.id
|
|
64
|
+
AND pl.consumer_group = $1
|
|
65
|
+
AND pl.released_at IS NULL
|
|
66
|
+
AND pl.lease_expires_at > NOW()
|
|
67
|
+
)
|
|
68
|
+
AND q.namespace = $2
|
|
69
|
+
ORDER BY q.priority DESC, m.created_at ASC LIMIT $3 FOR UPDATE OF m SKIP LOCKED
|
|
70
|
+
)
|
|
71
|
+
SELECT * FROM available_messages
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const result = await pool.query(testQuery, [actualConsumerGroup, namespace, batch]);
|
|
75
|
+
console.log('Query result count:', result.rows.length);
|
|
76
|
+
if (result.rows.length > 0) {
|
|
77
|
+
console.log('Sample message:', result.rows[0]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for any blocking issues
|
|
81
|
+
console.log('\n=== Checking for blocking issues ===');
|
|
82
|
+
const statusCheck = await pool.query(`
|
|
83
|
+
SELECT ms.*, m.transaction_id
|
|
84
|
+
FROM queen.messages_status ms
|
|
85
|
+
JOIN queen.messages m ON ms.message_id = m.id
|
|
86
|
+
WHERE ms.consumer_group = $1
|
|
87
|
+
AND ms.status != 'completed'
|
|
88
|
+
LIMIT 10
|
|
89
|
+
`, [actualConsumerGroup]);
|
|
90
|
+
console.log('Pending/processing messages:', statusCheck.rows);
|
|
91
|
+
|
|
92
|
+
const leaseCheck = await pool.query(`
|
|
93
|
+
SELECT pl.*, p.name as partition_name, q.name as queue_name
|
|
94
|
+
FROM queen.partition_leases pl
|
|
95
|
+
JOIN queen.partitions p ON pl.partition_id = p.id
|
|
96
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
97
|
+
WHERE pl.consumer_group = $1
|
|
98
|
+
AND pl.released_at IS NULL
|
|
99
|
+
AND pl.lease_expires_at > NOW()
|
|
100
|
+
`, [actualConsumerGroup]);
|
|
101
|
+
console.log('Active partition leases:', leaseCheck.rows);
|
|
102
|
+
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error:', error);
|
|
105
|
+
} finally {
|
|
106
|
+
await pool.end();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
debug();
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Long Polling in Queen Client
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Queen client implements efficient long polling for real-time message consumption. When configured with `wait: true`, the client will maintain a persistent connection to the server, waiting for messages to arrive.
|
|
6
|
+
|
|
7
|
+
## Continuous Polling Behavior
|
|
8
|
+
|
|
9
|
+
The client has been optimized for continuous long polling without unnecessary delays:
|
|
10
|
+
|
|
11
|
+
### Timeout Handling
|
|
12
|
+
|
|
13
|
+
When a long polling request times out (no messages received within the timeout period), the client **immediately** starts a new request without any delay. This ensures:
|
|
14
|
+
|
|
15
|
+
- **Zero message loss**: No gap between polling requests where messages could be missed
|
|
16
|
+
- **Minimal latency**: Messages are received as soon as they're available
|
|
17
|
+
- **Efficient resource usage**: No unnecessary waiting or reconnection overhead
|
|
18
|
+
|
|
19
|
+
### Implementation Details
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// The consume function handles timeouts gracefully
|
|
23
|
+
const stop = client.consume({
|
|
24
|
+
ns: 'myapp',
|
|
25
|
+
task: 'processing',
|
|
26
|
+
queue: 'orders',
|
|
27
|
+
handler: async (message) => {
|
|
28
|
+
// Process message
|
|
29
|
+
},
|
|
30
|
+
options: {
|
|
31
|
+
wait: true, // Enable long polling
|
|
32
|
+
timeout: 30000, // 30 second timeout
|
|
33
|
+
batch: 10, // Get up to 10 messages at once
|
|
34
|
+
stopOnError: false // Continue on errors
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### How It Works
|
|
40
|
+
|
|
41
|
+
1. **Normal Operation**: When messages are available, they are immediately returned and processed
|
|
42
|
+
2. **Timeout Scenario**: If no messages arrive within the timeout period:
|
|
43
|
+
- Server returns an empty response (not an error)
|
|
44
|
+
- Client immediately starts a new long polling request
|
|
45
|
+
- No delay or backoff is applied
|
|
46
|
+
3. **Error Handling**: Only actual errors (network issues, server errors) trigger retry delays
|
|
47
|
+
|
|
48
|
+
### Client-Server Timeout Coordination
|
|
49
|
+
|
|
50
|
+
The client automatically adds a buffer to its timeout when long polling:
|
|
51
|
+
- Server timeout: 30 seconds (configured value)
|
|
52
|
+
- Client timeout: 35 seconds (server timeout + 5 second buffer)
|
|
53
|
+
|
|
54
|
+
This prevents premature client-side timeouts and ensures the server has time to respond.
|
|
55
|
+
|
|
56
|
+
## Example Usage
|
|
57
|
+
|
|
58
|
+
### Basic Consumer
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
import { createQueenClient } from '@queen/client';
|
|
62
|
+
|
|
63
|
+
const client = createQueenClient({
|
|
64
|
+
baseUrl: 'http://localhost:6632'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Start continuous consumer
|
|
68
|
+
const stop = client.consume({
|
|
69
|
+
ns: 'production',
|
|
70
|
+
task: 'orders',
|
|
71
|
+
queue: 'processing',
|
|
72
|
+
handler: async (message) => {
|
|
73
|
+
console.log('Received:', message);
|
|
74
|
+
// Process the message
|
|
75
|
+
},
|
|
76
|
+
options: {
|
|
77
|
+
wait: true, // Enable long polling
|
|
78
|
+
timeout: 30000, // 30 second timeout
|
|
79
|
+
batch: 5 // Process up to 5 messages at once
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Stop when needed
|
|
84
|
+
// stop();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Advanced Consumer with Statistics
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
const stats = { processed: 0, errors: 0 };
|
|
91
|
+
|
|
92
|
+
const stop = client.consume({
|
|
93
|
+
ns: 'production',
|
|
94
|
+
task: 'orders',
|
|
95
|
+
queue: 'processing',
|
|
96
|
+
handler: async (message) => {
|
|
97
|
+
try {
|
|
98
|
+
await processOrder(message.data);
|
|
99
|
+
stats.processed++;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
stats.errors++;
|
|
102
|
+
throw error; // Re-throw to trigger failed acknowledgment
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
options: {
|
|
106
|
+
wait: true,
|
|
107
|
+
timeout: 30000,
|
|
108
|
+
batch: 10,
|
|
109
|
+
stopOnError: false // Continue even if individual messages fail
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Monitor statistics
|
|
114
|
+
setInterval(() => {
|
|
115
|
+
console.log(`Processed: ${stats.processed}, Errors: ${stats.errors}`);
|
|
116
|
+
}, 60000);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Benefits
|
|
120
|
+
|
|
121
|
+
1. **Real-time Processing**: Messages are processed immediately upon arrival
|
|
122
|
+
2. **Efficient**: No wasted time between polling cycles
|
|
123
|
+
3. **Reliable**: Automatic reconnection on network issues
|
|
124
|
+
4. **Scalable**: Supports multiple concurrent consumers
|
|
125
|
+
5. **Simple**: No complex configuration required
|
|
126
|
+
|
|
127
|
+
## Configuration Options
|
|
128
|
+
|
|
129
|
+
| Option | Default | Description |
|
|
130
|
+
|--------|---------|-------------|
|
|
131
|
+
| `wait` | `false` | Enable long polling |
|
|
132
|
+
| `timeout` | `30000` | Polling timeout in milliseconds |
|
|
133
|
+
| `batch` | `1` | Number of messages to retrieve per request |
|
|
134
|
+
| `stopOnError` | `false` | Stop consumer on handler errors |
|
|
135
|
+
|
|
136
|
+
## Best Practices
|
|
137
|
+
|
|
138
|
+
1. **Set Appropriate Timeouts**: Use 30-60 second timeouts for long polling
|
|
139
|
+
2. **Handle Errors Gracefully**: Implement proper error handling in your message handler
|
|
140
|
+
3. **Use Batching**: Process multiple messages per request for better throughput
|
|
141
|
+
4. **Monitor Performance**: Track message processing rates and errors
|
|
142
|
+
5. **Graceful Shutdown**: Always call the stop function when shutting down
|
|
143
|
+
|
|
144
|
+
## Comparison with Traditional Polling
|
|
145
|
+
|
|
146
|
+
### Traditional Polling (with delays)
|
|
147
|
+
```
|
|
148
|
+
Request -> Response -> Wait 5s -> Request -> Response -> Wait 5s -> ...
|
|
149
|
+
```
|
|
150
|
+
- Messages may wait up to 5 seconds before being processed
|
|
151
|
+
- Unnecessary requests when no messages are available
|
|
152
|
+
|
|
153
|
+
### Long Polling (Queen implementation)
|
|
154
|
+
```
|
|
155
|
+
Request -> Wait for message/timeout -> Response -> Immediately Request -> ...
|
|
156
|
+
```
|
|
157
|
+
- Messages processed immediately upon arrival
|
|
158
|
+
- No delays between requests
|
|
159
|
+
- Efficient use of resources
|