teyyare 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -57
- package/bin/teyyare.js +36 -3
- package/package.json +6 -2
- package/src/core/protocol.js +39 -6
- package/src/index.js +119 -57
- package/src/receiver/pipeline.js +272 -57
- package/src/sender/pipeline.js +51 -9
- package/test/e2e/e2e.js +276 -0
- package/test/integration/pull.test.js +51 -0
- package/test/integration/resume.test.js +62 -0
- package/test/integration/single-file.test.js +85 -0
- package/test/unit/protocol.test.js +20 -0
package/src/index.js
CHANGED
|
@@ -45,32 +45,45 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
|
|
|
45
45
|
if (socket._writableState) socket._writableState.highWaterMark = 4 * 1024 * 1024;
|
|
46
46
|
|
|
47
47
|
const chunks = [];
|
|
48
|
+
let chunkHead = 0;
|
|
49
|
+
let chunkOffset = 0;
|
|
48
50
|
let bufferedLength = 0;
|
|
51
|
+
const headerScratch = Buffer.allocUnsafe(HEADER_SIZE);
|
|
49
52
|
|
|
50
53
|
socket.on('data', (chunk) => {
|
|
51
54
|
chunks.push(chunk);
|
|
52
55
|
bufferedLength += chunk.length;
|
|
53
56
|
|
|
54
57
|
while (bufferedLength >= HEADER_SIZE) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const head = chunks[chunkHead];
|
|
59
|
+
const headAvail = head.length - chunkOffset;
|
|
60
|
+
let header;
|
|
61
|
+
|
|
62
|
+
if (headAvail >= HEADER_SIZE) {
|
|
63
|
+
try {
|
|
64
|
+
header = readHeader(head, chunkOffset);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
socket.destroy(err);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
58
70
|
let off = 0;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
let cIdx = chunkHead;
|
|
72
|
+
let cOff = chunkOffset;
|
|
73
|
+
while (off < HEADER_SIZE && cIdx < chunks.length) {
|
|
74
|
+
const c = chunks[cIdx];
|
|
75
|
+
const toCopy = Math.min(c.length - cOff, HEADER_SIZE - off);
|
|
76
|
+
headerScratch.set(c.subarray(cOff, cOff + toCopy), off);
|
|
63
77
|
off += toCopy;
|
|
78
|
+
cIdx++;
|
|
79
|
+
cOff = 0;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
header = readHeader(headerScratch, 0);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
socket.destroy(err);
|
|
85
|
+
return;
|
|
64
86
|
}
|
|
65
|
-
headerBuf = tempHeader;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let header;
|
|
69
|
-
try {
|
|
70
|
-
header = readHeader(headerBuf, 0);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
socket.destroy(err);
|
|
73
|
-
return;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
const totalLength = HEADER_SIZE + header.payloadLength;
|
|
@@ -79,29 +92,35 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
|
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
let rawFrame;
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
chunks[
|
|
95
|
+
if (headAvail >= totalLength) {
|
|
96
|
+
// Fast-path: single chunk zero-copy slice
|
|
97
|
+
rawFrame = head.subarray(chunkOffset, chunkOffset + totalLength);
|
|
98
|
+
chunkOffset += totalLength;
|
|
99
|
+
if (chunkOffset === head.length) {
|
|
100
|
+
chunks[chunkHead] = null;
|
|
101
|
+
chunkHead++;
|
|
102
|
+
chunkOffset = 0;
|
|
88
103
|
}
|
|
89
104
|
bufferedLength -= totalLength;
|
|
90
105
|
} else {
|
|
106
|
+
// Multi-chunk frame assembly
|
|
91
107
|
rawFrame = Buffer.allocUnsafe(totalLength);
|
|
92
108
|
let dstOff = 0;
|
|
93
109
|
let remaining = totalLength;
|
|
94
110
|
while (remaining > 0) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
dstOff
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
const curr = chunks[chunkHead];
|
|
112
|
+
const avail = curr.length - chunkOffset;
|
|
113
|
+
if (avail <= remaining) {
|
|
114
|
+
rawFrame.set(curr.subarray(chunkOffset), dstOff);
|
|
115
|
+
dstOff += avail;
|
|
116
|
+
remaining -= avail;
|
|
117
|
+
chunks[chunkHead] = null;
|
|
118
|
+
chunkHead++;
|
|
119
|
+
chunkOffset = 0;
|
|
101
120
|
} else {
|
|
102
|
-
rawFrame.set(
|
|
121
|
+
rawFrame.set(curr.subarray(chunkOffset, chunkOffset + remaining), dstOff);
|
|
103
122
|
dstOff += remaining;
|
|
104
|
-
|
|
123
|
+
chunkOffset += remaining;
|
|
105
124
|
remaining = 0;
|
|
106
125
|
}
|
|
107
126
|
}
|
|
@@ -126,6 +145,16 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
|
|
|
126
145
|
console.error(`[TCPTransport ${this.id}] Error decoding frame:`, err.message);
|
|
127
146
|
}
|
|
128
147
|
}
|
|
148
|
+
|
|
149
|
+
// Amortized array compaction to prevent unbounded array growth without O(N) shift()
|
|
150
|
+
if (bufferedLength === 0) {
|
|
151
|
+
chunks.length = 0;
|
|
152
|
+
chunkHead = 0;
|
|
153
|
+
chunkOffset = 0;
|
|
154
|
+
} else if (chunkHead >= 128 && chunkHead > (chunks.length >> 1)) {
|
|
155
|
+
chunks.splice(0, chunkHead);
|
|
156
|
+
chunkHead = 0;
|
|
157
|
+
}
|
|
129
158
|
});
|
|
130
159
|
|
|
131
160
|
socket.on('close', () => {
|
|
@@ -219,18 +248,19 @@ TCPTransport.prototype.sendv = function(peer, buffers) {
|
|
|
219
248
|
|
|
220
249
|
const pool = this._peerSocketPool ? this._peerSocketPool.get(peer) : null;
|
|
221
250
|
if (pool && pool.length > 0) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
let idx = this._channelIndex.get(peer) || 0;
|
|
252
|
+
const poolLen = pool.length;
|
|
253
|
+
for (let attempts = 0; attempts < poolLen; attempts++) {
|
|
254
|
+
const s = pool[(idx + attempts) % poolLen];
|
|
255
|
+
if (!s.destroyed && s.writable) {
|
|
256
|
+
this._channelIndex.set(peer, idx + attempts + 1);
|
|
257
|
+
s.cork();
|
|
258
|
+
for (let i = 0; i < buffers.length; i++) {
|
|
259
|
+
s.write(buffers[i]);
|
|
260
|
+
}
|
|
261
|
+
s.uncork();
|
|
262
|
+
return;
|
|
231
263
|
}
|
|
232
|
-
socket.uncork();
|
|
233
|
-
return;
|
|
234
264
|
}
|
|
235
265
|
}
|
|
236
266
|
|
|
@@ -252,13 +282,15 @@ TCPTransport.prototype.send = function(peer, buffer) {
|
|
|
252
282
|
|
|
253
283
|
const pool = this._peerSocketPool ? this._peerSocketPool.get(peer) : null;
|
|
254
284
|
if (pool && pool.length > 0) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
285
|
+
let idx = this._channelIndex.get(peer) || 0;
|
|
286
|
+
const poolLen = pool.length;
|
|
287
|
+
for (let attempts = 0; attempts < poolLen; attempts++) {
|
|
288
|
+
const s = pool[(idx + attempts) % poolLen];
|
|
289
|
+
if (!s.destroyed && s.writable) {
|
|
290
|
+
this._channelIndex.set(peer, idx + attempts + 1);
|
|
291
|
+
s.write(buffer);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
262
294
|
}
|
|
263
295
|
}
|
|
264
296
|
|
|
@@ -450,8 +482,8 @@ export async function serve({
|
|
|
450
482
|
transport,
|
|
451
483
|
localNode: 2,
|
|
452
484
|
remoteNode: 1,
|
|
453
|
-
maxInflightBatches:
|
|
454
|
-
maxInflightBytes:
|
|
485
|
+
maxInflightBatches: 32,
|
|
486
|
+
maxInflightBytes: 128 * 1024 * 1024
|
|
455
487
|
});
|
|
456
488
|
|
|
457
489
|
const receiver = new ReceiverPipeline({
|
|
@@ -478,7 +510,13 @@ export async function serve({
|
|
|
478
510
|
const [metaBuf, payloadBuf] = msg.buffers;
|
|
479
511
|
const entries = unpackGeneration(metaBuf, payloadBuf);
|
|
480
512
|
if (entries.length > 0 && entries[0].type === Opcodes.PULL_REQUEST) {
|
|
481
|
-
const {
|
|
513
|
+
const {
|
|
514
|
+
remotePath,
|
|
515
|
+
verifiedChunks = [],
|
|
516
|
+
chunkSize,
|
|
517
|
+
maxEntries,
|
|
518
|
+
maxBytes
|
|
519
|
+
} = unpackPullRequest(entries[0].key);
|
|
482
520
|
let resolved = path.isAbsolute(remotePath) ? remotePath : path.resolve(destinationDir, remotePath);
|
|
483
521
|
if (!existsSync(resolved)) {
|
|
484
522
|
const alt = path.resolve(destinationDir, remotePath);
|
|
@@ -513,9 +551,18 @@ export async function serve({
|
|
|
513
551
|
}));
|
|
514
552
|
}
|
|
515
553
|
|
|
554
|
+
const senderOpts = {
|
|
555
|
+
maxImmutableBuffers: 32,
|
|
556
|
+
maxConcurrentFlushes: 32
|
|
557
|
+
};
|
|
558
|
+
if (chunkSize) senderOpts.chunkSize = chunkSize;
|
|
559
|
+
if (maxEntries) senderOpts.maxEntries = maxEntries;
|
|
560
|
+
if (maxBytes) senderOpts.maxBytes = maxBytes;
|
|
561
|
+
|
|
516
562
|
const sender = new SenderPipeline({
|
|
517
563
|
replication,
|
|
518
564
|
manifest,
|
|
565
|
+
options: senderOpts,
|
|
519
566
|
knownResumeState,
|
|
520
567
|
metrics
|
|
521
568
|
});
|
|
@@ -604,8 +651,8 @@ export async function send({
|
|
|
604
651
|
transport,
|
|
605
652
|
localNode: 1,
|
|
606
653
|
remoteNode: 2,
|
|
607
|
-
maxInflightBatches: pipelineOptions.maxInflightBatches ||
|
|
608
|
-
maxInflightBytes: pipelineOptions.maxInflightBytes ||
|
|
654
|
+
maxInflightBatches: pipelineOptions.maxInflightBatches || 32,
|
|
655
|
+
maxInflightBytes: pipelineOptions.maxInflightBytes || 128 * 1024 * 1024
|
|
609
656
|
});
|
|
610
657
|
|
|
611
658
|
const manifest = await TransferManifest.build(sourcePath);
|
|
@@ -651,6 +698,15 @@ export async function pull({
|
|
|
651
698
|
pipelineOptions = {},
|
|
652
699
|
showProgress = true
|
|
653
700
|
}) {
|
|
701
|
+
const isRemote = host !== '127.0.0.1' && host !== 'localhost';
|
|
702
|
+
const defaultChunkSize = isRemote ? 2 * 1024 * 1024 : 1024 * 1024;
|
|
703
|
+
const defaultMaxEntries = 8;
|
|
704
|
+
const defaultMaxBytes = defaultChunkSize * defaultMaxEntries;
|
|
705
|
+
|
|
706
|
+
const effectiveChunkSize = pipelineOptions.chunkSize || defaultChunkSize;
|
|
707
|
+
const effectiveMaxEntries = pipelineOptions.maxEntries || defaultMaxEntries;
|
|
708
|
+
const effectiveMaxBytes = pipelineOptions.maxBytes || defaultMaxBytes;
|
|
709
|
+
|
|
654
710
|
const metrics = new MetricsCollector();
|
|
655
711
|
const progress = showProgress ? new ProgressReporter({ role: 'receiver' }) : null;
|
|
656
712
|
if (progress) {
|
|
@@ -675,8 +731,8 @@ export async function pull({
|
|
|
675
731
|
transport,
|
|
676
732
|
localNode: 1,
|
|
677
733
|
remoteNode: 2,
|
|
678
|
-
maxInflightBatches: pipelineOptions.maxInflightBatches ||
|
|
679
|
-
maxInflightBytes: pipelineOptions.maxInflightBytes ||
|
|
734
|
+
maxInflightBatches: pipelineOptions.maxInflightBatches || 32,
|
|
735
|
+
maxInflightBytes: pipelineOptions.maxInflightBytes || 128 * 1024 * 1024
|
|
680
736
|
});
|
|
681
737
|
|
|
682
738
|
const receiver = new ReceiverPipeline({
|
|
@@ -699,11 +755,17 @@ export async function pull({
|
|
|
699
755
|
}
|
|
700
756
|
} catch {}
|
|
701
757
|
|
|
702
|
-
// Submit PULL_REQUEST batch with verified chunks
|
|
758
|
+
// Submit PULL_REQUEST batch with verified chunks and WAN tuning
|
|
703
759
|
await submitSingleMutation(
|
|
704
760
|
replication,
|
|
705
761
|
Opcodes.PULL_REQUEST,
|
|
706
|
-
packPullRequest({
|
|
762
|
+
packPullRequest({
|
|
763
|
+
remotePath: sourceRemotePath,
|
|
764
|
+
verifiedChunks,
|
|
765
|
+
chunkSize: effectiveChunkSize,
|
|
766
|
+
maxEntries: effectiveMaxEntries,
|
|
767
|
+
maxBytes: effectiveMaxBytes
|
|
768
|
+
})
|
|
707
769
|
);
|
|
708
770
|
|
|
709
771
|
// Wait for receiver pipeline to receive and write all files
|