@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
163 lines (137 loc) • 4.26 kB
text/typescript
import {LinkedList} from "../../../util/array.js";
import {DropType, GossipQueue, LinearGossipQueueOpts, QueueType} from "./types.js";
// Having a drop ratio of 1 will empty the queue which is too severe
// Worse case drop 95% of the queue
const MAX_DROP_RATIO = 0.95;
/**
* Default gossip queue for all topics except for beacon_attestation
* Support LIFO and FIFO type.
*/
export class LinearGossipQueue<T> implements GossipQueue<T> {
private readonly list = new LinkedList<T>();
// Increase _dropRatio gradually, retest its initial value if node is in good status
private _dropRatio = 0;
// this is to avoid the case we drop 90% of the queue, then queue is empty and we consider
// node is in good status
private recentDrop = false;
// set recentDrop to false after we process up to maxLength items
private processedCountSinceDrop = 0;
constructor(private readonly opts: LinearGossipQueueOpts) {
if (opts.dropOpts.type === DropType.ratio) {
const {start, step} = opts.dropOpts;
if (start <= 0 || start > 1) {
throw Error(`Invalid drop ratio start ${start} step ${step}`);
}
this._dropRatio = opts.dropOpts.start;
}
}
get length(): number {
return this.list.length;
}
get keySize(): number {
// this implementation doesn't support indexing
return 1;
}
// not implemented for this gossip queue
getDataAgeMs(): number[] {
return [];
}
get dropRatio(): number {
return this._dropRatio;
}
clear(): void {
this.list.clear();
}
/**
* Add item to gossip queue.
* Return number of items dropped
*/
add(item: T): number {
// this signals the node is not overloaded anymore
if (this.opts.dropOpts.type === DropType.ratio && !this.recentDrop && this.length === 0) {
// reset drop ratio to see if node is comfortable with it
this._dropRatio = this.opts.dropOpts.start;
}
this.list.push(item);
if (this.list.length <= this.opts.maxLength) {
return 0;
}
// overload, need to drop more items
if (this.opts.dropOpts.type === DropType.count) {
return this.dropByCount(this.opts.dropOpts.count);
}
this.recentDrop = true;
const droppedCount = this.dropByRatio(this._dropRatio);
// increase drop ratio the next time queue is full
this._dropRatio = Math.min(MAX_DROP_RATIO, this._dropRatio + this.opts.dropOpts.step);
return droppedCount;
}
next(): T | null {
let item: T | null = null;
// LIFO -> pop() remove last item, FIFO -> shift() remove first item
switch (this.opts.type) {
case QueueType.LIFO:
item = this.list.pop();
break;
case QueueType.FIFO:
item = this.list.shift();
break;
}
// it's ok to mark recent drop as false if we dropped <50% of the queue the last time
if (this.opts.dropOpts.type === DropType.ratio && this.recentDrop && item !== null) {
this.processedCountSinceDrop++;
if (this.processedCountSinceDrop >= this.opts.maxLength) {
this.recentDrop = false;
this.processedCountSinceDrop = 0;
}
}
return item;
}
getAll(): T[] {
return this.list.toArray();
}
/**
* Drop up to some ratio of items from the queue
* ratio is from 0 to 1 inclusive
* Return number of items dropped
*/
private dropByRatio(ratio: number): number {
if (ratio < 0 || ratio > 1) {
throw Error(`Invalid ratio ${ratio}`);
}
if (ratio === 0) {
return 0;
}
if (ratio === 1) {
const numDeleted = this.length;
this.clear();
return numDeleted;
}
const count = Math.floor(this.list.length * ratio);
return this.dropByCount(count);
}
/**
* Drop up to some number of items from the queue
* Return number of items dropped
*/
private dropByCount(count: number): number {
if (count <= 0) {
return 0;
}
if (count >= this.length) {
const numDeleted = this.length;
this.clear();
return numDeleted;
}
let i = 0;
while (i < count && this.length > 0) {
if (this.opts.type === QueueType.LIFO) {
this.list.shift();
} else {
this.list.pop();
}
i++;
}
return i;
}
}