|
1 | 1 | import { redisTest } from "@internal/testcontainers"; |
2 | 2 | import { describe, expect, vi } from "vitest"; |
3 | 3 | import { BatchQueue } from "../index.js"; |
| 4 | +import type { GlobalRateLimiter } from "@trigger.dev/redis-worker"; |
4 | 5 | import type { CompleteBatchResult, InitializeBatchOptions, BatchItem } from "../types.js"; |
5 | 6 |
|
6 | 7 | vi.setConfig({ testTimeout: 60_000 }); |
@@ -658,4 +659,121 @@ describe("BatchQueue", () => { |
658 | 659 | } |
659 | 660 | ); |
660 | 661 | }); |
| 662 | + |
| 663 | + describe("global rate limiter at worker queue consumer level", () => { |
| 664 | + redisTest( |
| 665 | + "should call rate limiter before each processing attempt", |
| 666 | + async ({ redisContainer }) => { |
| 667 | + let limitCallCount = 0; |
| 668 | + const rateLimiter: GlobalRateLimiter = { |
| 669 | + async limit() { |
| 670 | + limitCallCount++; |
| 671 | + return { allowed: true }; |
| 672 | + }, |
| 673 | + }; |
| 674 | + |
| 675 | + const queue = new BatchQueue({ |
| 676 | + redis: { |
| 677 | + host: redisContainer.getHost(), |
| 678 | + port: redisContainer.getPort(), |
| 679 | + keyPrefix: "test:", |
| 680 | + }, |
| 681 | + drr: { quantum: 5, maxDeficit: 50 }, |
| 682 | + consumerCount: 1, |
| 683 | + consumerIntervalMs: 50, |
| 684 | + startConsumers: true, |
| 685 | + globalRateLimiter: rateLimiter, |
| 686 | + }); |
| 687 | + |
| 688 | + let completionResult: CompleteBatchResult | null = null; |
| 689 | + |
| 690 | + try { |
| 691 | + queue.onProcessItem(async ({ itemIndex }) => { |
| 692 | + return { success: true, runId: `run_${itemIndex}` }; |
| 693 | + }); |
| 694 | + |
| 695 | + queue.onBatchComplete(async (result) => { |
| 696 | + completionResult = result; |
| 697 | + }); |
| 698 | + |
| 699 | + const itemCount = 5; |
| 700 | + await queue.initializeBatch(createInitOptions("batch1", "env1", itemCount)); |
| 701 | + await enqueueItems(queue, "batch1", "env1", createBatchItems(itemCount)); |
| 702 | + |
| 703 | + await vi.waitFor( |
| 704 | + () => { |
| 705 | + expect(completionResult).not.toBeNull(); |
| 706 | + }, |
| 707 | + { timeout: 10000 } |
| 708 | + ); |
| 709 | + |
| 710 | + expect(completionResult!.successfulRunCount).toBe(itemCount); |
| 711 | + // Rate limiter is called before each blockingPop, including iterations |
| 712 | + // where no message is available, so count >= items processed |
| 713 | + expect(limitCallCount).toBeGreaterThanOrEqual(itemCount); |
| 714 | + } finally { |
| 715 | + await queue.close(); |
| 716 | + } |
| 717 | + } |
| 718 | + ); |
| 719 | + |
| 720 | + redisTest( |
| 721 | + "should delay processing when rate limited", |
| 722 | + async ({ redisContainer }) => { |
| 723 | + let limitCallCount = 0; |
| 724 | + const rateLimiter: GlobalRateLimiter = { |
| 725 | + async limit() { |
| 726 | + limitCallCount++; |
| 727 | + // Rate limit the first 3 calls, then allow |
| 728 | + if (limitCallCount <= 3) { |
| 729 | + return { allowed: false, resetAt: Date.now() + 100 }; |
| 730 | + } |
| 731 | + return { allowed: true }; |
| 732 | + }, |
| 733 | + }; |
| 734 | + |
| 735 | + const queue = new BatchQueue({ |
| 736 | + redis: { |
| 737 | + host: redisContainer.getHost(), |
| 738 | + port: redisContainer.getPort(), |
| 739 | + keyPrefix: "test:", |
| 740 | + }, |
| 741 | + drr: { quantum: 5, maxDeficit: 50 }, |
| 742 | + consumerCount: 1, |
| 743 | + consumerIntervalMs: 50, |
| 744 | + startConsumers: true, |
| 745 | + globalRateLimiter: rateLimiter, |
| 746 | + }); |
| 747 | + |
| 748 | + let completionResult: CompleteBatchResult | null = null; |
| 749 | + |
| 750 | + try { |
| 751 | + queue.onProcessItem(async ({ itemIndex }) => { |
| 752 | + return { success: true, runId: `run_${itemIndex}` }; |
| 753 | + }); |
| 754 | + |
| 755 | + queue.onBatchComplete(async (result) => { |
| 756 | + completionResult = result; |
| 757 | + }); |
| 758 | + |
| 759 | + await queue.initializeBatch(createInitOptions("batch1", "env1", 3)); |
| 760 | + await enqueueItems(queue, "batch1", "env1", createBatchItems(3)); |
| 761 | + |
| 762 | + // Should still complete despite initial rate limiting |
| 763 | + await vi.waitFor( |
| 764 | + () => { |
| 765 | + expect(completionResult).not.toBeNull(); |
| 766 | + }, |
| 767 | + { timeout: 10000 } |
| 768 | + ); |
| 769 | + |
| 770 | + expect(completionResult!.successfulRunCount).toBe(3); |
| 771 | + // Rate limiter was called more times than items due to initial rejections |
| 772 | + expect(limitCallCount).toBeGreaterThan(3); |
| 773 | + } finally { |
| 774 | + await queue.close(); |
| 775 | + } |
| 776 | + } |
| 777 | + ); |
| 778 | + }); |
661 | 779 | }); |
0 commit comments