Files
2026-07-13 13:32:57 +08:00

1479 lines
47 KiB
TypeScript

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<string, PositionStats> = {};
const orgStats: Record<string, PositionStats> = {};
// 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<string, string[]>
);
// 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<string, number>[] = [];
for (const strategy of strategies) {
const firstPositionCounts: Record<string, number> = {};
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<string, number> = {};
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<string, number[]> = {
"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<string, number> = {};
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<string, number>
);
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<string, number> = {};
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<string, number>
);
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<string, number> = {};
const queuePairings: Record<string, number> = {};
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<string, number> = {};
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<EnvQueues>): string[] {
return results.flatMap((envQueue) => envQueue.queues);
}