UNPKG

ipfs-bitswap

Version:

JavaScript implementation of the Bitswap data exchange protocol used by IPFS

353 lines (300 loc) 9.27 kB
import { SortedMap } from '../utils/sorted-map.js' import type { Task, TaskMerger } from './index.js' import type { PeerId } from '@libp2p/interface' export interface PopTaskResult { peerId?: PeerId tasks: Task[] pendingSize: number } export interface PendingTask { created: number task: Task } /** * The task merger that is used by default. * Assumes that new tasks do not add any information over existing tasks, * and doesn't try to merge. */ const DefaultTaskMerger: TaskMerger = { hasNewInfo () { return false }, merge () {} } /** * Queue of requests to be processed by the engine. * The requests from each peer are added to the peer's queue, sorted by * priority. * Tasks are popped in priority order from the best peer - see popTasks() * for more details. */ export class RequestQueue { private readonly _taskMerger: TaskMerger public _byPeer: SortedMap<string, PeerTasks> constructor (taskMerger: TaskMerger = DefaultTaskMerger) { this._taskMerger = taskMerger this._byPeer = new SortedMap([], PeerTasks.compare) } /** * Push tasks onto the queue for the given peer */ pushTasks (peerId: PeerId, tasks: Task[]): void { let peerTasks = this._byPeer.get(peerId.toString()) if (peerTasks == null) { peerTasks = new PeerTasks(peerId, this._taskMerger) } peerTasks.pushTasks(tasks) this._byPeer.set(peerId.toString(), peerTasks) } /** * Choose the peer with the least active work (or if all have the same active * work, the most pending tasks) and pop off the highest priority tasks until * the total size is at least targetMinBytes. * This puts the popped tasks into the "active" state, meaning they are * actively being processed (and cannot be modified). */ popTasks (targetMinBytes: number): PopTaskResult { // Get the queue of tasks for the best peer and pop off tasks up to // targetMinBytes const peerTasks = this._head() if (peerTasks === undefined) { return { tasks: [], pendingSize: 0 } } const { tasks, pendingSize } = peerTasks.popTasks(targetMinBytes) if (tasks.length === 0) { return { tasks, pendingSize } } const peerId = peerTasks.peerId if (peerTasks.isIdle()) { // If there are no more tasks for the peer, free up its memory this._byPeer.delete(peerId.toString()) } else { // If there are still tasks remaining, update the sort order of peerTasks // (because it depends on the number of pending tasks) this._byPeer.update(0) } return { peerId, tasks, pendingSize } } _head (): PeerTasks | undefined { // Shortcut if (this._byPeer.size === 0) { return undefined } // eslint-disable-next-line no-unreachable-loop for (const [, v] of this._byPeer) { return v } return undefined } /** * Remove the task with the given topic for the given peer. */ remove (topic: string, peerId: PeerId): void { const peerTasks = this._byPeer.get(peerId.toString()) peerTasks?.remove(topic) } /** * Called when the tasks for the given peer complete. */ tasksDone (peerId: PeerId, tasks: Task[]): void { const peerTasks = this._byPeer.get(peerId.toString()) if (peerTasks == null) { return } const i = this._byPeer.indexOf(peerId.toString()) for (const task of tasks) { peerTasks.taskDone(task) } // Marking the tasks as done takes them out of the "active" state, and the // sort order depends on the size of the active tasks, so we need to update // the order. this._byPeer.update(i) } } /** * Queue of tasks for a particular peer, sorted by priority. */ class PeerTasks { public peerId: PeerId private readonly _taskMerger: TaskMerger private _activeTotalSize: number private readonly _pending: PendingTasks private readonly _active: Set<Task> constructor (peerId: PeerId, taskMerger: TaskMerger) { this.peerId = peerId this._taskMerger = taskMerger this._activeTotalSize = 0 this._pending = new PendingTasks() this._active = new Set() } /** * Push tasks onto the queue */ pushTasks (tasks: Task[]): void { for (const t of tasks) { this._pushTask(t) } } _pushTask (task: Task): void { // If the new task doesn't add any more information over what we // already have in the active queue, then we can skip the new task if (!this._taskHasMoreInfoThanActiveTasks(task)) { return } // If there is already a non-active (pending) task with this topic const existingTask = this._pending.get(task.topic) if (existingTask != null) { // If the new task has a higher priority than the old task, if (task.priority > existingTask.priority) { // Update the priority and the task's position in the queue this._pending.updatePriority(task.topic, task.priority) } // Merge the information from the new task into the existing task this._taskMerger.merge(task, existingTask) // A task with the topic exists, so we don't need to add // the new task to the queue return } // Push the new task onto the queue this._pending.add(task) } /** * Indicates whether the new task adds any more information over tasks that are * already in the active task queue */ _taskHasMoreInfoThanActiveTasks (task: Task): boolean { const tasksWithTopic = [] for (const activeTask of this._active) { if (activeTask.topic === task.topic) { tasksWithTopic.push(activeTask) } } // No tasks with that topic, so the new task adds information if (tasksWithTopic.length === 0) { return true } return this._taskMerger.hasNewInfo(task, tasksWithTopic) } /** * Pop tasks off the queue such that the total size is at least targetMinBytes */ popTasks (targetMinBytes: number): PopTaskResult { let size = 0 const tasks = [] // Keep popping tasks until we get up to targetMinBytes (or one item over // targetMinBytes) const pendingTasks = this._pending.tasks() for (let i = 0; i < pendingTasks.length && size < targetMinBytes; i++) { const task = pendingTasks[i] tasks.push(task) size += task.size // Move tasks from pending to active this._pending.delete(task.topic) this._activeTotalSize += task.size this._active.add(task) } return { tasks, pendingSize: this._pending.totalSize } } /** * Called when a task completes. * Note: must be the same reference as returned from popTasks. */ taskDone (task: Task): void { if (this._active.has(task)) { this._activeTotalSize -= task.size this._active.delete(task) } } /** * Remove pending tasks with the given topic */ remove (topic: string): void { this._pending.delete(topic) } /** * No work to be done, this PeerTasks object can be freed. */ isIdle (): boolean { return this._pending.length === 0 && this._active.size === 0 } /** * Compare PeerTasks */ static compare <Key> (a: [Key, PeerTasks], b: [Key, PeerTasks]): number { // Move peers with no pending tasks to the back of the queue if (a[1]._pending.length === 0) { return 1 } if (b[1]._pending.length === 0) { return -1 } // If the amount of active work is the same if (a[1]._activeTotalSize === b[1]._activeTotalSize) { // Choose the peer with the most pending work return b[1]._pending.length - a[1]._pending.length } // Choose the peer with the least amount of active work ("keep peers busy") return a[1]._activeTotalSize - b[1]._activeTotalSize } } /** * Queue of pending tasks for a particular peer, sorted by priority. */ class PendingTasks { private readonly _tasks: SortedMap<string, PendingTask> constructor () { this._tasks = new SortedMap([], this._compare) } get length (): number { return this._tasks.size } /** * Sum of the size of all pending tasks **/ get totalSize (): number { return [...this._tasks.values()].reduce((a, t) => a + t.task.size, 0) } get (topic: string): Task | undefined { return this._tasks?.get(topic)?.task } add (task: Task): void { this._tasks.set(task.topic, { created: Date.now(), task }) } delete (topic: string): void { this._tasks.delete(topic) } // All pending tasks, in priority order tasks (): Task[] { return [...this._tasks.values()].map(i => i.task) } /** * Update the priority of the task with the given topic, and update the order **/ updatePriority (topic: string, priority: number): void { const obj = this._tasks.get(topic) if (obj == null) { return } const i = this._tasks.indexOf(topic) obj.task.priority = priority this._tasks.update(i) } /** * Sort by priority desc then FIFO */ _compare (a: [string, PendingTask], b: [string, PendingTask]): number { if (a[1].task.priority === b[1].task.priority) { // FIFO return a[1].created - b[1].created } // Priority high -> low return b[1].task.priority - a[1].task.priority } }