pp-command-bus 1.0.2 → 1.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.
Files changed (59) hide show
  1. package/dist/command-bus/command-bus.spec.js +26 -8
  2. package/dist/command-bus/command-bus.spec.js.map +1 -1
  3. package/dist/command-bus/config/auto-config-optimizer.d.ts +35 -0
  4. package/dist/command-bus/config/auto-config-optimizer.js +52 -0
  5. package/dist/command-bus/config/auto-config-optimizer.js.map +1 -0
  6. package/dist/command-bus/config/auto-config-optimizer.spec.d.ts +1 -0
  7. package/dist/command-bus/config/auto-config-optimizer.spec.js +42 -0
  8. package/dist/command-bus/config/auto-config-optimizer.spec.js.map +1 -0
  9. package/dist/command-bus/config/command-bus-config.d.ts +6 -0
  10. package/dist/command-bus/config/command-bus-config.js +33 -6
  11. package/dist/command-bus/config/command-bus-config.js.map +1 -1
  12. package/dist/command-bus/config/command-bus-config.spec.js +62 -2
  13. package/dist/command-bus/config/command-bus-config.spec.js.map +1 -1
  14. package/dist/command-bus/index.d.ts +4 -0
  15. package/dist/command-bus/index.js +21 -6
  16. package/dist/command-bus/index.js.map +1 -1
  17. package/dist/command-bus/job/job-processor.d.ts +2 -0
  18. package/dist/command-bus/job/job-processor.js +50 -2
  19. package/dist/command-bus/job/job-processor.js.map +1 -1
  20. package/dist/command-bus/job/job-processor.spec.js +13 -0
  21. package/dist/command-bus/job/job-processor.spec.js.map +1 -1
  22. package/dist/command-bus/queue/queue-manager.d.ts +35 -3
  23. package/dist/command-bus/queue/queue-manager.js +107 -4
  24. package/dist/command-bus/queue/queue-manager.js.map +1 -1
  25. package/dist/command-bus/queue/queue-manager.spec.js +155 -3
  26. package/dist/command-bus/queue/queue-manager.spec.js.map +1 -1
  27. package/dist/command-bus/rpc/rpc-coordinator.js +6 -1
  28. package/dist/command-bus/rpc/rpc-coordinator.js.map +1 -1
  29. package/dist/command-bus/rpc/rpc-coordinator.spec.js +211 -0
  30. package/dist/command-bus/rpc/rpc-coordinator.spec.js.map +1 -1
  31. package/dist/command-bus/worker/index.d.ts +6 -1
  32. package/dist/command-bus/worker/index.js +8 -2
  33. package/dist/command-bus/worker/index.js.map +1 -1
  34. package/dist/command-bus/worker/worker-benchmark.d.ts +70 -0
  35. package/dist/command-bus/worker/worker-benchmark.js +210 -0
  36. package/dist/command-bus/worker/worker-benchmark.js.map +1 -0
  37. package/dist/command-bus/worker/worker-benchmark.spec.d.ts +1 -0
  38. package/dist/command-bus/worker/worker-benchmark.spec.js +300 -0
  39. package/dist/command-bus/worker/worker-benchmark.spec.js.map +1 -0
  40. package/dist/command-bus/worker/worker-metrics-collector.d.ts +87 -0
  41. package/dist/command-bus/worker/worker-metrics-collector.js +259 -0
  42. package/dist/command-bus/worker/worker-metrics-collector.js.map +1 -0
  43. package/dist/command-bus/worker/worker-metrics-collector.spec.d.ts +1 -0
  44. package/dist/command-bus/worker/worker-metrics-collector.spec.js +315 -0
  45. package/dist/command-bus/worker/worker-metrics-collector.spec.js.map +1 -0
  46. package/dist/command-bus/worker/worker-orchestrator.d.ts +29 -4
  47. package/dist/command-bus/worker/worker-orchestrator.js +198 -27
  48. package/dist/command-bus/worker/worker-orchestrator.js.map +1 -1
  49. package/dist/command-bus/worker/worker-orchestrator.spec.js +473 -52
  50. package/dist/command-bus/worker/worker-orchestrator.spec.js.map +1 -1
  51. package/dist/examples/auto-config.demo.d.ts +9 -0
  52. package/dist/examples/auto-config.demo.js +106 -0
  53. package/dist/examples/auto-config.demo.js.map +1 -0
  54. package/dist/examples/rpc-throughput.demo.d.ts +5 -0
  55. package/dist/examples/rpc-throughput.demo.js +326 -0
  56. package/dist/examples/rpc-throughput.demo.js.map +1 -0
  57. package/dist/pp-command-bus-1.1.0.tgz +0 -0
  58. package/package.json +3 -2
  59. package/dist/pp-command-bus-1.0.2.tgz +0 -0
@@ -16,16 +16,38 @@ const bullmq_1 = require("bullmq");
16
16
  const worker_orchestrator_1 = __importDefault(require("./worker-orchestrator"));
17
17
  // Mock BullMQ
18
18
  jest.mock('bullmq');
19
+ // Mock WorkerBenchmark
20
+ jest.mock('./worker-benchmark', () => {
21
+ return jest.fn().mockImplementation(() => ({
22
+ run: jest.fn().mockResolvedValue({
23
+ recommendedConcurrency: 5,
24
+ jobsPerSecond: 100,
25
+ avgProcessingTime: 10,
26
+ }),
27
+ }));
28
+ });
29
+ // Mock WorkerMetricsCollector
30
+ jest.mock('./worker-metrics-collector', () => {
31
+ return jest.fn().mockImplementation(() => ({
32
+ start: jest.fn(),
33
+ stop: jest.fn(),
34
+ recordJobProcessingTime: jest.fn(),
35
+ }));
36
+ });
19
37
  describe('WorkerOrchestrator', () => {
20
38
  let workerOrchestrator;
21
39
  let mockRedisConnection;
22
40
  let mockJobProcessor;
41
+ let mockQueueManager;
23
42
  let mockLogger;
24
43
  let mockWorker;
25
44
  const concurrency = 5;
26
45
  const maxAttempts = 3;
46
+ // Storage dla concurrency każdego workera
47
+ const workerConcurrencyStorage = new Map();
27
48
  beforeEach(() => {
28
49
  jest.clearAllMocks();
50
+ workerConcurrencyStorage.clear();
29
51
  mockLogger = {
30
52
  log: jest.fn(),
31
53
  error: jest.fn(),
@@ -37,42 +59,64 @@ describe('WorkerOrchestrator', () => {
37
59
  mockJobProcessor = {
38
60
  process: jest.fn().mockResolvedValue({ success: true }),
39
61
  };
40
- // Mock Worker
41
- mockWorker = {
42
- close: jest.fn().mockResolvedValue(undefined),
43
- on: jest.fn().mockReturnThis(),
44
- removeAllListeners: jest.fn().mockReturnThis(),
62
+ // Mock QueueManager
63
+ mockQueueManager = {
64
+ getOrCreateQueue: jest.fn().mockReturnValue({
65
+ name: 'test-queue',
66
+ }),
45
67
  };
46
- bullmq_1.Worker.mockImplementation(() => mockWorker);
47
- workerOrchestrator = new worker_orchestrator_1.default(mockRedisConnection, mockJobProcessor, concurrency, maxAttempts, mockLogger);
68
+ // Mock Worker factory z concurrency property per-instance
69
+ bullmq_1.Worker.mockImplementation((_queueName, _processor, options) => {
70
+ var _a;
71
+ const workerInstance = {
72
+ close: jest.fn().mockResolvedValue(undefined),
73
+ on: jest.fn().mockReturnThis(),
74
+ removeAllListeners: jest.fn().mockReturnThis(),
75
+ get concurrency() {
76
+ var _a;
77
+ return (_a = workerConcurrencyStorage.get(workerInstance)) !== null && _a !== void 0 ? _a : 5;
78
+ },
79
+ set concurrency(value) {
80
+ workerConcurrencyStorage.set(workerInstance, value);
81
+ },
82
+ };
83
+ // Ustaw początkową wartość z opcji lub domyślnie 5
84
+ const initialConcurrency = (_a = options === null || options === void 0 ? void 0 : options.concurrency) !== null && _a !== void 0 ? _a : 5;
85
+ workerConcurrencyStorage.set(workerInstance, initialConcurrency);
86
+ mockWorker = workerInstance;
87
+ return workerInstance;
88
+ });
89
+ workerOrchestrator = new worker_orchestrator_1.default(mockRedisConnection, mockJobProcessor, mockQueueManager, concurrency, maxAttempts, mockLogger);
48
90
  });
49
91
  describe('registerWorker', () => {
50
- it('powinno utworzyć nowy worker dla komendy', () => {
92
+ it('powinno utworzyć nowy worker dla komendy', () => __awaiter(void 0, void 0, void 0, function* () {
51
93
  // Given
52
94
  const commandName = 'TestCommand';
53
95
  // When
54
- workerOrchestrator.registerWorker(commandName);
96
+ yield workerOrchestrator.registerWorker(commandName);
55
97
  // Then
56
98
  expect(bullmq_1.Worker).toHaveBeenCalledWith(`{${commandName}}`, expect.any(Function), expect.objectContaining({
57
99
  connection: mockRedisConnection,
58
- concurrency,
100
+ concurrency: 5, // Z benchmark result
59
101
  }));
60
- expect(mockLogger.log).toHaveBeenCalledWith(`Worker zarejestrowany dla komendy ${commandName}`, expect.objectContaining({
61
- concurrency,
102
+ expect(mockLogger.log).toHaveBeenCalledWith('Benchmark zakończony - tworzę workera', expect.objectContaining({
103
+ commandName,
104
+ oldConcurrency: concurrency,
105
+ newConcurrency: 5,
62
106
  }));
63
- });
64
- it('powinno rzucić błąd gdy worker już istnieje dla komendy', () => {
107
+ }));
108
+ it('powinno rzucić błąd gdy worker już istnieje dla komendy', () => __awaiter(void 0, void 0, void 0, function* () {
65
109
  // Given
66
110
  const commandName = 'TestCommand';
67
- workerOrchestrator.registerWorker(commandName);
111
+ yield workerOrchestrator.registerWorker(commandName);
68
112
  // When & Then
69
- expect(() => workerOrchestrator.registerWorker(commandName)).toThrow(`Worker dla komendy ${commandName} już istnieje`);
70
- });
71
- it('powinno skonfigurować event handlery dla workera', () => {
113
+ yield expect(workerOrchestrator.registerWorker(commandName)).rejects.toThrow(`Worker dla komendy ${commandName} już istnieje`);
114
+ }));
115
+ it('powinno skonfigurować event handlery dla workera', () => __awaiter(void 0, void 0, void 0, function* () {
72
116
  // Given
73
117
  const commandName = 'TestCommand';
74
118
  // When
75
- workerOrchestrator.registerWorker(commandName);
119
+ yield workerOrchestrator.registerWorker(commandName);
76
120
  // Then
77
121
  expect(mockWorker.on).toHaveBeenCalledWith('failed', expect.any(Function));
78
122
  expect(mockWorker.on).toHaveBeenCalledWith('completed', expect.any(Function));
@@ -80,11 +124,11 @@ describe('WorkerOrchestrator', () => {
80
124
  expect(mockWorker.on).toHaveBeenCalledWith('stalled', expect.any(Function));
81
125
  expect(mockWorker.on).toHaveBeenCalledWith('progress', expect.any(Function));
82
126
  expect(mockWorker.on).toHaveBeenCalledWith('error', expect.any(Function));
83
- });
127
+ }));
84
128
  it("powinno wywołać jobProcessor.process w handler'ze workera", () => __awaiter(void 0, void 0, void 0, function* () {
85
129
  // Given
86
130
  const commandName = 'TestCommand';
87
- workerOrchestrator.registerWorker(commandName);
131
+ yield workerOrchestrator.registerWorker(commandName);
88
132
  const workerCalls = bullmq_1.Worker.mock.calls;
89
133
  const jobHandler = workerCalls[0][1];
90
134
  const mockJob = {
@@ -101,7 +145,7 @@ describe('WorkerOrchestrator', () => {
101
145
  const commandName = 'TestCommand';
102
146
  const processorError = new Error('Processor error');
103
147
  mockJobProcessor.process.mockRejectedValueOnce(processorError);
104
- workerOrchestrator.registerWorker(commandName);
148
+ yield workerOrchestrator.registerWorker(commandName);
105
149
  const workerCalls = bullmq_1.Worker.mock.calls;
106
150
  const jobHandler = workerCalls[0][1];
107
151
  const mockJob = {
@@ -118,10 +162,10 @@ describe('WorkerOrchestrator', () => {
118
162
  }));
119
163
  });
120
164
  describe('setupWorkerEventHandlers', () => {
121
- it("powinno zalogować event 'failed'", () => {
165
+ it("powinno zalogować event 'failed'", () => __awaiter(void 0, void 0, void 0, function* () {
122
166
  // Given
123
167
  const commandName = 'TestCommand';
124
- workerOrchestrator.registerWorker(commandName);
168
+ yield workerOrchestrator.registerWorker(commandName);
125
169
  const failedHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'failed')[1];
126
170
  const mockJob = {
127
171
  id: 'job-123',
@@ -141,11 +185,11 @@ describe('WorkerOrchestrator', () => {
141
185
  maxAttempts,
142
186
  isRetryable: true,
143
187
  }));
144
- });
145
- it("powinno zalogować event 'completed'", () => {
188
+ }));
189
+ it("powinno zalogować event 'completed'", () => __awaiter(void 0, void 0, void 0, function* () {
146
190
  // Given
147
191
  const commandName = 'TestCommand';
148
- workerOrchestrator.registerWorker(commandName);
192
+ yield workerOrchestrator.registerWorker(commandName);
149
193
  const completedHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'completed')[1];
150
194
  const mockJob = {
151
195
  id: 'job-123',
@@ -160,11 +204,11 @@ describe('WorkerOrchestrator', () => {
160
204
  jobId: 'job-123',
161
205
  hasReturnValue: true,
162
206
  }));
163
- });
164
- it("powinno zalogować event 'active'", () => {
207
+ }));
208
+ it("powinno zalogować event 'active'", () => __awaiter(void 0, void 0, void 0, function* () {
165
209
  // Given
166
210
  const commandName = 'TestCommand';
167
- workerOrchestrator.registerWorker(commandName);
211
+ yield workerOrchestrator.registerWorker(commandName);
168
212
  const activeHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'active')[1];
169
213
  const mockJob = {
170
214
  id: 'job-123',
@@ -177,11 +221,11 @@ describe('WorkerOrchestrator', () => {
177
221
  commandName,
178
222
  jobId: 'job-123',
179
223
  }));
180
- });
181
- it("powinno zalogować event 'stalled'", () => {
224
+ }));
225
+ it("powinno zalogować event 'stalled'", () => __awaiter(void 0, void 0, void 0, function* () {
182
226
  // Given
183
227
  const commandName = 'TestCommand';
184
- workerOrchestrator.registerWorker(commandName);
228
+ yield workerOrchestrator.registerWorker(commandName);
185
229
  const stalledHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'stalled')[1];
186
230
  const jobId = 'job-123';
187
231
  // When
@@ -192,11 +236,11 @@ describe('WorkerOrchestrator', () => {
192
236
  jobId,
193
237
  reason: 'Prawdopodobnie worker przestał odpowiadać',
194
238
  }));
195
- });
196
- it("powinno zalogować event 'progress'", () => {
239
+ }));
240
+ it("powinno zalogować event 'progress'", () => __awaiter(void 0, void 0, void 0, function* () {
197
241
  // Given
198
242
  const commandName = 'TestCommand';
199
- workerOrchestrator.registerWorker(commandName);
243
+ yield workerOrchestrator.registerWorker(commandName);
200
244
  const progressHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'progress')[1];
201
245
  const mockJob = {
202
246
  id: 'job-123',
@@ -211,11 +255,11 @@ describe('WorkerOrchestrator', () => {
211
255
  jobId: 'job-123',
212
256
  progress: 50,
213
257
  }));
214
- });
215
- it("powinno zalogować event 'error'", () => {
258
+ }));
259
+ it("powinno zalogować event 'error'", () => __awaiter(void 0, void 0, void 0, function* () {
216
260
  // Given
217
261
  const commandName = 'TestCommand';
218
- workerOrchestrator.registerWorker(commandName);
262
+ yield workerOrchestrator.registerWorker(commandName);
219
263
  const errorHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'error')[1];
220
264
  const error = new Error('Worker error');
221
265
  // When
@@ -225,11 +269,11 @@ describe('WorkerOrchestrator', () => {
225
269
  commandName,
226
270
  error: 'Worker error',
227
271
  }));
228
- });
229
- it('powinno obsłużyć brak attempt info w failed event', () => {
272
+ }));
273
+ it('powinno obsłużyć brak attempt info w failed event', () => __awaiter(void 0, void 0, void 0, function* () {
230
274
  // Given
231
275
  const commandName = 'TestCommand';
232
- workerOrchestrator.registerWorker(commandName);
276
+ yield workerOrchestrator.registerWorker(commandName);
233
277
  const failedHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'failed')[1];
234
278
  const mockJob = {
235
279
  id: 'job-123',
@@ -245,18 +289,18 @@ describe('WorkerOrchestrator', () => {
245
289
  maxAttempts,
246
290
  isRetryable: true, // (0 || 0) < maxAttempts = true
247
291
  }));
248
- });
292
+ }));
249
293
  });
250
294
  describe('getWorker', () => {
251
- it('powinno zwrócić workera dla komendy', () => {
295
+ it('powinno zwrócić workera dla komendy', () => __awaiter(void 0, void 0, void 0, function* () {
252
296
  // Given
253
297
  const commandName = 'TestCommand';
254
- workerOrchestrator.registerWorker(commandName);
298
+ yield workerOrchestrator.registerWorker(commandName);
255
299
  // When
256
300
  const worker = workerOrchestrator.getWorker(commandName);
257
301
  // Then
258
302
  expect(worker).toBe(mockWorker);
259
- });
303
+ }));
260
304
  it('powinno zwrócić undefined dla niezarejestrowanego workera', () => {
261
305
  // When
262
306
  const worker = workerOrchestrator.getWorker('NonExistentCommand');
@@ -267,17 +311,22 @@ describe('WorkerOrchestrator', () => {
267
311
  describe('closeAll', () => {
268
312
  it('powinno zamknąć wszystkich workerów', () => __awaiter(void 0, void 0, void 0, function* () {
269
313
  // Given
270
- workerOrchestrator.registerWorker('TestCommand1');
271
- workerOrchestrator.registerWorker('TestCommand2');
272
- workerOrchestrator.registerWorker('TestCommand3');
314
+ yield workerOrchestrator.registerWorker('TestCommand1');
315
+ yield workerOrchestrator.registerWorker('TestCommand2');
316
+ yield workerOrchestrator.registerWorker('TestCommand3');
317
+ const worker1 = workerOrchestrator.getWorker('TestCommand1');
318
+ const worker2 = workerOrchestrator.getWorker('TestCommand2');
319
+ const worker3 = workerOrchestrator.getWorker('TestCommand3');
273
320
  // When
274
321
  yield workerOrchestrator.closeAll();
275
- // Then
276
- expect(mockWorker.close).toHaveBeenCalledTimes(3);
322
+ // Then - każdy worker powinien mieć wywołane close
323
+ expect(worker1 === null || worker1 === void 0 ? void 0 : worker1.close).toHaveBeenCalledTimes(1);
324
+ expect(worker2 === null || worker2 === void 0 ? void 0 : worker2.close).toHaveBeenCalledTimes(1);
325
+ expect(worker3 === null || worker3 === void 0 ? void 0 : worker3.close).toHaveBeenCalledTimes(1);
277
326
  }));
278
327
  it('powinno wyczyścić cache workerów', () => __awaiter(void 0, void 0, void 0, function* () {
279
328
  // Given
280
- workerOrchestrator.registerWorker('TestCommand');
329
+ yield workerOrchestrator.registerWorker('TestCommand');
281
330
  yield workerOrchestrator.closeAll();
282
331
  // When
283
332
  const worker = workerOrchestrator.getWorker('TestCommand');
@@ -288,6 +337,378 @@ describe('WorkerOrchestrator', () => {
288
337
  // When & Then
289
338
  yield expect(workerOrchestrator.closeAll()).resolves.toBeUndefined();
290
339
  }));
340
+ it('powinno zatrzymać wszystkie collectory metryk', () => __awaiter(void 0, void 0, void 0, function* () {
341
+ // Given
342
+ const WorkerMetricsCollector = jest.requireMock('./worker-metrics-collector');
343
+ yield workerOrchestrator.registerWorker('TestCommand');
344
+ // When
345
+ yield workerOrchestrator.closeAll();
346
+ // Then
347
+ const collectorInstance = WorkerMetricsCollector.mock.results[0].value;
348
+ expect(collectorInstance.stop).toHaveBeenCalled();
349
+ }));
350
+ it('powinno usunąć wszystkie event listeners przed zamknięciem', () => __awaiter(void 0, void 0, void 0, function* () {
351
+ // Given
352
+ yield workerOrchestrator.registerWorker('TestCommand');
353
+ // When
354
+ yield workerOrchestrator.closeAll();
355
+ // Then
356
+ expect(mockWorker.removeAllListeners).toHaveBeenCalled();
357
+ }));
358
+ });
359
+ describe('adjustConcurrency', () => {
360
+ beforeEach(() => {
361
+ jest.useFakeTimers();
362
+ });
363
+ afterEach(() => {
364
+ jest.useRealTimers();
365
+ });
366
+ it('powinno zwiększyć concurrency o 20%', () => __awaiter(void 0, void 0, void 0, function* () {
367
+ // Given - użyj wyższego początkowego concurrency żeby test był sensowny
368
+ const WorkerBenchmark = jest.requireMock('./worker-benchmark');
369
+ WorkerBenchmark.mockImplementation(() => ({
370
+ run: jest.fn().mockResolvedValue({
371
+ recommendedConcurrency: 50, // Wysokie początkowe concurrency
372
+ jobsPerSecond: 100,
373
+ avgProcessingTime: 10,
374
+ }),
375
+ }));
376
+ const commandName = 'TestCommand';
377
+ yield workerOrchestrator.registerWorker(commandName);
378
+ const worker = workerOrchestrator.getWorker(commandName);
379
+ // When
380
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'Test reason');
381
+ // Then: 50 * 1.2 = 60
382
+ expect(worker === null || worker === void 0 ? void 0 : worker.concurrency).toBe(60);
383
+ expect(mockLogger.log).toHaveBeenCalledWith('Dynamiczna zmiana concurrency', expect.objectContaining({
384
+ commandName,
385
+ oldConcurrency: 50,
386
+ newConcurrency: 60,
387
+ change: '+20%',
388
+ direction: 'increase',
389
+ reason: 'Test reason',
390
+ }));
391
+ }));
392
+ it('powinno zmniejszyć concurrency o 20%', () => __awaiter(void 0, void 0, void 0, function* () {
393
+ // Given - użyj wyższego początkowego concurrency
394
+ const WorkerBenchmark = jest.requireMock('./worker-benchmark');
395
+ WorkerBenchmark.mockImplementation(() => ({
396
+ run: jest.fn().mockResolvedValue({
397
+ recommendedConcurrency: 50,
398
+ jobsPerSecond: 100,
399
+ avgProcessingTime: 10,
400
+ }),
401
+ }));
402
+ const commandName = 'TestCommand';
403
+ yield workerOrchestrator.registerWorker(commandName);
404
+ const worker = workerOrchestrator.getWorker(commandName);
405
+ // When
406
+ workerOrchestrator.adjustConcurrency(commandName, 'decrease', 'Low backlog');
407
+ // Then: 50 * 0.8 = 40
408
+ expect(worker === null || worker === void 0 ? void 0 : worker.concurrency).toBe(40);
409
+ expect(mockLogger.log).toHaveBeenCalledWith('Dynamiczna zmiana concurrency', expect.objectContaining({
410
+ oldConcurrency: 50,
411
+ newConcurrency: 40,
412
+ change: '-20%',
413
+ direction: 'decrease',
414
+ }));
415
+ }));
416
+ it('powinno zastosować limit minimum 10', () => __awaiter(void 0, void 0, void 0, function* () {
417
+ // Given
418
+ const commandName = 'TestCommand';
419
+ yield workerOrchestrator.registerWorker(commandName);
420
+ const worker = workerOrchestrator.getWorker(commandName);
421
+ // Zmniejsz kilka razy do limitu
422
+ workerOrchestrator.adjustConcurrency(commandName, 'decrease', 'reason');
423
+ jest.advanceTimersByTime(31000); // Przekrocz cooldown
424
+ workerOrchestrator.adjustConcurrency(commandName, 'decrease', 'reason');
425
+ jest.advanceTimersByTime(31000);
426
+ workerOrchestrator.adjustConcurrency(commandName, 'decrease', 'reason');
427
+ jest.advanceTimersByTime(31000);
428
+ // When - próba zmniejszenia poniżej 10
429
+ workerOrchestrator.adjustConcurrency(commandName, 'decrease', 'reason');
430
+ // Then - powinno zatrzymać się na 10
431
+ expect(worker === null || worker === void 0 ? void 0 : worker.concurrency).toBeGreaterThanOrEqual(10);
432
+ }));
433
+ it('powinno zastosować limit maksimum 2000', () => __awaiter(void 0, void 0, void 0, function* () {
434
+ // Given - mockuj benchmark aby zwrócił wysokie concurrency
435
+ const WorkerBenchmark = jest.requireMock('./worker-benchmark');
436
+ WorkerBenchmark.mockImplementation(() => ({
437
+ run: jest.fn().mockResolvedValue({
438
+ recommendedConcurrency: 1800,
439
+ jobsPerSecond: 1000,
440
+ avgProcessingTime: 5,
441
+ }),
442
+ }));
443
+ const commandName = 'HighConcurrencyCommand';
444
+ yield workerOrchestrator.registerWorker(commandName);
445
+ const worker = workerOrchestrator.getWorker(commandName);
446
+ // Zwiększ kilka razy
447
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'reason');
448
+ jest.advanceTimersByTime(31000);
449
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'reason');
450
+ jest.advanceTimersByTime(31000);
451
+ // When - próba zwiększenia powyżej 2000
452
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'reason');
453
+ // Then - powinno zatrzymać się na 2000
454
+ expect(worker === null || worker === void 0 ? void 0 : worker.concurrency).toBeLessThanOrEqual(2000);
455
+ }));
456
+ it('powinno respektować cooldown 30s między zmianami', () => __awaiter(void 0, void 0, void 0, function* () {
457
+ // Given
458
+ const commandName = 'TestCommand';
459
+ yield workerOrchestrator.registerWorker(commandName);
460
+ const worker = workerOrchestrator.getWorker(commandName);
461
+ // First change
462
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'reason 1');
463
+ const firstConcurrency = worker === null || worker === void 0 ? void 0 : worker.concurrency;
464
+ // When - próba zmiany w czasie cooldown
465
+ jest.advanceTimersByTime(15000); // 15s - poniżej cooldown
466
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'reason 2');
467
+ // Then - concurrency nie powinno się zmienić
468
+ expect(worker === null || worker === void 0 ? void 0 : worker.concurrency).toBe(firstConcurrency);
469
+ expect(mockLogger.debug).toHaveBeenCalledWith('Cooldown aktywny - pomijam zmianę concurrency', expect.any(Object));
470
+ }));
471
+ it('powinno pozwolić na zmianę po upływie cooldown', () => __awaiter(void 0, void 0, void 0, function* () {
472
+ var _a;
473
+ // Given - użyj wyższego początkowego concurrency
474
+ const WorkerBenchmark = jest.requireMock('./worker-benchmark');
475
+ WorkerBenchmark.mockImplementation(() => ({
476
+ run: jest.fn().mockResolvedValue({
477
+ recommendedConcurrency: 1000,
478
+ jobsPerSecond: 100,
479
+ avgProcessingTime: 10,
480
+ }),
481
+ }));
482
+ const commandName = 'TestCommand';
483
+ yield workerOrchestrator.registerWorker(commandName);
484
+ const worker = workerOrchestrator.getWorker(commandName);
485
+ // First change: 1000 * 1.2 = 1200
486
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'reason 1');
487
+ const firstConcurrency = (_a = worker === null || worker === void 0 ? void 0 : worker.concurrency) !== null && _a !== void 0 ? _a : 0;
488
+ expect(firstConcurrency).toBe(1200);
489
+ // When - czekamy > 30s i zmieniamy ponownie
490
+ jest.advanceTimersByTime(31000); // 31s - powyżej cooldown
491
+ workerOrchestrator.adjustConcurrency(commandName, 'increase', 'reason 2');
492
+ // Then - concurrency powinno się zmienić: 1200 * 1.2 = 1440
493
+ expect(worker === null || worker === void 0 ? void 0 : worker.concurrency).toBe(1440);
494
+ }));
495
+ it('powinno zalogować warning gdy worker nie istnieje', () => {
496
+ // When
497
+ workerOrchestrator.adjustConcurrency('NonExistentCommand', 'increase', 'reason');
498
+ // Then
499
+ expect(mockLogger.warn).toHaveBeenCalledWith('Nie można dostosować concurrency - worker nie istnieje', expect.objectContaining({
500
+ commandName: 'NonExistentCommand',
501
+ }));
502
+ });
503
+ it('nie powinno zmieniać concurrency gdy wartość się nie zmienia (na limicie)', () => __awaiter(void 0, void 0, void 0, function* () {
504
+ var _a;
505
+ // Given - zresetuj mock benchmark do domyślnych wartości
506
+ const WorkerBenchmark = jest.requireMock('./worker-benchmark');
507
+ WorkerBenchmark.mockImplementation(() => ({
508
+ run: jest.fn().mockResolvedValue({
509
+ recommendedConcurrency: 50, // Wystarczająco wysokie aby test miał sens
510
+ jobsPerSecond: 100,
511
+ avgProcessingTime: 10,
512
+ }),
513
+ }));
514
+ const commandName = 'TestCommand';
515
+ yield workerOrchestrator.registerWorker(commandName);
516
+ const worker = workerOrchestrator.getWorker(commandName);
517
+ // Ustaw na minimum (10): 50 -> 40 -> 32 -> 26 -> 21 -> 17 -> 14 -> 11 -> 10 -> 10
518
+ for (let i = 0; i < 10; i++) {
519
+ workerOrchestrator.adjustConcurrency(commandName, 'decrease', 'reason');
520
+ jest.advanceTimersByTime(31000);
521
+ }
522
+ const minConcurrency = (_a = worker === null || worker === void 0 ? void 0 : worker.concurrency) !== null && _a !== void 0 ? _a : 0;
523
+ expect(minConcurrency).toBe(10); // Powinno osiągnąć minimum 10
524
+ // When - próba dalszego zmniejszenia na limicie
525
+ workerOrchestrator.adjustConcurrency(commandName, 'decrease', 'reason');
526
+ // Then - wartość nie powinna się zmienić
527
+ expect(worker === null || worker === void 0 ? void 0 : worker.concurrency).toBe(minConcurrency);
528
+ expect(mockLogger.debug).toHaveBeenCalledWith('Concurrency bez zmian - osiągnięto limit', expect.any(Object));
529
+ }));
530
+ });
531
+ describe('integracja z WorkerBenchmark', () => {
532
+ it('powinno uruchomić benchmark przed utworzeniem workera', () => __awaiter(void 0, void 0, void 0, function* () {
533
+ // Given
534
+ const WorkerBenchmark = jest.requireMock('./worker-benchmark');
535
+ const commandName = 'TestCommand';
536
+ // When
537
+ yield workerOrchestrator.registerWorker(commandName);
538
+ // Then
539
+ expect(WorkerBenchmark).toHaveBeenCalledWith(commandName, mockRedisConnection, mockJobProcessor, mockLogger);
540
+ const benchmarkInstance = WorkerBenchmark.mock.results[0].value;
541
+ expect(benchmarkInstance.run).toHaveBeenCalled();
542
+ }));
543
+ it('powinno użyć recommendedConcurrency z benchmarku', () => __awaiter(void 0, void 0, void 0, function* () {
544
+ // Given
545
+ const WorkerBenchmark = jest.requireMock('./worker-benchmark');
546
+ WorkerBenchmark.mockImplementation(() => ({
547
+ run: jest.fn().mockResolvedValue({
548
+ recommendedConcurrency: 42,
549
+ jobsPerSecond: 200,
550
+ avgProcessingTime: 20,
551
+ }),
552
+ }));
553
+ const commandName = 'OptimizedCommand';
554
+ // When
555
+ yield workerOrchestrator.registerWorker(commandName);
556
+ // Then
557
+ expect(bullmq_1.Worker).toHaveBeenCalledWith(`{${commandName}}`, expect.any(Function), expect.objectContaining({
558
+ concurrency: 42,
559
+ }));
560
+ }));
561
+ });
562
+ describe('integracja z WorkerMetricsCollector', () => {
563
+ it('powinno utworzyć i uruchomić metrics collector', () => __awaiter(void 0, void 0, void 0, function* () {
564
+ // Given
565
+ const WorkerMetricsCollector = jest.requireMock('./worker-metrics-collector');
566
+ const commandName = 'TestCommand';
567
+ // When
568
+ yield workerOrchestrator.registerWorker(commandName);
569
+ // Then
570
+ expect(WorkerMetricsCollector).toHaveBeenCalledWith(commandName, expect.any(Object), // queue
571
+ mockLogger, expect.any(Function));
572
+ const collectorInstance = WorkerMetricsCollector.mock.results[0].value;
573
+ expect(collectorInstance.start).toHaveBeenCalled();
574
+ }));
575
+ it('powinno wywołać adjustConcurrency gdy collector rekomenduje zmianę', () => __awaiter(void 0, void 0, void 0, function* () {
576
+ // Given
577
+ const commandName = 'TestCommand';
578
+ yield workerOrchestrator.registerWorker(commandName);
579
+ // Pobierz callback przekazany do WorkerMetricsCollector
580
+ const WorkerMetricsCollector = jest.requireMock('./worker-metrics-collector');
581
+ const collectorCallback = WorkerMetricsCollector.mock.calls[0][3];
582
+ // When - symuluj rekomendację od collectora
583
+ collectorCallback({
584
+ shouldAdjust: true,
585
+ direction: 'increase',
586
+ reason: 'High backlog detected',
587
+ currentMetrics: {
588
+ waiting: 100,
589
+ active: 10,
590
+ jobsPerSecond: 5,
591
+ avgProcessingTimeMs: 200,
592
+ timestamp: Date.now(),
593
+ },
594
+ });
595
+ // Then
596
+ expect(mockWorker.concurrency).toBeGreaterThan(5); // Zwiększone o 20%
597
+ }));
598
+ it('nie powinno wywoływać adjustConcurrency gdy direction=none', () => __awaiter(void 0, void 0, void 0, function* () {
599
+ // Given
600
+ const commandName = 'TestCommand';
601
+ yield workerOrchestrator.registerWorker(commandName);
602
+ const WorkerMetricsCollector = jest.requireMock('./worker-metrics-collector');
603
+ const collectorCallback = WorkerMetricsCollector.mock.calls[0][3];
604
+ const initialConcurrency = mockWorker.concurrency;
605
+ // When - symuluj brak rekomendacji
606
+ collectorCallback({
607
+ shouldAdjust: false,
608
+ direction: 'none',
609
+ reason: 'Stable metrics',
610
+ currentMetrics: {
611
+ waiting: 10,
612
+ active: 10,
613
+ jobsPerSecond: 10,
614
+ avgProcessingTimeMs: 100,
615
+ timestamp: Date.now(),
616
+ },
617
+ });
618
+ // Then - concurrency nie zmienione
619
+ expect(mockWorker.concurrency).toBe(initialConcurrency);
620
+ }));
621
+ });
622
+ describe('tracking czasu przetwarzania jobów', () => {
623
+ beforeEach(() => {
624
+ jest.useFakeTimers();
625
+ });
626
+ afterEach(() => {
627
+ jest.useRealTimers();
628
+ });
629
+ it('powinno zapisać czas przetwarzania w metricsCollector', () => __awaiter(void 0, void 0, void 0, function* () {
630
+ // Given
631
+ const commandName = 'TestCommand';
632
+ yield workerOrchestrator.registerWorker(commandName);
633
+ const WorkerMetricsCollector = jest.requireMock('./worker-metrics-collector');
634
+ const collectorInstance = WorkerMetricsCollector.mock.results[0].value;
635
+ // Pobierz handlery eventów
636
+ const activeHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'active')[1];
637
+ const completedHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'completed')[1];
638
+ const mockJob = {
639
+ id: 'job-123',
640
+ data: { __id: 'cmd-123' },
641
+ returnvalue: { success: true },
642
+ };
643
+ // When - symuluj job lifecycle
644
+ activeHandler(mockJob); // Start job
645
+ jest.advanceTimersByTime(150); // 150ms przetwarzania
646
+ completedHandler(mockJob); // Complete job
647
+ // Then - czas przetwarzania powinien być zapisany
648
+ expect(collectorInstance.recordJobProcessingTime).toHaveBeenCalledWith(150);
649
+ }));
650
+ it('powinno obsłużyć sytuację gdy brak jobId', () => __awaiter(void 0, void 0, void 0, function* () {
651
+ // Given
652
+ const commandName = 'TestCommand';
653
+ yield workerOrchestrator.registerWorker(commandName);
654
+ const completedHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'completed')[1];
655
+ const mockJob = {
656
+ id: undefined, // Brak ID
657
+ data: { __id: 'cmd-123' },
658
+ };
659
+ // When
660
+ completedHandler(mockJob);
661
+ // Then - nie powinno wywołać recordJobProcessingTime
662
+ const WorkerMetricsCollector = jest.requireMock('./worker-metrics-collector');
663
+ const collectorInstance = WorkerMetricsCollector.mock.results[0].value;
664
+ expect(collectorInstance.recordJobProcessingTime).not.toHaveBeenCalled();
665
+ }));
666
+ it('powinno wyczyścić czas startu dla failed jobs', () => __awaiter(void 0, void 0, void 0, function* () {
667
+ // Given
668
+ const commandName = 'TestCommand';
669
+ yield workerOrchestrator.registerWorker(commandName);
670
+ const activeHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'active')[1];
671
+ const failedHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'failed')[1];
672
+ const completedHandler = mockWorker.on.mock.calls.find((call) => call[0] === 'completed')[1];
673
+ const mockJob = {
674
+ id: 'job-123',
675
+ data: { __id: 'cmd-123' },
676
+ attemptsMade: 1,
677
+ opts: { attempts: 3 },
678
+ };
679
+ // When - job fails
680
+ activeHandler(mockJob);
681
+ failedHandler(mockJob, new Error('Job failed'));
682
+ // Then - próba completed nie powinna zapisać czasu (start time został wyczyszczony)
683
+ completedHandler(mockJob);
684
+ const WorkerMetricsCollector = jest.requireMock('./worker-metrics-collector');
685
+ const collectorInstance = WorkerMetricsCollector.mock.results[0].value;
686
+ // recordJobProcessingTime nie powinno być wywołane dla failed job
687
+ expect(collectorInstance.recordJobProcessingTime).not.toHaveBeenCalled();
688
+ }));
689
+ });
690
+ describe('getWorkerStats', () => {
691
+ it('powinno zwrócić statystyki workerów', () => __awaiter(void 0, void 0, void 0, function* () {
692
+ // Given
693
+ yield workerOrchestrator.registerWorker('Command1');
694
+ yield workerOrchestrator.registerWorker('Command2');
695
+ // When
696
+ const stats = workerOrchestrator.getWorkerStats();
697
+ // Then
698
+ expect(stats).toEqual({
699
+ activeWorkersCount: 2,
700
+ workerNames: ['Command1', 'Command2'],
701
+ });
702
+ }));
703
+ it('powinno zwrócić puste statystyki gdy brak workerów', () => {
704
+ // When
705
+ const stats = workerOrchestrator.getWorkerStats();
706
+ // Then
707
+ expect(stats).toEqual({
708
+ activeWorkersCount: 0,
709
+ workerNames: [],
710
+ });
711
+ });
291
712
  });
292
713
  });
293
714
  //# sourceMappingURL=worker-orchestrator.spec.js.map