import { describe, expect, vi } from "vitest"; // Mock the db prisma client vi.mock("~/db.server", () => ({ prisma: {}, $replica: {}, })); import { redisTest } from "@internal/testcontainers"; import { FairDequeuingStrategy } from "../app/v3/marqs/fairDequeuingStrategy.server.js"; import { calculateStandardDeviation, createKeyProducer, setupConcurrency, setupQueue, } from "./utils/marqs.js"; import { trace } from "@opentelemetry/api"; import type { EnvQueues } from "~/v3/marqs/types.js"; import { MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET } from "~/v3/marqs/constants.server.js"; import { createRedisClient } from "@internal/redis"; const tracer = trace.getTracer("test"); vi.setConfig({ testTimeout: 30_000 }); // 30 seconds timeout describe("FairDequeuingStrategy", () => { redisTest("should distribute a single queue from a single env", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-1", // for deterministic shuffling }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: Date.now() - 1000, // 1 second ago queueId: "queue-1", orgId: "org-1", envId: "env-1", }); const result = await strategy.distributeFairQueuesFromParentQueue("parent-queue", "consumer-1"); expect(result).toHaveLength(1); expect(result[0]).toEqual({ envId: "env-1", queues: ["org:org-1:env:env-1:queue:queue-1"], }); }); redisTest("should respect env concurrency limits", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 2, parentQueueLimit: 100, seed: "test-seed-3", }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: Date.now() - 1000, queueId: "queue-1", orgId: "org-1", envId: "env-1", }); await setupConcurrency({ redis, keyProducer, env: { id: "env-1", currentConcurrency: 2, limit: 2 }, }); const result = await strategy.distributeFairQueuesFromParentQueue("parent-queue", "consumer-1"); expect(result).toHaveLength(0); }); redisTest( "should give extra concurrency when the env has reserve concurrency", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 2, parentQueueLimit: 100, seed: "test-seed-3", }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: Date.now() - 1000, queueId: "queue-1", orgId: "org-1", envId: "env-1", }); await setupConcurrency({ redis, keyProducer, env: { id: "env-1", currentConcurrency: 2, limit: 2, reserveConcurrency: 1 }, }); const result = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); expect(result).toHaveLength(1); expect(result[0]).toEqual({ envId: "env-1", queues: ["org:org-1:env:env-1:queue:queue-1"], }); } ); redisTest("should respect parentQueueLimit", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 2, // Only take 2 queues seed: "test-seed-6", }); const now = Date.now(); // Setup 3 queues but parentQueueLimit is 2 await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 3000, queueId: "queue-1", orgId: "org-1", envId: "env-1", }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 2000, queueId: "queue-2", orgId: "org-1", envId: "env-1", }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 1000, queueId: "queue-3", orgId: "org-1", envId: "env-1", }); const result = await strategy.distributeFairQueuesFromParentQueue("parent-queue", "consumer-1"); expect(result).toHaveLength(1); const queue1 = keyProducer.queueKey("org-1", "env-1", "queue-1"); const queue2 = keyProducer.queueKey("org-1", "env-1", "queue-2"); expect(result[0]).toEqual({ envId: "env-1", queues: [queue1, queue2], }); }); redisTest( "should reuse snapshots across calls for the same consumer", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 10, seed: "test-seed-reuse-1", reuseSnapshotCount: 1, }); const now = Date.now(); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 3000, queueId: "queue-1", orgId: "org-1", envId: "env-1", }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 2000, queueId: "queue-2", orgId: "org-2", envId: "env-2", }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 1000, queueId: "queue-3", orgId: "org-3", envId: "env-3", }); const startDistribute1 = performance.now(); const envResult = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); const result = flattenResults(envResult); const distribute1Duration = performance.now() - startDistribute1; console.log("First distribution took", distribute1Duration, "ms"); expect(result).toHaveLength(3); // Should only get the two oldest queues const queue1 = keyProducer.queueKey("org-1", "env-1", "queue-1"); const queue2 = keyProducer.queueKey("org-2", "env-2", "queue-2"); const queue3 = keyProducer.queueKey("org-3", "env-3", "queue-3"); expect(result).toEqual([queue2, queue1, queue3]); const startDistribute2 = performance.now(); const _result2 = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); const tolerance = 0.15; const withTolerance = (value: number) => value * (1 + tolerance); const distribute2Duration = performance.now() - startDistribute2; console.log("Second distribution took", distribute2Duration, "ms"); // Make sure the second call is faster than the first expect(distribute2Duration).toBeLessThan(distribute1Duration); const startDistribute3 = performance.now(); const _result3 = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); const distribute3Duration = performance.now() - startDistribute3; console.log("Third distribution took", distribute3Duration, "ms"); // Make sure the third call is faster than the second expect(withTolerance(distribute3Duration)).toBeGreaterThan(distribute2Duration); } ); redisTest( "should fairly distribute queues across environments over time", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-5", }); const now = Date.now(); // Test configuration const orgs = ["org-1", "org-2", "org-3"]; const envsPerOrg = 3; // Each org has 3 environments const queuesPerEnv = 5; // Each env has 5 queues const iterations = 1000; // Setup queues for (const orgId of orgs) { for (let envNum = 1; envNum <= envsPerOrg; envNum++) { const envId = `env-${orgId}-${envNum}`; for (let queueNum = 1; queueNum <= queuesPerEnv; queueNum++) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", // Vary the ages slightly score: now - Math.random() * 10000, queueId: `queue-${orgId}-${envId}-${queueNum}`, orgId, envId, }); } // Setup reasonable concurrency limits await setupConcurrency({ redis, keyProducer, env: { id: envId, currentConcurrency: 1, limit: 5 }, }); } } // Track distribution statistics type PositionStats = { firstPosition: number; // Count of times this env/org was first positionSums: number; // Sum of positions (for averaging) appearances: number; // Total number of appearances }; const envStats: Record = {}; const orgStats: Record = {}; // Initialize stats objects for (const orgId of orgs) { orgStats[orgId] = { firstPosition: 0, positionSums: 0, appearances: 0 }; for (let envNum = 1; envNum <= envsPerOrg; envNum++) { const envId = `env-${orgId}-${envNum}`; envStats[envId] = { firstPosition: 0, positionSums: 0, appearances: 0 }; } } // Run multiple iterations for (let i = 0; i < iterations; i++) { const envResult = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", `consumer-${i % 3}` // Simulate 3 different consumers ); const result = flattenResults(envResult); // Track positions of queues result.forEach((queueId, position) => { const orgId = keyProducer.orgIdFromQueue(queueId); const envId = keyProducer.envIdFromQueue(queueId); // Update org stats orgStats[orgId].appearances++; orgStats[orgId].positionSums += position; if (position === 0) orgStats[orgId].firstPosition++; // Update env stats envStats[envId].appearances++; envStats[envId].positionSums += position; if (position === 0) envStats[envId].firstPosition++; }); } // Calculate and log statistics console.log("\nOrganization Statistics:"); for (const [orgId, stats] of Object.entries(orgStats)) { const avgPosition = stats.positionSums / stats.appearances; const firstPositionPercentage = (stats.firstPosition / iterations) * 100; console.log(`${orgId}: First Position: ${firstPositionPercentage.toFixed(2)}% Average Position: ${avgPosition.toFixed(2)} Total Appearances: ${stats.appearances}`); } console.log("\nEnvironment Statistics:"); for (const [envId, stats] of Object.entries(envStats)) { const avgPosition = stats.positionSums / stats.appearances; const firstPositionPercentage = (stats.firstPosition / iterations) * 100; console.log(`${envId}: First Position: ${firstPositionPercentage.toFixed(2)}% Average Position: ${avgPosition.toFixed(2)} Total Appearances: ${stats.appearances}`); } // Verify fairness of first position distribution const expectedFirstPositionPercentage = 100 / orgs.length; const firstPositionStdDevOrgs = calculateStandardDeviation( Object.values(orgStats).map((stats) => (stats.firstPosition / iterations) * 100) ); const expectedEnvFirstPositionPercentage = 100 / (orgs.length * envsPerOrg); const firstPositionStdDevEnvs = calculateStandardDeviation( Object.values(envStats).map((stats) => (stats.firstPosition / iterations) * 100) ); // Assert reasonable fairness for first position expect(firstPositionStdDevOrgs).toBeLessThan(5); // Allow 5% standard deviation for orgs expect(firstPositionStdDevEnvs).toBeLessThan(5); // Allow 5% standard deviation for envs // Verify that each org and env gets a fair chance at first position for (const [_orgId, stats] of Object.entries(orgStats)) { const firstPositionPercentage = (stats.firstPosition / iterations) * 100; expect(firstPositionPercentage).toBeGreaterThan(expectedFirstPositionPercentage * 0.7); // Within 30% of expected expect(firstPositionPercentage).toBeLessThan(expectedFirstPositionPercentage * 1.3); } for (const [_envId, stats] of Object.entries(envStats)) { const firstPositionPercentage = (stats.firstPosition / iterations) * 100; expect(firstPositionPercentage).toBeGreaterThan(expectedEnvFirstPositionPercentage * 0.7); // Within 30% of expected expect(firstPositionPercentage).toBeLessThan(expectedEnvFirstPositionPercentage * 1.3); } // Verify average positions are reasonably distributed const avgPositionsOrgs = Object.values(orgStats).map( (stats) => stats.positionSums / stats.appearances ); const avgPositionsEnvs = Object.values(envStats).map( (stats) => stats.positionSums / stats.appearances ); const avgPositionStdDevOrgs = calculateStandardDeviation(avgPositionsOrgs); const avgPositionStdDevEnvs = calculateStandardDeviation(avgPositionsEnvs); expect(avgPositionStdDevOrgs).toBeLessThan(1); // Average positions should be fairly consistent expect(avgPositionStdDevEnvs).toBeLessThan(1); } ); redisTest( "should shuffle environments while maintaining age order within environments", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "fixed-seed", }); const now = Date.now(); // Setup three environments, each with two queues of different ages await Promise.all([ // env-1: one old queue (3000ms old) and one new queue (1000ms old) setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 3000, queueId: "queue-1-old", orgId: "org-1", envId: "env-1", }), setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 1000, queueId: "queue-1-new", orgId: "org-1", envId: "env-1", }), // env-2: same pattern setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 3000, queueId: "queue-2-old", orgId: "org-1", envId: "env-2", }), setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 1000, queueId: "queue-2-new", orgId: "org-1", envId: "env-2", }), ]); // Setup basic concurrency settings await setupConcurrency({ redis, keyProducer, env: { id: "env-1", currentConcurrency: 0, limit: 5 }, }); await setupConcurrency({ redis, keyProducer, env: { id: "env-2", currentConcurrency: 0, limit: 5 }, }); const envResult = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); const result = flattenResults(envResult); // Group queues by environment const queuesByEnv = result.reduce( (acc, queueId) => { const envId = keyProducer.envIdFromQueue(queueId); if (!acc[envId]) { acc[envId] = []; } acc[envId].push(queueId); return acc; }, {} as Record ); // Verify that: // 1. We got all queues expect(result).toHaveLength(4); // 2. Queues are grouped by environment for (const envQueues of Object.values(queuesByEnv)) { expect(envQueues).toHaveLength(2); // 3. Within each environment, older queue comes before newer queue const [firstQueue, secondQueue] = envQueues; expect(firstQueue).toContain("old"); expect(secondQueue).toContain("new"); } } ); redisTest( "should bias shuffling based on concurrency limits and available capacity", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const now = Date.now(); // Setup three environments with different concurrency settings const envSetups = [ { envId: "env-1", limit: 100, current: 20, // Lots of available capacity queueCount: 3, }, { envId: "env-2", limit: 50, current: 40, // Less available capacity queueCount: 3, }, { envId: "env-3", limit: 10, current: 5, // Some available capacity queueCount: 3, }, ]; // Setup queues and concurrency for each environment for (const setup of envSetups) { await setupConcurrency({ redis, keyProducer, env: { id: setup.envId, currentConcurrency: setup.current, limit: setup.limit, }, }); for (let i = 0; i < setup.queueCount; i++) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 1000 * (i + 1), queueId: `queue-${i}`, orgId: "org-1", envId: setup.envId, }); } } // Create multiple strategies with different seeds const numStrategies = 5; const strategies = Array.from( { length: numStrategies }, (_, i) => new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: `test-seed-${i}`, biases: { concurrencyLimitBias: 0.8, availableCapacityBias: 0.5, queueAgeRandomization: 0.0, }, }) ); // Run iterations across all strategies const iterationsPerStrategy = 100; const allResults: Record[] = []; for (const strategy of strategies) { const firstPositionCounts: Record = {}; for (let i = 0; i < iterationsPerStrategy; i++) { const envResult = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", `consumer-${i % 3}` ); const result = flattenResults(envResult); expect(result.length).toBeGreaterThan(0); const firstEnv = keyProducer.envIdFromQueue(result[0]); firstPositionCounts[firstEnv] = (firstPositionCounts[firstEnv] || 0) + 1; } allResults.push(firstPositionCounts); } // Calculate average distributions across all strategies const avgDistribution: Record = {}; const envIds = ["env-1", "env-2", "env-3"]; for (const envId of envIds) { const sum = allResults.reduce((acc, result) => acc + (result[envId] || 0), 0); avgDistribution[envId] = sum / numStrategies; } // Log individual strategy results and the average console.log("\nResults by strategy:"); allResults.forEach((result, i) => { console.log(`Strategy ${i + 1}:`, result); }); console.log("\nAverage distribution:", avgDistribution); // Calculate percentages from average distribution const totalCount = Object.values(avgDistribution).reduce((sum, count) => sum + count, 0); const highLimitPercentage = (avgDistribution["env-1"] / totalCount) * 100; const lowLimitPercentage = (avgDistribution["env-3"] / totalCount) * 100; console.log("\nPercentages:"); console.log("High limit percentage:", highLimitPercentage); console.log("Low limit percentage:", lowLimitPercentage); // Verify distribution across all strategies expect(highLimitPercentage).toBeLessThan(60); expect(lowLimitPercentage).toBeGreaterThan(10); expect(highLimitPercentage).toBeGreaterThan(lowLimitPercentage); } ); redisTest( "should respect ageInfluence parameter for queue ordering", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const now = Date.now(); // Setup queues with different ages in the same environment const queueAges = [ { id: "queue-1", age: 5000 }, // oldest { id: "queue-2", age: 3000 }, { id: "queue-3", age: 1000 }, // newest ]; // Helper function to run iterations with a specific age influence async function runWithQueueAgeRandomization(queueAgeRandomization: number) { const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "fixed-seed", biases: { concurrencyLimitBias: 0, availableCapacityBias: 0, queueAgeRandomization, }, }); const positionCounts: Record = { "queue-1": [0, 0, 0], "queue-2": [0, 0, 0], "queue-3": [0, 0, 0], }; const iterations = 1000; for (let i = 0; i < iterations; i++) { const envResult = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); const result = flattenResults(envResult); result.forEach((queueId, position) => { const baseQueueId = queueId.split(":").pop()!; positionCounts[baseQueueId][position]++; }); } return positionCounts; } // Setup test data for (const { id, age } of queueAges) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - age, queueId: id, orgId: "org-1", envId: "env-1", }); } await setupConcurrency({ redis, keyProducer, env: { id: "env-1", currentConcurrency: 0, limit: 5 }, }); // Test with different age influence values const strictAge = await runWithQueueAgeRandomization(0); // Strict age-based ordering const mixed = await runWithQueueAgeRandomization(0.5); // Mix of age and random const fullyRandom = await runWithQueueAgeRandomization(1); // Completely random console.log("Distribution with strict age ordering (0.0):", strictAge); console.log("Distribution with mixed ordering (0.5):", mixed); console.log("Distribution with random ordering (1.0):", fullyRandom); // With strict age ordering (0.0), oldest should always be first expect(strictAge["queue-1"][0]).toBe(1000); // Always in first position expect(strictAge["queue-3"][0]).toBe(0); // Never in first position // With fully random (1.0), positions should still allow for some age bias const randomFirstPositionSpread = Math.abs( fullyRandom["queue-1"][0] - fullyRandom["queue-3"][0] ); expect(randomFirstPositionSpread).toBeLessThan(200); // Allow for larger spread in distribution // With mixed (0.5), should show preference for age but not absolute expect(mixed["queue-1"][0]).toBeGreaterThan(mixed["queue-3"][0]); // Older preferred expect(mixed["queue-3"][0]).toBeGreaterThan(0); // But newer still gets chances } ); redisTest( "should respect maximumEnvCount and select envs based on queue ages", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-max-orgs", maximumEnvCount: 2, // Only select top 2 orgs }); const now = Date.now(); // Setup 4 envs with different queue age profiles const envSetups = [ { envId: "env-1", queues: [ { age: 1000 }, // Average age: 1000 ], }, { envId: "env-2", queues: [ { age: 5000 }, // Average age: 5000 { age: 5000 }, ], }, { envId: "env-3", queues: [ { age: 2000 }, // Average age: 2000 { age: 2000 }, ], }, { envId: "env-4", queues: [ { age: 500 }, // Average age: 500 { age: 500 }, ], }, ]; // Setup queues and concurrency for each org for (const setup of envSetups) { await setupConcurrency({ redis, keyProducer, env: { id: setup.envId, currentConcurrency: 0, limit: 5 }, }); for (let i = 0; i < setup.queues.length; i++) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - setup.queues[i].age, queueId: `queue-${setup.envId}-${i}`, orgId: `org-${setup.envId}`, envId: setup.envId, }); } } // Run multiple iterations to verify consistent behavior const iterations = 100; const selectedEnvCounts: Record = {}; for (let i = 0; i < iterations; i++) { const envResult = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", `consumer-${i}` ); const result = flattenResults(envResult); // Track which orgs were included in the result const selectedEnvs = new Set(result.map((queueId) => keyProducer.envIdFromQueue(queueId))); // Verify we never get more than maximumOrgCount orgs expect(selectedEnvs.size).toBeLessThanOrEqual(2); for (const envId of selectedEnvs) { selectedEnvCounts[envId] = (selectedEnvCounts[envId] || 0) + 1; } } console.log("Environment selection counts:", selectedEnvCounts); // org-2 should be selected most often (highest average age) expect(selectedEnvCounts["env-2"]).toBeGreaterThan(selectedEnvCounts["env-4"] || 0); // org-4 should be selected least often (lowest average age) const env4Count = selectedEnvCounts["env-4"] || 0; expect(env4Count).toBeLessThan(selectedEnvCounts["env-2"]); // Verify that envs with higher average queue age are selected more frequently const sortedEnvs = Object.entries(selectedEnvCounts).sort((a, b) => b[1] - a[1]); console.log("Sorted environment frequencies:", sortedEnvs); // The top 2 most frequently selected orgs should be env-2 and env-3 // as they have the highest average queue ages const topTwoEnvs = new Set([sortedEnvs[0][0], sortedEnvs[1][0]]); expect(topTwoEnvs).toContain("env-2"); // Highest average age expect(topTwoEnvs).toContain("env-3"); // Second highest average age // Calculate selection percentages const totalSelections = Object.values(selectedEnvCounts).reduce((a, b) => a + b, 0); const selectionPercentages = Object.entries(selectedEnvCounts).reduce( (acc, [orgId, count]) => { acc[orgId] = (count / totalSelections) * 100; return acc; }, {} as Record ); console.log("Environment selection percentages:", selectionPercentages); // Verify that env-2 (highest average age) gets selected in at least 40% of iterations expect(selectionPercentages["env-2"]).toBeGreaterThan(40); // Verify that env-4 (lowest average age) gets selected in less than 20% of iterations expect(selectionPercentages["env-4"] || 0).toBeLessThan(20); } ); redisTest( "should not overly bias picking environments when queue have priority offset ages", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-max-orgs", maximumEnvCount: 2, // Only select top 2 orgs }); const now = Date.now(); // Setup 4 envs with different queue age profiles const envSetups = [ { envId: "env-1", queues: [ { age: 1000 }, // Average age: 1000 ], }, { envId: "env-2", queues: [ { age: 5000 + MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET }, // Average age: 5000 + 1 year { age: 5000 + MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET }, ], }, { envId: "env-3", queues: [ { age: 2000 }, // Average age: 2000 { age: 2000 }, ], }, { envId: "env-4", queues: [ { age: 500 }, // Average age: 500 { age: 500 }, ], }, ]; // Setup queues and concurrency for each org for (const setup of envSetups) { await setupConcurrency({ redis, keyProducer, env: { id: setup.envId, currentConcurrency: 0, limit: 5 }, }); for (let i = 0; i < setup.queues.length; i++) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - setup.queues[i].age, queueId: `queue-${setup.envId}-${i}`, orgId: `org-${setup.envId}`, envId: setup.envId, }); } } // Run multiple iterations to verify consistent behavior const iterations = 100; const selectedEnvCounts: Record = {}; for (let i = 0; i < iterations; i++) { const envResult = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", `consumer-${i}` ); const result = flattenResults(envResult); // Track which orgs were included in the result const selectedEnvs = new Set(result.map((queueId) => keyProducer.envIdFromQueue(queueId))); // Verify we never get more than maximumOrgCount orgs expect(selectedEnvs.size).toBeLessThanOrEqual(2); for (const envId of selectedEnvs) { selectedEnvCounts[envId] = (selectedEnvCounts[envId] || 0) + 1; } } console.log("Environment selection counts:", selectedEnvCounts); // org-2 should be selected most often (highest average age) expect(selectedEnvCounts["env-2"]).toBeGreaterThan(selectedEnvCounts["env-4"] || 0); // org-4 should be selected least often (lowest average age) const env4Count = selectedEnvCounts["env-4"] || 0; expect(env4Count).toBeLessThan(selectedEnvCounts["env-2"]); // Verify that envs with higher average queue age are selected more frequently const sortedEnvs = Object.entries(selectedEnvCounts).sort((a, b) => b[1] - a[1]); console.log("Sorted environment frequencies:", sortedEnvs); // The top 2 most frequently selected orgs should be env-2 and env-3 // as they have the highest average queue ages const topTwoEnvs = new Set([sortedEnvs[0][0], sortedEnvs[1][0]]); expect(topTwoEnvs).toContain("env-2"); // Highest average age expect(topTwoEnvs).toContain("env-3"); // Second highest average age // Calculate selection percentages const totalSelections = Object.values(selectedEnvCounts).reduce((a, b) => a + b, 0); const selectionPercentages = Object.entries(selectedEnvCounts).reduce( (acc, [orgId, count]) => { acc[orgId] = (count / totalSelections) * 100; return acc; }, {} as Record ); console.log("Environment selection percentages:", selectionPercentages); // Verify that env-2 (highest average age) gets selected in at least 40% of iterations expect(selectionPercentages["env-2"]).toBeGreaterThan(40); // Verify that env-4 (lowest average age) gets selected in less than 20% of iterations expect(selectionPercentages["env-4"] || 0).toBeLessThan(20); } ); redisTest( "should respect maximumQueuePerEnvCount when distributing queues", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-max-queues", maximumQueuePerEnvCount: 2, // Only take 2 queues per env }); const now = Date.now(); // Setup two environments with different numbers of queues const envSetups = [ { envId: "env-1", queues: [ { age: 5000 }, // Oldest { age: 4000 }, { age: 3000 }, // This should be excluded due to maximumQueuePerEnvCount ], }, { envId: "env-2", queues: [ { age: 2000 }, { age: 1000 }, // Newest ], }, ]; // Setup queues and concurrency for each env for (const setup of envSetups) { await setupConcurrency({ redis, keyProducer, env: { id: setup.envId, currentConcurrency: 0, limit: 5 }, }); for (let i = 0; i < setup.queues.length; i++) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - setup.queues[i].age, queueId: `queue-${setup.envId}-${i}`, orgId: `org-${setup.envId}`, envId: setup.envId, }); } } const result = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); // Verify that each environment has at most 2 queues for (const envQueues of result) { expect(envQueues.queues.length).toBeLessThanOrEqual(2); } // Get queues for env-1 (which had 3 queues originally) const env1Queues = result.find((eq) => eq.envId === "env-1")?.queues ?? []; // Should have exactly 2 queues expect(env1Queues.length).toBe(2); // The queues should be the two oldest ones (queue-env-1-0 and queue-env-1-1) expect(env1Queues).toContain(keyProducer.queueKey("org-env-1", "env-1", "queue-env-1-0")); expect(env1Queues).toContain(keyProducer.queueKey("org-env-1", "env-1", "queue-env-1-1")); expect(env1Queues).not.toContain(keyProducer.queueKey("org-env-1", "env-1", "queue-env-1-2")); // Get queues for env-2 (which had 2 queues originally) const env2Queues = result.find((eq) => eq.envId === "env-2")?.queues ?? []; // Should still have both queues since it was within the limit expect(env2Queues.length).toBe(2); } ); redisTest( "should fairly distribute queues when using maximumQueuePerEnvCount over time", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-fair-distribution", maximumQueuePerEnvCount: 2, // Only take 2 queues at a time biases: { concurrencyLimitBias: 0, availableCapacityBias: 0, queueAgeRandomization: 0.3, // Add some randomization to allow newer queues a chance }, }); const now = Date.now(); // Setup one environment with 5 queues of different ages const queues = [ { age: 5000, id: "queue-0" }, // Oldest { age: 4000, id: "queue-1" }, { age: 3000, id: "queue-2" }, { age: 2000, id: "queue-3" }, { age: 1000, id: "queue-4" }, // Newest ]; // Setup the environment and its queues await setupConcurrency({ redis, keyProducer, env: { id: "env-1", currentConcurrency: 0, limit: 5 }, }); for (const queue of queues) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - queue.age, queueId: queue.id, orgId: "org-1", envId: "env-1", }); } // Run multiple iterations and track which queues are selected const iterations = 1000; const queueSelectionCounts: Record = {}; const queuePairings: Record = {}; for (let i = 0; i < iterations; i++) { const result = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", `consumer-${i}` ); // There should be exactly one environment expect(result.length).toBe(1); const selectedQueues = result[0].queues; // Should always get exactly 2 queues due to maximumQueuePerEnvCount expect(selectedQueues.length).toBe(2); // Track individual queue selections for (const queueId of selectedQueues) { const baseQueueId = queueId.split(":").pop()!; queueSelectionCounts[baseQueueId] = (queueSelectionCounts[baseQueueId] || 0) + 1; } // Track queue pairings to ensure variety const [first, second] = selectedQueues.map((qId) => qId.split(":").pop()!).sort(); const pairingKey = `${first}-${second}`; queuePairings[pairingKey] = (queuePairings[pairingKey] || 0) + 1; } console.log("\nQueue Selection Statistics:"); for (const [queueId, count] of Object.entries(queueSelectionCounts)) { const percentage = (count / (iterations * 2)) * 100; // Times 2 because we select 2 queues each time console.log(`${queueId}: ${percentage.toFixed(2)}% (${count} times)`); } console.log("\nQueue Pairing Statistics:"); for (const [pair, count] of Object.entries(queuePairings)) { const percentage = (count / iterations) * 100; console.log(`${pair}: ${percentage.toFixed(2)}% (${count} times)`); } // Verify that all queues were selected at least once for (const queue of queues) { expect(queueSelectionCounts[queue.id]).toBeGreaterThan(0); } // Calculate standard deviation of selection percentages const selectionPercentages = Object.values(queueSelectionCounts).map( (count) => (count / (iterations * 2)) * 100 ); const stdDev = calculateStandardDeviation(selectionPercentages); // The standard deviation should be reasonable given our age bias // Higher stdDev means more bias towards older queues // We expect some bias due to queueAgeRandomization being 0.3 expect(stdDev).toBeLessThan(15); // Allow for age-based bias but not extreme // Verify we get different pairings of queues const uniquePairings = Object.keys(queuePairings).length; // With 5 queues, we can have 10 possible unique pairs expect(uniquePairings).toBeGreaterThan(5); // Should see at least half of possible combinations } ); redisTest( "should handle maximumQueuePerEnvCount larger than available queues", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-max-larger", maximumQueuePerEnvCount: 5, // Larger than the number of queues we'll create }); const now = Date.now(); // Setup two environments with different numbers of queues const envSetups = [ { envId: "env-1", queues: [{ age: 5000 }, { age: 4000 }], }, { envId: "env-2", queues: [{ age: 3000 }], }, ]; // Setup queues and concurrency for each env for (const setup of envSetups) { await setupConcurrency({ redis, keyProducer, env: { id: setup.envId, currentConcurrency: 0, limit: 5 }, }); for (let i = 0; i < setup.queues.length; i++) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - setup.queues[i].age, queueId: `queue-${setup.envId}-${i}`, orgId: `org-${setup.envId}`, envId: setup.envId, }); } } const result = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); // Should get all queues from both environments const env1Queues = result.find((eq) => eq.envId === "env-1")?.queues ?? []; const env2Queues = result.find((eq) => eq.envId === "env-2")?.queues ?? []; // env-1 should have both its queues expect(env1Queues.length).toBe(2); // env-2 should have its single queue expect(env2Queues.length).toBe(1); } ); redisTest( "should handle empty environments with maximumQueuePerEnvCount", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-empty-env", maximumQueuePerEnvCount: 2, }); const now = Date.now(); // Setup two environments, one with queues, one without await setupConcurrency({ redis, keyProducer, env: { id: "env-1", currentConcurrency: 0, limit: 5 }, }); await setupConcurrency({ redis, keyProducer, env: { id: "env-2", currentConcurrency: 0, limit: 5 }, }); // Only add queues to env-1 await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 5000, queueId: "queue-1", orgId: "org-1", envId: "env-1", }); await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - 4000, queueId: "queue-2", orgId: "org-1", envId: "env-1", }); const result = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", "consumer-1" ); // Should only get one environment in the result expect(result.length).toBe(1); expect(result[0].envId).toBe("env-1"); expect(result[0].queues.length).toBe(2); } ); redisTest( "should respect maximumQueuePerEnvCount with priority offset queues", async ({ redisOptions }) => { const redis = createRedisClient(redisOptions); const keyProducer = createKeyProducer("test"); const strategy = new FairDequeuingStrategy({ tracer, redis, keys: keyProducer, defaultEnvConcurrency: 5, parentQueueLimit: 100, seed: "test-seed-priority", maximumQueuePerEnvCount: 2, biases: { concurrencyLimitBias: 0, availableCapacityBias: 0, queueAgeRandomization: 0.3, }, }); const now = Date.now(); // Setup queues with a mix of normal and priority offset ages const queues = [ { age: 5000, id: "queue-0" }, // Normal age { age: 4000 + MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET, id: "queue-1" }, // Priority { age: 3000, id: "queue-2" }, // Normal age { age: 2000 + MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET, id: "queue-3" }, // Priority { age: 1000, id: "queue-4" }, // Normal age ]; await setupConcurrency({ redis, keyProducer, env: { id: "env-1", currentConcurrency: 0, limit: 5 }, }); for (const queue of queues) { await setupQueue({ redis, keyProducer, parentQueue: "parent-queue", score: now - queue.age, queueId: queue.id, orgId: "org-1", envId: "env-1", }); } // Run multiple iterations to check distribution const iterations = 1000; const queueSelectionCounts: Record = {}; for (let i = 0; i < iterations; i++) { const result = await strategy.distributeFairQueuesFromParentQueue( "parent-queue", `consumer-${i}` ); const selectedQueues = result[0].queues; for (const queueId of selectedQueues) { const baseQueueId = queueId.split(":").pop()!; queueSelectionCounts[baseQueueId] = (queueSelectionCounts[baseQueueId] || 0) + 1; } } console.log("\nPriority Queue Selection Statistics:"); for (const [queueId, count] of Object.entries(queueSelectionCounts)) { const percentage = (count / (iterations * 2)) * 100; const isPriority = queues.find((q) => q.id === queueId)?.age! > MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET; console.log( `${queueId}${isPriority ? " (priority)" : ""}: ${percentage.toFixed(2)}% (${count} times)` ); } // Verify all queues get selected for (const queue of queues) { expect(queueSelectionCounts[queue.id]).toBeGreaterThan(0); } // Even with priority queues, we should still see a reasonable distribution const selectionPercentages = Object.values(queueSelectionCounts).map( (count) => (count / (iterations * 2)) * 100 ); const stdDev = calculateStandardDeviation(selectionPercentages); expect(stdDev).toBeLessThan(20); // Allow for slightly more variance due to priority queues } ); }); // Helper function to flatten results for counting function flattenResults(results: Array): string[] { return results.flatMap((envQueue) => envQueue.queues); }