UNPKG

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

214 lines (196 loc) 8.85 kB
/** * 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. */ import { SlimQueue } from 'data-oriented-slim-queue'; /** * A higher priority number indicates that the item should be prioritized in greedy settings, * meaning it would be returned **before** items with lower priority values. * * Note: The `StarvationFreePriorityQueue` does not follow a purely greedy approach. It may * override the raw priority attribute when the starvation limit (Maximum Deferment) is reached. */ export type PriorityGetter<T> = (item: Readonly<T>) => number; type ItemsComparer<T> = (item1: Readonly<T>, item2: Readonly<T>) => number; /** * StarvationFreePriorityQueue * * The `StarvationFreePriorityQueue` class implements an in-memory priority queue designed to mitigate * the issue of starvation—where low-priority items may never be processed due to the continuous arrival * of higher-priority items. It achieves this by balancing two key factors: * - **Priority**: Represented by a numeric value. * - **Arrival time**: Ensures fairness for items that have waited longer. * * This balanced approach makes the implementation ideal for task scheduling where fairness is critical, * though it may not be suitable for greedy algorithms such as Dijkstra's algorithm. * * ### Algorithm * The balance between priority and arrival time is achieved through the following data structures: * - **FIFO Queue**: Holds all pending items in their order of arrival. * - **Priority Frontier**: A sorted array representing the **next batch** of items ready for removal, * prioritized by their numeric priority value. * * **Push and Pop Operations**: * - **Pushing Items**: New items are added to the FIFO Queue. * - **Popping Items**: * - If the Priority Frontier contains items, the top (highest-priority) item is removed and returned, * prioritizing the priority attribute greedily within each batch. * - If the Priority Frontier is empty, up to `_maxDeferment + 1` items are pulled from the FIFO Queue, * sorted by priority, and moved to the Priority Frontier for removal. This ensures fairness by * accounting for the order of arrival. * * ### Starvation Coefficient - Maximum Deferment * Low-priority items can be delayed by **at most** `maxDeferment` pop attempts, as defined by the user. * This ensures fairness while maintaining the priority order, provided that `maxDeferment` is not exceeded. * * ### Complexity * - **Push**: O(1). * - **Pop**: * - **Amortized**: O(log(maxDeferment)). * - **Best case**: O(1). * - **Worst case**: O(maxDeferment * log(maxDeferment)) when a new frontier is populated. * * ### Example Use Cases * - Task scheduling where fairness is critical but priority remains significant. * - Resource allocation systems requiring bounded delays for low-priority tasks. */ export class StarvationFreePriorityQueue<T> { private readonly _maxDeferment: number; private readonly _ascByPriorityComparer: ItemsComparer<T>; private readonly _pendingItemsQueue: SlimQueue<T>; private readonly _priorityFrontier: T[] = []; // Frontier of items ready to be removed, sorted by ascending priority. /** * Constructor * * The `StarvationFreePriorityQueue` constructor provides precise control over the * Starvation Coefficient (Maximum Deferment), determining the maximum number of * 'pop' operations during which low-priority items can be delayed by higher-priority items. * * @param maxDeferment The maximum number of 'pop' operations a low-priority item * can be delayed by higher-priority items within the same batch. * @param getPriority A callback function to extract the priority of an item. * Larger numeric values indicate higher priority. * @param estimatedMaxCapacity An estimate of the queue's maximum capacity, representing * the expected backpressure of items waiting to be removed * by the 'pop' operation. This serves as an optimization * parameter, as higher estimates reduce the frequency of * dynamic memory allocations at runtime. */ constructor( maxDeferment: number, getPriority: PriorityGetter<T>, estimatedMaxCapacity?: number ) { if (!isNaturalNumber(maxDeferment)) { throw new Error( `Failed to instantiate StarvationFreePriorityQueue: maxDeferment must ` + `be a natural number, but received ${maxDeferment}` ); } if (estimatedMaxCapacity && !isNaturalNumber(estimatedMaxCapacity)) { throw new Error( `Failed to instantiate StarvationFreePriorityQueue: estimatedMaxCapacity must ` + `be a natural number, but received ${estimatedMaxCapacity}` ); } this._maxDeferment = maxDeferment; this._pendingItemsQueue = new SlimQueue<T>(estimatedMaxCapacity); this._ascByPriorityComparer = (item1: Readonly<T>, item2: Readonly<T>): number => getPriority(item1) - getPriority(item2); } /** * size * * @returns The number of items currently stored in the priority queue. */ public get size(): number { return this._pendingItemsQueue.size + this._priorityFrontier.length; } /** * isEmpty * * @returns True if and only if the queue does not contain any item. */ public get isEmpty(): boolean { return this._pendingItemsQueue.size === 0 && this._priorityFrontier.length === 0; } /** * push * * Appends an item to the end of the queue (i.e., the Last In), increasing its size by one. * * ### Complexity * O(1) under normal conditions. However, if the internal FIFO Queue capacity is exceeded, * a reallocation occurs, which can temporarily increase latency. This behavior may result * in latency spikes during the warm-up period. To mitigate this, consider setting the * optional `estimatedMaxCapacity` parameter in the constructor. * * @param item The item to add as the newest entry in the queue (i.e., the Last In). */ public push(item: T): void { this._pendingItemsQueue.push(item); } /** * pop * * Removes and returns the most prioritized item from the current batch, decreasing the queue's * size by one. * * ### Algorithm * - If the Priority Frontier is empty, up to `_maxDeferment + 1` items are pulled from the FIFO * Queue and moved to the Priority Frontier. Once shifted, the items in the Priority Frontier * are sorted by priority. * - The top (highest-priority) item is then removed from the Priority Frontier and returned. * * ### Complexity * - **Amortized**: O(log(maxDeferment)). * - **Best case**: O(1). * - **Worst case**: O(maxDeferment * log(maxDeferment)) when a new frontier is populated. * * @returns The item that was removed from the queue. * @throws Error if the queue is empty. */ public pop(): T { if (this._priorityFrontier.length > 0) { return this._priorityFrontier.pop(); } if (this._pendingItemsQueue.size === 0) { throw new Error('Cannot perform a pop operation on an empty StarvationFreePriorityQueue'); } // Populate the frontier, representing the current batch of prioritized items to be removed first. // Unlike a typical priority queue, this approach considers *both* the waiting time of items // and their raw priority attribute. This mechanism ensures *limited aging* of items in the queue. for (let movedCount = 0; movedCount <= this._maxDeferment && !this._pendingItemsQueue.isEmpty; ++movedCount) { const oldestPendingItem = this._pendingItemsQueue.pop(); this._priorityFrontier.push(oldestPendingItem); } this._priorityFrontier.sort(this._ascByPriorityComparer); return this._priorityFrontier.pop(); } /** * clear * * Removes all items from the queue, leaving it empty. */ public clear(): void { this._pendingItemsQueue.clear(); while (this._priorityFrontier.length > 0) { this._priorityFrontier.pop(); } } } function isNaturalNumber(num: number): boolean { const floored = Math.floor(num); return floored >= 1 && floored === num; }