@checkstack/queue-backend 0.1.0 → 0.2.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/CHANGELOG.md +39 -0
- package/package.json +1 -1
- package/src/router.test.ts +138 -0
- package/src/router.ts +80 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @checkstack/queue-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 180be38: # Queue Lag Warning
|
|
8
|
+
|
|
9
|
+
Added a queue lag warning system that displays alerts when pending jobs exceed configurable thresholds.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Backend Stats API**: New `getStats`, `getLagStatus`, and `updateLagThresholds` RPC endpoints
|
|
14
|
+
- **Signal-based Updates**: `QUEUE_LAG_CHANGED` signal for real-time frontend updates
|
|
15
|
+
- **Aggregated Stats**: `QueueManager.getAggregatedStats()` sums stats across all queues
|
|
16
|
+
- **Configurable Thresholds**: Warning (default 100) and Critical (default 500) thresholds stored in config
|
|
17
|
+
- **Dashboard Integration**: Queue lag alert displayed on main Dashboard (access-gated)
|
|
18
|
+
- **Queue Settings Page**: Lag alert and Performance Tuning guidance card with concurrency tips
|
|
19
|
+
|
|
20
|
+
## UI Changes
|
|
21
|
+
|
|
22
|
+
- Queue lag alert banner appears on Dashboard and Queue Settings when pending jobs exceed thresholds
|
|
23
|
+
- New "Performance Tuning" card with concurrency settings guidance and bottleneck indicators
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [180be38]
|
|
28
|
+
- Updated dependencies [7a23261]
|
|
29
|
+
- @checkstack/queue-common@0.2.0
|
|
30
|
+
- @checkstack/queue-api@0.1.0
|
|
31
|
+
- @checkstack/common@0.3.0
|
|
32
|
+
- @checkstack/backend-api@0.3.2
|
|
33
|
+
|
|
34
|
+
## 0.1.1
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [9a27800]
|
|
39
|
+
- @checkstack/queue-api@0.0.6
|
|
40
|
+
- @checkstack/backend-api@0.3.1
|
|
41
|
+
|
|
3
42
|
## 0.1.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/package.json
CHANGED
package/src/router.test.ts
CHANGED
|
@@ -31,10 +31,21 @@ describe("Queue Router", () => {
|
|
|
31
31
|
setActiveBackend: mock(() =>
|
|
32
32
|
Promise.resolve({ success: true, migratedRecurringJobs: 0, warnings: [] })
|
|
33
33
|
),
|
|
34
|
+
getAggregatedStats: mock(() =>
|
|
35
|
+
Promise.resolve({
|
|
36
|
+
pending: 50,
|
|
37
|
+
processing: 5,
|
|
38
|
+
completed: 100,
|
|
39
|
+
failed: 2,
|
|
40
|
+
consumerGroups: 3,
|
|
41
|
+
})
|
|
42
|
+
),
|
|
34
43
|
};
|
|
35
44
|
|
|
36
45
|
const mockConfigService: any = {
|
|
37
46
|
getRedacted: mock(() => Promise.resolve({ concurrency: 10 })),
|
|
47
|
+
get: mock(() => Promise.resolve(undefined)),
|
|
48
|
+
set: mock(() => Promise.resolve()),
|
|
38
49
|
};
|
|
39
50
|
|
|
40
51
|
const router = createQueueRouter(mockConfigService);
|
|
@@ -81,4 +92,131 @@ describe("Queue Router", () => {
|
|
|
81
92
|
expect(result.pluginId).toBe("memory");
|
|
82
93
|
expect(mockManager.setActiveBackend).toHaveBeenCalled();
|
|
83
94
|
});
|
|
95
|
+
|
|
96
|
+
describe("Queue Stats", () => {
|
|
97
|
+
it("getStats returns aggregated stats from QueueManager", async () => {
|
|
98
|
+
const context = createMockRpcContext({
|
|
99
|
+
user: mockUser,
|
|
100
|
+
queueManager: mockManager,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await call(router.getStats, undefined, { context });
|
|
104
|
+
expect(result.pending).toBe(50);
|
|
105
|
+
expect(result.processing).toBe(5);
|
|
106
|
+
expect(result.completed).toBe(100);
|
|
107
|
+
expect(result.failed).toBe(2);
|
|
108
|
+
expect(mockManager.getAggregatedStats).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("Queue Lag Status", () => {
|
|
113
|
+
it("getLagStatus returns 'none' severity when pending is below warning threshold", async () => {
|
|
114
|
+
const lowPendingManager = {
|
|
115
|
+
...mockManager,
|
|
116
|
+
getAggregatedStats: mock(() => Promise.resolve({ pending: 50 })),
|
|
117
|
+
};
|
|
118
|
+
const context = createMockRpcContext({
|
|
119
|
+
user: mockUser,
|
|
120
|
+
queueManager: lowPendingManager,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await call(router.getLagStatus, undefined, { context });
|
|
124
|
+
expect(result.severity).toBe("none");
|
|
125
|
+
expect(result.pending).toBe(50);
|
|
126
|
+
expect(result.thresholds.warningThreshold).toBe(100);
|
|
127
|
+
expect(result.thresholds.criticalThreshold).toBe(500);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("getLagStatus returns 'warning' severity when pending exceeds warning threshold", async () => {
|
|
131
|
+
const warningPendingManager = {
|
|
132
|
+
...mockManager,
|
|
133
|
+
getAggregatedStats: mock(() => Promise.resolve({ pending: 150 })),
|
|
134
|
+
};
|
|
135
|
+
const context = createMockRpcContext({
|
|
136
|
+
user: mockUser,
|
|
137
|
+
queueManager: warningPendingManager,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = await call(router.getLagStatus, undefined, { context });
|
|
141
|
+
expect(result.severity).toBe("warning");
|
|
142
|
+
expect(result.pending).toBe(150);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("getLagStatus returns 'critical' severity when pending exceeds critical threshold", async () => {
|
|
146
|
+
const criticalPendingManager = {
|
|
147
|
+
...mockManager,
|
|
148
|
+
getAggregatedStats: mock(() => Promise.resolve({ pending: 600 })),
|
|
149
|
+
};
|
|
150
|
+
const context = createMockRpcContext({
|
|
151
|
+
user: mockUser,
|
|
152
|
+
queueManager: criticalPendingManager,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = await call(router.getLagStatus, undefined, { context });
|
|
156
|
+
expect(result.severity).toBe("critical");
|
|
157
|
+
expect(result.pending).toBe(600);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("getLagStatus uses custom thresholds from config", async () => {
|
|
161
|
+
const customConfigService = {
|
|
162
|
+
...mockConfigService,
|
|
163
|
+
get: mock(() =>
|
|
164
|
+
Promise.resolve({
|
|
165
|
+
warningThreshold: 20,
|
|
166
|
+
criticalThreshold: 50,
|
|
167
|
+
})
|
|
168
|
+
),
|
|
169
|
+
};
|
|
170
|
+
const customRouter = createQueueRouter(customConfigService);
|
|
171
|
+
const pendingManager = {
|
|
172
|
+
...mockManager,
|
|
173
|
+
getAggregatedStats: mock(() => Promise.resolve({ pending: 30 })),
|
|
174
|
+
};
|
|
175
|
+
const context = createMockRpcContext({
|
|
176
|
+
user: mockUser,
|
|
177
|
+
queueManager: pendingManager,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await call(customRouter.getLagStatus, undefined, {
|
|
181
|
+
context,
|
|
182
|
+
});
|
|
183
|
+
expect(result.severity).toBe("warning");
|
|
184
|
+
expect(result.thresholds.warningThreshold).toBe(20);
|
|
185
|
+
expect(result.thresholds.criticalThreshold).toBe(50);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("Update Lag Thresholds", () => {
|
|
190
|
+
it("updateLagThresholds stores new thresholds", async () => {
|
|
191
|
+
const context = createMockRpcContext({
|
|
192
|
+
user: mockUser,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await call(
|
|
196
|
+
router.updateLagThresholds,
|
|
197
|
+
{ warningThreshold: 200, criticalThreshold: 1000 },
|
|
198
|
+
{ context }
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(result.warningThreshold).toBe(200);
|
|
202
|
+
expect(result.criticalThreshold).toBe(1000);
|
|
203
|
+
expect(mockConfigService.set).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("updateLagThresholds rejects if warning >= critical", async () => {
|
|
207
|
+
const context = createMockRpcContext({
|
|
208
|
+
user: mockUser,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await expect(
|
|
212
|
+
call(
|
|
213
|
+
router.updateLagThresholds,
|
|
214
|
+
{ warningThreshold: 500, criticalThreshold: 100 },
|
|
215
|
+
{ context }
|
|
216
|
+
)
|
|
217
|
+
).rejects.toThrow(
|
|
218
|
+
"Warning threshold must be less than critical threshold"
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
84
222
|
});
|
package/src/router.ts
CHANGED
|
@@ -4,13 +4,43 @@ import {
|
|
|
4
4
|
RpcContext,
|
|
5
5
|
autoAuthMiddleware,
|
|
6
6
|
} from "@checkstack/backend-api";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
queueContract,
|
|
9
|
+
QueueLagThresholdsSchema,
|
|
10
|
+
type QueueLagThresholds,
|
|
11
|
+
type LagSeverity,
|
|
12
|
+
} from "@checkstack/queue-common";
|
|
8
13
|
import { implement, ORPCError } from "@orpc/server";
|
|
9
14
|
|
|
10
15
|
const os = implement(queueContract)
|
|
11
16
|
.$context<RpcContext>()
|
|
12
17
|
.use(autoAuthMiddleware);
|
|
13
18
|
|
|
19
|
+
// Config key for lag thresholds
|
|
20
|
+
const LAG_THRESHOLDS_KEY = "queue:lag-thresholds";
|
|
21
|
+
|
|
22
|
+
// Default thresholds
|
|
23
|
+
const DEFAULT_THRESHOLDS: QueueLagThresholds = {
|
|
24
|
+
warningThreshold: 100,
|
|
25
|
+
criticalThreshold: 500,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calculate lag severity based on pending count and thresholds
|
|
30
|
+
*/
|
|
31
|
+
function calculateSeverity(
|
|
32
|
+
pending: number,
|
|
33
|
+
thresholds: QueueLagThresholds
|
|
34
|
+
): LagSeverity {
|
|
35
|
+
if (pending >= thresholds.criticalThreshold) {
|
|
36
|
+
return "critical";
|
|
37
|
+
}
|
|
38
|
+
if (pending >= thresholds.warningThreshold) {
|
|
39
|
+
return "warning";
|
|
40
|
+
}
|
|
41
|
+
return "none";
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
export const createQueueRouter = (configService: ConfigService) => {
|
|
15
45
|
return os.router({
|
|
16
46
|
getPlugins: os.getPlugins.handler(async ({ context }) => {
|
|
@@ -67,5 +97,54 @@ export const createQueueRouter = (configService: ConfigService) => {
|
|
|
67
97
|
};
|
|
68
98
|
}
|
|
69
99
|
),
|
|
100
|
+
|
|
101
|
+
getStats: os.getStats.handler(async ({ context }) => {
|
|
102
|
+
const stats = await context.queueManager.getAggregatedStats();
|
|
103
|
+
return stats;
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
getLagStatus: os.getLagStatus.handler(async ({ context }) => {
|
|
107
|
+
const stats = await context.queueManager.getAggregatedStats();
|
|
108
|
+
|
|
109
|
+
// Load thresholds from config
|
|
110
|
+
const thresholds =
|
|
111
|
+
(await configService.get<QueueLagThresholds>(
|
|
112
|
+
LAG_THRESHOLDS_KEY,
|
|
113
|
+
QueueLagThresholdsSchema,
|
|
114
|
+
1
|
|
115
|
+
)) ?? DEFAULT_THRESHOLDS;
|
|
116
|
+
|
|
117
|
+
const severity = calculateSeverity(stats.pending, thresholds);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
pending: stats.pending,
|
|
121
|
+
severity,
|
|
122
|
+
thresholds,
|
|
123
|
+
};
|
|
124
|
+
}),
|
|
125
|
+
|
|
126
|
+
updateLagThresholds: os.updateLagThresholds.handler(
|
|
127
|
+
async ({ input, context }) => {
|
|
128
|
+
// Validate that warning < critical
|
|
129
|
+
if (input.warningThreshold >= input.criticalThreshold) {
|
|
130
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
131
|
+
message: "Warning threshold must be less than critical threshold",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await configService.set(
|
|
136
|
+
LAG_THRESHOLDS_KEY,
|
|
137
|
+
QueueLagThresholdsSchema,
|
|
138
|
+
1,
|
|
139
|
+
input
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
context.logger.info(
|
|
143
|
+
`Queue lag thresholds updated: warning=${input.warningThreshold}, critical=${input.criticalThreshold}`
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return input;
|
|
147
|
+
}
|
|
148
|
+
),
|
|
70
149
|
});
|
|
71
150
|
};
|