@checkstack/queue-backend 0.1.1 → 0.2.1

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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # @checkstack/queue-backend
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [d94121b]
8
+ - @checkstack/backend-api@0.3.3
9
+ - @checkstack/queue-api@0.1.1
10
+
11
+ ## 0.2.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 180be38: # Queue Lag Warning
16
+
17
+ Added a queue lag warning system that displays alerts when pending jobs exceed configurable thresholds.
18
+
19
+ ## Features
20
+
21
+ - **Backend Stats API**: New `getStats`, `getLagStatus`, and `updateLagThresholds` RPC endpoints
22
+ - **Signal-based Updates**: `QUEUE_LAG_CHANGED` signal for real-time frontend updates
23
+ - **Aggregated Stats**: `QueueManager.getAggregatedStats()` sums stats across all queues
24
+ - **Configurable Thresholds**: Warning (default 100) and Critical (default 500) thresholds stored in config
25
+ - **Dashboard Integration**: Queue lag alert displayed on main Dashboard (access-gated)
26
+ - **Queue Settings Page**: Lag alert and Performance Tuning guidance card with concurrency tips
27
+
28
+ ## UI Changes
29
+
30
+ - Queue lag alert banner appears on Dashboard and Queue Settings when pending jobs exceed thresholds
31
+ - New "Performance Tuning" card with concurrency settings guidance and bottleneck indicators
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [180be38]
36
+ - Updated dependencies [7a23261]
37
+ - @checkstack/queue-common@0.2.0
38
+ - @checkstack/queue-api@0.1.0
39
+ - @checkstack/common@0.3.0
40
+ - @checkstack/backend-api@0.3.2
41
+
3
42
  ## 0.1.1
4
43
 
5
44
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/queue-backend",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -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 { queueContract } from "@checkstack/queue-common";
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
  };