starvation-free-priority-queue
Version:
An in-memory priority queue that prevents starvation by balancing priority and arrival time. Items are greedily prioritized within each batch of the longest-waiting items, ensuring fairness alongside prioritization and bounded delays for low-priority task
141 lines • 7.53 kB
JavaScript
;
/**
* Copyright 2024 Ori Cohen https://github.com/ori88c
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const starvation_free_priority_queue_1 = require("./starvation-free-priority-queue");
describe('StarvationFreePriorityQueue tests', () => {
describe('Happy path tests', () => {
test('push all items in ascending order, then pop all items, ' +
'expecting a descending order of priorities per batch', async () => {
const maxDeferment = 48;
const batchSize = maxDeferment + 1;
const lastBatchSize = 25;
const fullBatchesCount = 34;
const itemsCount = (batchSize * fullBatchesCount) + lastBatchSize;
const priorityQueue = new starvation_free_priority_queue_1.StarvationFreePriorityQueue(maxDeferment, (task) => task.priority, itemsCount // Max capacity is known in advance.
);
let expectedSize = 0;
const validateSize = () => {
expect(priorityQueue.size).toBe(expectedSize);
expect(priorityQueue.isEmpty).toBe(expectedSize === 0);
};
// Push items in ascending order: 1,2,3,...,itemsCount.
for (let currPriority = 1; currPriority <= itemsCount; ++currPriority) {
priorityQueue.push({ priority: currPriority });
++expectedSize;
validateSize();
}
let expectedNextRemovedPriority;
const popAndValidate = () => {
const item = priorityQueue.pop();
expect(item.priority).toBe(expectedNextRemovedPriority);
--expectedNextRemovedPriority;
--expectedSize;
validateSize();
};
// Validate full batches.
for (let currBatch = 1; currBatch <= fullBatchesCount; ++currBatch) {
expectedNextRemovedPriority = currBatch * batchSize;
for (let ithPopFromBatch = 1; ithPopFromBatch <= batchSize; ++ithPopFromBatch) {
popAndValidate();
}
}
// Validate the last (partial) batch.
expectedNextRemovedPriority = itemsCount;
for (let ithPopFromBatch = 1; ithPopFromBatch <= lastBatchSize; ++ithPopFromBatch) {
popAndValidate();
}
expect(priorityQueue.isEmpty).toBe(true);
});
test('after processing a batch for removal (even without reaching Max Deferment), ' +
'newer higher-priority items are not removed until the batch is fully exhausted', async () => {
const maxDeferment = 451;
const popAfterKPushes = 306;
const maxPriorityInTest = 2000;
const priorityQueue = new starvation_free_priority_queue_1.StarvationFreePriorityQueue(maxDeferment, (priority) => priority);
let expectedSize = 0;
const validateSize = () => {
expect(priorityQueue.size).toBe(expectedSize);
expect(priorityQueue.isEmpty).toBe(expectedSize === 0);
};
// Push items in ascending order: 1,2,3,...,popAfterKPushes.
for (let currPriority = 1; currPriority <= popAfterKPushes; ++currPriority) {
priorityQueue.push(currPriority);
++expectedSize;
validateSize();
}
// The first pop operation initiates Frontier formation.
let expectedNextRemovedPriority = popAfterKPushes;
for (let ithPopFromFrontier = 1; ithPopFromFrontier <= popAfterKPushes; ++ithPopFromFrontier) {
const removedPriority = priorityQueue.pop();
expect(removedPriority).toBe(expectedNextRemovedPriority);
--expectedNextRemovedPriority;
// Add "noise" by pushing items before fully exhausting the current frontier.
// These new items are ignored until the current frontier is fully processed.
priorityQueue.push(maxPriorityInTest);
validateSize();
}
while (!priorityQueue.isEmpty) {
expect(priorityQueue.pop()).toBe(maxPriorityInTest);
--expectedSize;
validateSize();
}
});
test('the clear method should empty the priority queue by removing all items', async () => {
const irrelevantMaxDeferment = 293;
const itemsCount = 890;
const priorityQueue = new starvation_free_priority_queue_1.StarvationFreePriorityQueue(irrelevantMaxDeferment, (priority) => priority);
// Push items in ascending order: 1,2,3,...,itemsCount.
for (let currPriority = 1; currPriority <= itemsCount; ++currPriority) {
priorityQueue.push(currPriority);
}
expect(priorityQueue.size).toBe(itemsCount);
priorityQueue.clear();
expect(priorityQueue.size).toBe(0);
expect(priorityQueue.isEmpty).toBe(true);
expect(() => priorityQueue.pop()).toThrow();
});
});
describe('Negative path tests', () => {
test('constructor should throw an error if the maxDeferment is not a natural number', () => {
const getPriority = (priority) => priority;
const nonNaturalNumbers = [-74, -65, -5.67, -0.00001, 0, 0.1, 0.08974, 9.543, 1898.5, 4000.0000001];
for (const invalidMaxDeferment of nonNaturalNumbers) {
expect(() => new starvation_free_priority_queue_1.StarvationFreePriorityQueue(invalidMaxDeferment, getPriority)).toThrow();
}
});
test('constructor should throw an error if the estimatedMaxCapacity is provided ' +
'but is not a natural number', () => {
const maxDeferment = 50;
const getPriority = (priority) => priority;
const nonNaturalNumbers = [-74, -65, -5.67, -0.00001, 0, 0.1, 0.08974, 9.543, 1898.5, 4000.0000001];
for (const invalidMaxCapacity of nonNaturalNumbers) {
expect(() => new starvation_free_priority_queue_1.StarvationFreePriorityQueue(maxDeferment, getPriority, invalidMaxCapacity)).toThrow();
}
});
test('pop operation should throw an error when the priority queue is empty', () => {
const maxDeferment = 74;
const estimatedMaxCapacity = 250;
const getPriority = (priority) => priority;
const priorityQueue = new starvation_free_priority_queue_1.StarvationFreePriorityQueue(maxDeferment, getPriority, estimatedMaxCapacity);
const popAttemptsCount = 365;
for (let ithAttempt = 1; ithAttempt <= popAttemptsCount; ++ithAttempt) {
expect(() => priorityQueue.pop()).toThrow();
}
});
});
});
//# sourceMappingURL=starvation-free-priority-queue.test.js.map