@checkstack/queue-backend 0.1.1 → 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 CHANGED
@@ -1,5 +1,36 @@
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
+
3
34
  ## 0.1.1
4
35
 
5
36
  ### 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.0",
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
  };