Skip to content

Commit 88f3d9e

Browse files
authored
fix: replace denque with a local ring buffer queue (#4472)
* refactor: replace `denque` with an local ring buffer queue * chore: ensure all public methods available from `denque` to `MySQL2` exists * perf: take a non-negative fast path in peekAt * chore: ensure denque credits * ci: add a integration test
1 parent c9ed1ca commit 88f3d9e

7 files changed

Lines changed: 918 additions & 13 deletions

File tree

lib/base/connection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const Tls = require('tls');
2020
const Timers = require('timers');
2121
const EventEmitter = require('events').EventEmitter;
2222
const Readable = require('stream').Readable;
23-
const Queue = require('denque');
23+
const Queue = require('../ring_queue.js');
2424
const SqlString = require('sql-escaper');
2525
const { createLRU } = require('lru.min');
2626
const PacketParser = require('../packet_parser.js');

lib/base/pool.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const process = require('process');
44
const SqlString = require('sql-escaper');
55
const EventEmitter = require('events').EventEmitter;
66
const PoolConnection = require('../pool_connection.js');
7-
const Queue = require('denque');
7+
const Queue = require('../ring_queue.js');
88
const BaseConnection = require('./connection.js');
99
const Errors = require('../constants/errors.js');
1010
const {

lib/ring_queue.js

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
/**
2+
* Simplified abstraction adapted from denque (https://github.com/invertase/denque/tree/539105bb57854e997dd469221cdc52a0ad80e0a2)
3+
* License: Apache-2.0 (https://github.com/invertase/denque/blob/master/LICENSE)
4+
*/
5+
6+
'use strict';
7+
8+
const MIN_SHRINK_TAIL = 10000;
9+
10+
class RingQueue {
11+
constructor() {
12+
this._list = new Array(4);
13+
this._mask = 3;
14+
this._head = 0;
15+
this._tail = 0;
16+
}
17+
18+
get length() {
19+
return (this._tail - this._head) & this._mask;
20+
}
21+
22+
size() {
23+
return this.length;
24+
}
25+
26+
isEmpty() {
27+
return this._head === this._tail;
28+
}
29+
30+
push(item) {
31+
// biome-ignore lint/correctness/noUndeclaredVariables: arguments distinguishes push() from push(undefined)
32+
if (arguments.length === 0) {
33+
return this.length;
34+
}
35+
36+
this._list[this._tail] = item;
37+
this._tail = (this._tail + 1) & this._mask;
38+
39+
if (this._tail === this._head) {
40+
this._grow();
41+
}
42+
43+
return this.length;
44+
}
45+
46+
unshift(item) {
47+
// biome-ignore lint/correctness/noUndeclaredVariables: arguments distinguishes unshift() from unshift(undefined)
48+
if (arguments.length === 0) {
49+
return this.length;
50+
}
51+
52+
this._head = (this._head - 1) & this._mask;
53+
this._list[this._head] = item;
54+
55+
if (this._tail === this._head) {
56+
this._grow();
57+
}
58+
59+
return this.length;
60+
}
61+
62+
shift() {
63+
const head = this._head;
64+
65+
if (head === this._tail) {
66+
return undefined;
67+
}
68+
69+
const item = this._list[head];
70+
this._list[head] = undefined;
71+
this._head = (head + 1) & this._mask;
72+
73+
if (
74+
head < 2 &&
75+
this._tail > MIN_SHRINK_TAIL &&
76+
this._tail <= this._list.length >>> 2
77+
) {
78+
this._shrink();
79+
}
80+
81+
return item;
82+
}
83+
84+
pop() {
85+
const tail = this._tail;
86+
87+
if (tail === this._head) {
88+
return undefined;
89+
}
90+
91+
const capacity = this._list.length;
92+
this._tail = (tail - 1) & this._mask;
93+
94+
const item = this._list[this._tail];
95+
this._list[this._tail] = undefined;
96+
97+
if (this._head < 2 && tail > MIN_SHRINK_TAIL && tail <= capacity >>> 2) {
98+
this._shrink();
99+
}
100+
101+
return item;
102+
}
103+
104+
peekAt(index) {
105+
if (index !== (index | 0)) {
106+
return undefined;
107+
}
108+
109+
if (index >= 0) {
110+
if (index >= this.length) {
111+
return undefined;
112+
}
113+
114+
return this._list[(this._head + index) & this._mask];
115+
}
116+
117+
const size = this.length;
118+
119+
if (index < -size) {
120+
return undefined;
121+
}
122+
123+
return this._list[(this._head + index + size) & this._mask];
124+
}
125+
126+
get(index) {
127+
return this.peekAt(index);
128+
}
129+
130+
peek() {
131+
if (this._head === this._tail) {
132+
return undefined;
133+
}
134+
135+
return this._list[this._head];
136+
}
137+
138+
peekFront() {
139+
return this.peek();
140+
}
141+
142+
peekBack() {
143+
return this.peekAt(-1);
144+
}
145+
146+
removeOne(index) {
147+
if (index !== (index | 0)) {
148+
return undefined;
149+
}
150+
151+
const size = this.length;
152+
153+
if (index >= size || index < -size) {
154+
return undefined;
155+
}
156+
157+
if (index < 0) {
158+
index += size;
159+
}
160+
161+
const mask = this._mask;
162+
let slot = (this._head + index) & mask;
163+
const item = this._list[slot];
164+
const isCloserToHead = index < size / 2;
165+
166+
if (isCloserToHead) {
167+
for (let moves = index; moves > 0; moves--) {
168+
const previous = (slot - 1) & mask;
169+
this._list[slot] = this._list[previous];
170+
slot = previous;
171+
}
172+
173+
this._list[slot] = undefined;
174+
this._head = (this._head + 1) & mask;
175+
} else {
176+
for (let moves = size - 1 - index; moves > 0; moves--) {
177+
const next = (slot + 1) & mask;
178+
this._list[slot] = this._list[next];
179+
slot = next;
180+
}
181+
182+
this._list[slot] = undefined;
183+
this._tail = (this._tail - 1) & mask;
184+
}
185+
186+
return item;
187+
}
188+
189+
remove(index, count) {
190+
if (index !== (index | 0)) {
191+
return undefined;
192+
}
193+
194+
if (this._head === this._tail) {
195+
return undefined;
196+
}
197+
198+
const size = this.length;
199+
200+
if (index >= size || index < -size || count < 1) {
201+
return undefined;
202+
}
203+
204+
if (index < 0) {
205+
index += size;
206+
}
207+
208+
if (count === 1 || !count) {
209+
return [this.removeOne(index)];
210+
}
211+
212+
if (count !== (count | 0)) {
213+
return undefined;
214+
}
215+
216+
if (index + count > size) {
217+
count = size - index;
218+
}
219+
220+
const items = this.toArray();
221+
const removed = items.splice(index, count);
222+
this._rebuild(items);
223+
224+
return removed;
225+
}
226+
227+
splice(index, count, ...newItems) {
228+
if (index !== (index | 0)) {
229+
return undefined;
230+
}
231+
232+
const size = this.length;
233+
234+
if (index < 0) {
235+
index += size;
236+
}
237+
238+
if (index > size) {
239+
return undefined;
240+
}
241+
242+
if (newItems.length === 0) {
243+
return this.remove(index, count);
244+
}
245+
246+
if (index < 0) {
247+
return undefined;
248+
}
249+
250+
const removalCount = count === undefined ? 1 : count;
251+
252+
if (removalCount !== (removalCount | 0) || removalCount < 0) {
253+
return undefined;
254+
}
255+
256+
const items = this.toArray();
257+
let removed;
258+
259+
if (removalCount === 0) {
260+
removed = [];
261+
items.splice(index, 0, ...newItems);
262+
} else if (index >= size) {
263+
removed = undefined;
264+
items.splice(index, 0, ...newItems);
265+
} else {
266+
removed = items.splice(index, removalCount, ...newItems);
267+
}
268+
269+
this._rebuild(items);
270+
271+
return removed;
272+
}
273+
274+
clear() {
275+
this._list = new Array(this._list.length);
276+
this._head = 0;
277+
this._tail = 0;
278+
}
279+
280+
toArray() {
281+
const head = this._head;
282+
const tail = this._tail;
283+
284+
if (head <= tail) {
285+
return this._list.slice(head, tail);
286+
}
287+
288+
const capacity = this._list.length;
289+
const items = new Array(this.length);
290+
let count = 0;
291+
292+
for (let slot = head; slot < capacity; slot++) {
293+
items[count++] = this._list[slot];
294+
}
295+
296+
for (let slot = 0; slot < tail; slot++) {
297+
items[count++] = this._list[slot];
298+
}
299+
300+
return items;
301+
}
302+
303+
_grow() {
304+
const list = this._list;
305+
const capacity = list.length;
306+
307+
if (this._head === 0) {
308+
this._tail = capacity;
309+
list.length = capacity << 1;
310+
} else {
311+
const grown = new Array(capacity << 1);
312+
let count = 0;
313+
314+
for (let slot = this._head; slot < capacity; slot++) {
315+
grown[count++] = list[slot];
316+
}
317+
318+
for (let slot = 0; slot < this._tail; slot++) {
319+
grown[count++] = list[slot];
320+
}
321+
322+
this._list = grown;
323+
this._head = 0;
324+
this._tail = capacity;
325+
}
326+
327+
this._mask = (this._mask << 1) | 1;
328+
}
329+
330+
_shrink() {
331+
this._list.length >>>= 1;
332+
this._mask >>>= 1;
333+
}
334+
335+
_rebuild(items) {
336+
let capacity = this._list.length;
337+
338+
while (items.length >= capacity) {
339+
capacity <<= 1;
340+
}
341+
342+
this._list = new Array(capacity);
343+
this._mask = capacity - 1;
344+
this._head = 0;
345+
this._tail = items.length;
346+
347+
for (let i = 0; i < items.length; i++) {
348+
this._list[i] = items[i];
349+
}
350+
}
351+
}
352+
353+
module.exports = RingQueue;

package-lock.json

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)