Skip to content

Commit 261d469

Browse files
authored
feat: limit how many tasks are executing at any time (#1103)
Allows user defined concurrency limit when evaluating check alert tasks. Includes some further refinements of the task execution types to make actual task execution more type safe.
1 parent bb2221a commit 261d469

File tree

8 files changed

+400
-99
lines changed

8 files changed

+400
-99
lines changed

.changeset/six-squids-stare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/api": patch
3+
---
4+
5+
feat: limit how many tasks are executing at any time

packages/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99
"dependencies": {
1010
"@clickhouse/client": "^0.2.10",
11+
"@esm2cjs/p-queue": "^7.3.0",
1112
"@hyperdx/common-utils": "^0.3.2",
1213
"@hyperdx/lucene": "^3.1.1",
1314
"@hyperdx/node-opentelemetry": "^0.8.2",

packages/api/src/tasks/__tests__/types.test.ts

Lines changed: 257 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ describe('asTaskArgs', () => {
1414
provider: 'default',
1515
});
1616
expect(result.taskName).toBe('command');
17-
expect(result.provider).toBe('default');
17+
// For non-check-alerts tasks, we need to use type assertion to access provider
18+
expect((result as any).provider).toBe('default');
1819
});
1920

2021
it('should return valid TaskArgs when provider is undefined', () => {
@@ -29,18 +30,8 @@ describe('asTaskArgs', () => {
2930
provider: undefined,
3031
});
3132
expect(result.taskName).toBe('command');
32-
expect(result.provider).toBeUndefined();
33-
});
34-
35-
it('should throw error when provider is empty string', () => {
36-
const invalidArgs = {
37-
_: ['command'],
38-
provider: '',
39-
};
40-
41-
expect(() => asTaskArgs(invalidArgs)).toThrow(
42-
'Provider must contain valid characters',
43-
);
33+
// For non-check-alerts tasks, we need to use type assertion to access provider
34+
expect((result as any).provider).toBeUndefined();
4435
});
4536

4637
it('should throw error for null input', () => {
@@ -95,21 +86,6 @@ describe('asTaskArgs', () => {
9586
);
9687
});
9788

98-
it('should throw error when provider is not a string', () => {
99-
const invalidProviders = [123, null, { name: 'default' }, ['default']];
100-
101-
invalidProviders.forEach(provider => {
102-
const invalidArgs = {
103-
_: ['command'],
104-
provider,
105-
};
106-
107-
expect(() => asTaskArgs(invalidArgs)).toThrow(
108-
'Provider must be a string if provided',
109-
);
110-
});
111-
});
112-
11389
it('should handle empty array for _ property', () => {
11490
const validArgs = {
11591
_: [],
@@ -169,6 +145,11 @@ describe('asTaskArgs', () => {
169145
taskName: 'check-alerts',
170146
provider: 'default',
171147
});
148+
expect(result.taskName).toBe('check-alerts');
149+
// For check-alerts tasks, provider property is directly accessible
150+
if (result.taskName === 'check-alerts') {
151+
expect(result.provider).toBe('default');
152+
}
172153
});
173154

174155
it('should accept check-alerts task without provider', () => {
@@ -182,16 +163,257 @@ describe('asTaskArgs', () => {
182163
taskName: 'check-alerts',
183164
provider: undefined,
184165
});
166+
expect(result.taskName).toBe('check-alerts');
167+
// For check-alerts tasks, provider property is directly accessible
168+
if (result.taskName === 'check-alerts') {
169+
expect(result.provider).toBeUndefined();
170+
}
185171
});
186172

187-
it('should throw error when provider is whitespace-only', () => {
188-
const invalidArgs = {
189-
_: ['command'],
190-
provider: ' ',
173+
it('should accept ping-pong task without provider', () => {
174+
const validArgs = {
175+
_: ['ping-pong'],
191176
};
192177

193-
expect(() => asTaskArgs(invalidArgs)).toThrow(
194-
'Provider must contain valid characters',
195-
);
178+
const result = asTaskArgs(validArgs);
179+
180+
expect(result).toEqual({
181+
taskName: 'ping-pong',
182+
});
183+
expect(result.taskName).toBe('ping-pong');
184+
// Ping-pong tasks should not have a provider property
185+
expect('provider' in result).toBe(false);
186+
});
187+
188+
describe('concurrency parameter validation', () => {
189+
it('should accept check-alerts task with valid concurrency', () => {
190+
const validArgs = {
191+
_: ['check-alerts'],
192+
concurrency: 4,
193+
};
194+
195+
const result = asTaskArgs(validArgs);
196+
197+
expect(result).toEqual({
198+
taskName: 'check-alerts',
199+
provider: undefined,
200+
concurrency: 4,
201+
});
202+
expect(result.taskName).toBe('check-alerts');
203+
if (result.taskName === 'check-alerts') {
204+
expect(result.concurrency).toBe(4);
205+
}
206+
});
207+
208+
it('should accept check-alerts task with concurrency value of 1', () => {
209+
const validArgs = {
210+
_: ['check-alerts'],
211+
concurrency: 1,
212+
};
213+
214+
const result = asTaskArgs(validArgs);
215+
216+
expect(result).toEqual({
217+
taskName: 'check-alerts',
218+
provider: undefined,
219+
concurrency: 1,
220+
});
221+
expect(result.taskName).toBe('check-alerts');
222+
if (result.taskName === 'check-alerts') {
223+
expect(result.concurrency).toBe(1);
224+
}
225+
});
226+
227+
it('should accept check-alerts task with large concurrency values', () => {
228+
const validArgs = {
229+
_: ['check-alerts'],
230+
concurrency: 100,
231+
};
232+
233+
const result = asTaskArgs(validArgs);
234+
235+
expect(result).toEqual({
236+
taskName: 'check-alerts',
237+
provider: undefined,
238+
concurrency: 100,
239+
});
240+
expect(result.taskName).toBe('check-alerts');
241+
if (result.taskName === 'check-alerts') {
242+
expect(result.concurrency).toBe(100);
243+
}
244+
});
245+
246+
it('should accept check-alerts task without concurrency parameter', () => {
247+
const validArgs = {
248+
_: ['check-alerts'],
249+
provider: 'default',
250+
};
251+
252+
const result = asTaskArgs(validArgs);
253+
254+
expect(result).toEqual({
255+
taskName: 'check-alerts',
256+
provider: 'default',
257+
concurrency: undefined,
258+
});
259+
expect(result.taskName).toBe('check-alerts');
260+
if (result.taskName === 'check-alerts') {
261+
expect(result.concurrency).toBeUndefined();
262+
}
263+
});
264+
265+
it('should throw error when concurrency is not a number', () => {
266+
const invalidArgs = {
267+
_: ['check-alerts'],
268+
concurrency: 'invalid',
269+
};
270+
271+
expect(() => asTaskArgs(invalidArgs)).toThrow(
272+
'Concurrency must be a number if provided',
273+
);
274+
});
275+
276+
it('should throw error when concurrency is boolean', () => {
277+
const invalidArgs = {
278+
_: ['check-alerts'],
279+
concurrency: true,
280+
};
281+
282+
expect(() => asTaskArgs(invalidArgs)).toThrow(
283+
'Concurrency must be a number if provided',
284+
);
285+
});
286+
287+
it('should throw error when concurrency is null', () => {
288+
const invalidArgs = {
289+
_: ['check-alerts'],
290+
concurrency: null,
291+
};
292+
293+
expect(() => asTaskArgs(invalidArgs)).toThrow(
294+
'Concurrency must be a number if provided',
295+
);
296+
});
297+
298+
it('should throw error when concurrency is an object', () => {
299+
const invalidArgs = {
300+
_: ['check-alerts'],
301+
concurrency: { value: 4 },
302+
};
303+
304+
expect(() => asTaskArgs(invalidArgs)).toThrow(
305+
'Concurrency must be a number if provided',
306+
);
307+
});
308+
309+
it('should throw error when concurrency is an array', () => {
310+
const invalidArgs = {
311+
_: ['check-alerts'],
312+
concurrency: [4],
313+
};
314+
315+
expect(() => asTaskArgs(invalidArgs)).toThrow(
316+
'Concurrency must be a number if provided',
317+
);
318+
});
319+
320+
it('should throw error when concurrency is zero', () => {
321+
const invalidArgs = {
322+
_: ['check-alerts'],
323+
concurrency: 0,
324+
};
325+
326+
expect(() => asTaskArgs(invalidArgs)).toThrow(
327+
'Concurrency cannot be less than 1',
328+
);
329+
});
330+
331+
it('should throw error when concurrency is negative', () => {
332+
const invalidArgs = {
333+
_: ['check-alerts'],
334+
concurrency: -1,
335+
};
336+
337+
expect(() => asTaskArgs(invalidArgs)).toThrow(
338+
'Concurrency cannot be less than 1',
339+
);
340+
});
341+
342+
it('should throw error when concurrency is a negative decimal', () => {
343+
const invalidArgs = {
344+
_: ['check-alerts'],
345+
concurrency: -0.5,
346+
};
347+
348+
expect(() => asTaskArgs(invalidArgs)).toThrow(
349+
'Concurrency must be an integer if provided',
350+
);
351+
});
352+
353+
it('should throw error when concurrency is a positive decimal', () => {
354+
const invalidArgs = {
355+
_: ['check-alerts'],
356+
concurrency: 2.5,
357+
};
358+
359+
expect(() => asTaskArgs(invalidArgs)).toThrow(
360+
'Concurrency must be an integer if provided',
361+
);
362+
});
363+
364+
it('should throw error when concurrency is a small decimal', () => {
365+
const invalidArgs = {
366+
_: ['check-alerts'],
367+
concurrency: 1.1,
368+
};
369+
370+
expect(() => asTaskArgs(invalidArgs)).toThrow(
371+
'Concurrency must be an integer if provided',
372+
);
373+
});
374+
375+
it('should throw error when concurrency is a large decimal', () => {
376+
const invalidArgs = {
377+
_: ['check-alerts'],
378+
concurrency: 100.999,
379+
};
380+
381+
expect(() => asTaskArgs(invalidArgs)).toThrow(
382+
'Concurrency must be an integer if provided',
383+
);
384+
});
385+
386+
it('should ignore concurrency parameter for non-check-alerts tasks', () => {
387+
const validArgs = {
388+
_: ['ping-pong'],
389+
concurrency: 4,
390+
};
391+
392+
const result = asTaskArgs(validArgs);
393+
394+
expect(result).toEqual({
395+
taskName: 'ping-pong',
396+
});
397+
expect(result.taskName).toBe('ping-pong');
398+
// Ping-pong tasks should not have a concurrency property
399+
expect('concurrency' in result).toBe(false);
400+
});
401+
402+
it('should ignore concurrency parameter for unknown task types', () => {
403+
const validArgs = {
404+
_: ['unknown-task'],
405+
concurrency: 4,
406+
};
407+
408+
const result = asTaskArgs(validArgs);
409+
410+
expect(result).toEqual({
411+
taskName: 'unknown-task',
412+
provider: undefined,
413+
});
414+
expect(result.taskName).toBe('unknown-task');
415+
// Unknown task types should not process concurrency parameter
416+
expect('concurrency' in result).toBe(false);
417+
});
196418
});
197419
});

0 commit comments

Comments
 (0)