teyyare 0.2.0 → 0.2.2
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/bin/teyyare.js +1 -1
- package/package.json +1 -1
- package/src/cli/progress.js +72 -28
- package/src/core/generation.js +107 -0
- package/src/index.js +20 -23
- package/src/receiver/pipeline.js +69 -10
- package/src/sender/pipeline.js +16 -8
package/bin/teyyare.js
CHANGED
|
@@ -136,7 +136,7 @@ Protocol overhead:${snap.protocolOverheadPercent.toFixed(2)}%
|
|
|
136
136
|
Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
137
137
|
`);
|
|
138
138
|
} else if (command === 'status') {
|
|
139
|
-
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.
|
|
139
|
+
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.2 status: ready');
|
|
140
140
|
} else {
|
|
141
141
|
console.error(`Unknown command: ${command}`);
|
|
142
142
|
printUsage();
|
package/package.json
CHANGED
package/src/cli/progress.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Throttled terminal progress renderer.
|
|
3
|
+
* Intelligent, accurate metrics for both Upload (send) and Download (pull) modes.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
export class ProgressReporter {
|
|
@@ -7,10 +8,12 @@ export class ProgressReporter {
|
|
|
7
8
|
* @param {object} [options]
|
|
8
9
|
* @param {number} [options.intervalMs=150]
|
|
9
10
|
* @param {boolean} [options.enabled=true]
|
|
11
|
+
* @param {'sender'|'receiver'} [options.role='sender']
|
|
10
12
|
*/
|
|
11
|
-
constructor({ intervalMs = 150, enabled = true } = {}) {
|
|
13
|
+
constructor({ intervalMs = 150, enabled = true, role = 'sender' } = {}) {
|
|
12
14
|
this.intervalMs = intervalMs;
|
|
13
15
|
this.enabled = enabled && Boolean(process.stdout.isTTY || process.env.CI !== 'true');
|
|
16
|
+
this.role = role;
|
|
14
17
|
|
|
15
18
|
this.fileName = '';
|
|
16
19
|
this.bytesDone = 0;
|
|
@@ -23,16 +26,20 @@ export class ProgressReporter {
|
|
|
23
26
|
this.muttafaQueue = 0;
|
|
24
27
|
this.raptiyeRTT = 0;
|
|
25
28
|
this.tcpPressure = 0;
|
|
29
|
+
this.resumeHits = 0;
|
|
30
|
+
this.diskSpeedMBs = 0;
|
|
26
31
|
|
|
27
32
|
this.startTime = Date.now();
|
|
28
33
|
this.lastRender = 0;
|
|
29
34
|
this._timer = null;
|
|
30
35
|
this._linesRendered = 0;
|
|
36
|
+
this._speedHistory = [];
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
start() {
|
|
34
40
|
if (!this.enabled) return;
|
|
35
41
|
this.startTime = Date.now();
|
|
42
|
+
this._speedHistory = [{ time: this.startTime, bytes: this.bytesDone }];
|
|
36
43
|
this._timer = setInterval(() => this.render(), this.intervalMs);
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -52,13 +59,15 @@ export class ProgressReporter {
|
|
|
52
59
|
if (state.bytesDone !== undefined) this.bytesDone = state.bytesDone;
|
|
53
60
|
if (state.totalBytes !== undefined) this.totalBytes = state.totalBytes;
|
|
54
61
|
if (state.chunksDone !== undefined) this.chunksDone = state.chunksDone;
|
|
55
|
-
if (state.totalChunks !== undefined) this.totalChunks = state.totalChunks;
|
|
62
|
+
if (state.totalChunks !== undefined && state.totalChunks > 0) this.totalChunks = state.totalChunks;
|
|
56
63
|
if (state.generationId !== undefined) this.generationId = state.generationId;
|
|
57
64
|
if (state.inflightGen !== undefined) this.inflightGen = state.inflightGen;
|
|
58
65
|
if (state.inflightBytes !== undefined) this.inflightBytes = state.inflightBytes;
|
|
59
66
|
if (state.muttafaQueue !== undefined) this.muttafaQueue = state.muttafaQueue;
|
|
60
67
|
if (state.raptiyeRTT !== undefined) this.raptiyeRTT = state.raptiyeRTT;
|
|
61
68
|
if (state.tcpPressure !== undefined) this.tcpPressure = state.tcpPressure;
|
|
69
|
+
if (state.resumeHits !== undefined) this.resumeHits = state.resumeHits;
|
|
70
|
+
if (state.diskSpeedMBs !== undefined) this.diskSpeedMBs = state.diskSpeedMBs;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
formatBytes(bytes) {
|
|
@@ -77,39 +86,75 @@ export class ProgressReporter {
|
|
|
77
86
|
render() {
|
|
78
87
|
if (!this.enabled) return;
|
|
79
88
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const elapsedSec = Math.max(0.001, (now - this.startTime) / 1000);
|
|
91
|
+
|
|
92
|
+
// Track sliding window (last 2 seconds) for smooth, responsive current speed
|
|
93
|
+
this._speedHistory.push({ time: now, bytes: this.bytesDone });
|
|
94
|
+
while (this._speedHistory.length > 2 && (now - this._speedHistory[0].time) > 2000) {
|
|
95
|
+
this._speedHistory.shift();
|
|
96
|
+
}
|
|
97
|
+
const oldest = this._speedHistory[0];
|
|
98
|
+
const windowSec = Math.max(0.001, (now - oldest.time) / 1000);
|
|
99
|
+
const windowBytes = Math.max(0, this.bytesDone - oldest.bytes);
|
|
100
|
+
|
|
101
|
+
const avgSpeedMBs = (this.bytesDone / (1024 * 1024)) / elapsedSec;
|
|
102
|
+
const currentSpeedMBs = windowSec >= 0.4 ? (windowBytes / (1024 * 1024)) / windowSec : avgSpeedMBs;
|
|
103
|
+
const effectiveSpeed = currentSpeedMBs > 0.05 ? currentSpeedMBs : avgSpeedMBs;
|
|
104
|
+
|
|
82
105
|
const percent = this.totalBytes > 0 ? ((this.bytesDone / this.totalBytes) * 100).toFixed(1) : '0.0';
|
|
83
106
|
|
|
84
107
|
let etaStr = '--:--';
|
|
85
|
-
if (
|
|
108
|
+
if (effectiveSpeed > 0 && this.totalBytes > this.bytesDone) {
|
|
86
109
|
const remainingBytes = this.totalBytes - this.bytesDone;
|
|
87
|
-
const remainingSec = Math.round(remainingBytes / (
|
|
110
|
+
const remainingSec = Math.round(remainingBytes / (effectiveSpeed * 1024 * 1024));
|
|
88
111
|
const mm = String(Math.floor(remainingSec / 60)).padStart(2, '0');
|
|
89
112
|
const ss = String(remainingSec % 60).padStart(2, '0');
|
|
90
113
|
etaStr = `${mm}:${ss}`;
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
let lines;
|
|
117
|
+
if (this.role === 'receiver') {
|
|
118
|
+
const diskStr = this.diskSpeedMBs > 0 ? `${this.diskSpeedMBs.toFixed(1)} MB/s` : `${effectiveSpeed.toFixed(1)} MB/s`;
|
|
119
|
+
lines = [
|
|
120
|
+
'\x1b[1m\x1b[36mTeyyare [Download]\x1b[0m',
|
|
121
|
+
'',
|
|
122
|
+
`\x1b[1m${this.fileName || 'Transferring...'}\x1b[0m`,
|
|
123
|
+
'',
|
|
124
|
+
`${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
|
|
125
|
+
`\x1b[32m${percent}%\x1b[0m`,
|
|
126
|
+
'',
|
|
127
|
+
`network speed \x1b[1m${effectiveSpeed.toFixed(1)} MB/s\x1b[0m`,
|
|
128
|
+
`disk speed \x1b[1m${diskStr}\x1b[0m`,
|
|
129
|
+
`ETA ${etaStr}`,
|
|
130
|
+
'',
|
|
131
|
+
`chunks ${this.chunksDone} / ${this.totalChunks || this.chunksDone}`,
|
|
132
|
+
`generation ${this.generationId}`,
|
|
133
|
+
`verified CRC32 ${this.chunksDone}`,
|
|
134
|
+
`resume hits ${this.resumeHits}`
|
|
135
|
+
];
|
|
136
|
+
} else {
|
|
137
|
+
lines = [
|
|
138
|
+
'\x1b[1m\x1b[36mTeyyare [Upload]\x1b[0m',
|
|
139
|
+
'',
|
|
140
|
+
`\x1b[1m${this.fileName || 'Transferring...'}\x1b[0m`,
|
|
141
|
+
'',
|
|
142
|
+
`${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
|
|
143
|
+
`\x1b[32m${percent}%\x1b[0m`,
|
|
144
|
+
'',
|
|
145
|
+
`throughput \x1b[1m${effectiveSpeed.toFixed(1)} MB/s\x1b[0m`,
|
|
146
|
+
`ETA ${etaStr}`,
|
|
147
|
+
'',
|
|
148
|
+
`chunks ${this.chunksDone} / ${this.totalChunks || this.chunksDone}`,
|
|
149
|
+
`generation ${this.generationId}`,
|
|
150
|
+
`inflight gen ${this.inflightGen}`,
|
|
151
|
+
`inflight bytes ${this.formatBytes(this.inflightBytes)}`,
|
|
152
|
+
'',
|
|
153
|
+
`muttafa queue ${this.muttafaQueue}`,
|
|
154
|
+
`raptiye RTT ${this.raptiyeRTT} ms`,
|
|
155
|
+
`TCP pressure ${this.tcpPressure}`
|
|
156
|
+
];
|
|
157
|
+
}
|
|
113
158
|
|
|
114
159
|
if (process.stdout.isTTY) {
|
|
115
160
|
if (this._linesRendered > 0) {
|
|
@@ -118,8 +163,7 @@ export class ProgressReporter {
|
|
|
118
163
|
process.stdout.write(lines.join('\n') + '\n');
|
|
119
164
|
this._linesRendered = lines.length;
|
|
120
165
|
} else {
|
|
121
|
-
|
|
122
|
-
process.stdout.write(`[Teyyare] ${percent}% | ${throughputMBs.toFixed(1)} MB/s | Chunks: ${this.chunksDone}/${this.totalChunks} | ETA: ${etaStr}\n`);
|
|
166
|
+
process.stdout.write(`[Teyyare] ${percent}% | ${effectiveSpeed.toFixed(1)} MB/s | Chunks: ${this.chunksDone}/${this.totalChunks || this.chunksDone} | ETA: ${etaStr}\n`);
|
|
123
167
|
}
|
|
124
168
|
}
|
|
125
169
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-copy generation framing and buffer extraction utilities.
|
|
3
|
+
* Ensures complete compatibility across all versions of Muttafa and Raptiye.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract scatter/gather buffers [metaBuf, payloadBuf] from any Muttafa generation.
|
|
8
|
+
* @param {object} batch
|
|
9
|
+
* @returns {Uint8Array[]}
|
|
10
|
+
*/
|
|
11
|
+
export function extractGenerationBuffers(batch) {
|
|
12
|
+
if (typeof batch.buffers === 'function') {
|
|
13
|
+
return batch.buffers();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const store = batch.store || batch;
|
|
17
|
+
if (typeof store.buffers === 'function') {
|
|
18
|
+
return store.buffers();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Direct extraction from ArenaStore
|
|
22
|
+
const rawBuf = store._allocator?.rawBuffer;
|
|
23
|
+
const payload = rawBuf ? rawBuf.subarray(0, store._allocator.usedBytes) : new Uint8Array(0);
|
|
24
|
+
const metaBuf = new Uint8Array(store.length * 25);
|
|
25
|
+
const view = new DataView(metaBuf.buffer, metaBuf.byteOffset, metaBuf.byteLength);
|
|
26
|
+
|
|
27
|
+
let off = 0;
|
|
28
|
+
const chunks = store._chunks || [];
|
|
29
|
+
for (let c = 0; c < chunks.length; c++) {
|
|
30
|
+
const chunk = chunks[c];
|
|
31
|
+
for (let i = 0; i < chunk.count; i++) {
|
|
32
|
+
view.setFloat64(off, chunk.seqs[i], true);
|
|
33
|
+
view.setUint8(off + 8, chunk.types[i]);
|
|
34
|
+
view.setUint32(off + 9, chunk.keyOffsets[i], true);
|
|
35
|
+
view.setUint32(off + 13, chunk.keyLengths[i], true);
|
|
36
|
+
view.setUint32(off + 17, chunk.valOffsets[i], true);
|
|
37
|
+
view.setUint32(off + 21, chunk.valLengths[i], true);
|
|
38
|
+
off += 25;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return [metaBuf, payload];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fast zero-copy generator / unpacker for received generation buffers.
|
|
47
|
+
* @param {Uint8Array} metaBuf
|
|
48
|
+
* @param {Uint8Array} payloadBuf
|
|
49
|
+
* @returns {Array<{ seq: number, type: number, key: Uint8Array, val: Uint8Array }>}
|
|
50
|
+
*/
|
|
51
|
+
export function unpackGeneration(metaBuf, payloadBuf) {
|
|
52
|
+
const entryCount = (metaBuf.byteLength / 25) | 0;
|
|
53
|
+
const view = new DataView(metaBuf.buffer, metaBuf.byteOffset, metaBuf.byteLength);
|
|
54
|
+
const entries = new Array(entryCount);
|
|
55
|
+
|
|
56
|
+
let off = 0;
|
|
57
|
+
for (let i = 0; i < entryCount; i++) {
|
|
58
|
+
const seq = view.getFloat64(off, true);
|
|
59
|
+
const type = view.getUint8(off + 8);
|
|
60
|
+
const keyOffset = view.getUint32(off + 9, true);
|
|
61
|
+
const keyLength = view.getUint32(off + 13, true);
|
|
62
|
+
const valOffset = view.getUint32(off + 17, true);
|
|
63
|
+
const valLength = view.getUint32(off + 21, true);
|
|
64
|
+
off += 25;
|
|
65
|
+
|
|
66
|
+
entries[i] = {
|
|
67
|
+
seq,
|
|
68
|
+
type,
|
|
69
|
+
key: payloadBuf.subarray(keyOffset, keyOffset + keyLength),
|
|
70
|
+
val: payloadBuf.subarray(valOffset, valOffset + valLength)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return entries;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Pack a single-mutation batch directly into scatter/gather wire buffers.
|
|
79
|
+
* @param {number} generationId
|
|
80
|
+
* @param {number} opcode
|
|
81
|
+
* @param {Uint8Array} key
|
|
82
|
+
* @param {Uint8Array} [val]
|
|
83
|
+
* @returns {object}
|
|
84
|
+
*/
|
|
85
|
+
export function packSingleMutationBatch(generationId, opcode, key, val = new Uint8Array(0)) {
|
|
86
|
+
const metaBuf = new Uint8Array(25);
|
|
87
|
+
const view = new DataView(metaBuf.buffer, metaBuf.byteOffset, 25);
|
|
88
|
+
view.setFloat64(0, 1, true); // seq
|
|
89
|
+
view.setUint8(8, opcode);
|
|
90
|
+
view.setUint32(9, 0, true); // keyOffset
|
|
91
|
+
view.setUint32(13, key.byteLength, true); // keyLength
|
|
92
|
+
view.setUint32(17, key.byteLength, true); // valOffset
|
|
93
|
+
view.setUint32(21, val.byteLength, true); // valLength
|
|
94
|
+
|
|
95
|
+
const payloadBuf = new Uint8Array(key.byteLength + val.byteLength);
|
|
96
|
+
payloadBuf.set(key, 0);
|
|
97
|
+
payloadBuf.set(val, key.byteLength);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
generationId,
|
|
101
|
+
firstSequence: 1,
|
|
102
|
+
lastSequence: 1,
|
|
103
|
+
entryCount: 1,
|
|
104
|
+
byteLength: metaBuf.byteLength + payloadBuf.byteLength,
|
|
105
|
+
buffers: [metaBuf, payloadBuf]
|
|
106
|
+
};
|
|
107
|
+
}
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
|
-
import { MutableBuffer, ArenaStore, BumpAllocator, FrozenArenaStore } from 'muttafa';
|
|
8
7
|
import { TCPTransport, ReplicationPipeline } from 'raptiye';
|
|
9
8
|
import { SenderPipeline } from './sender/pipeline.js';
|
|
10
9
|
import { ReceiverPipeline } from './receiver/pipeline.js';
|
|
@@ -12,6 +11,7 @@ import { TransferManifest } from './manifest/manifest.js';
|
|
|
12
11
|
import { FileResumeState } from './resume/state.js';
|
|
13
12
|
import { MetricsCollector } from './metrics/collector.js';
|
|
14
13
|
import { ProgressReporter } from './cli/progress.js';
|
|
14
|
+
import { packSingleMutationBatch, unpackGeneration } from './core/generation.js';
|
|
15
15
|
import {
|
|
16
16
|
Opcodes,
|
|
17
17
|
packChunkMeta,
|
|
@@ -39,22 +39,9 @@ export {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
async function submitSingleMutation(replication, opcode, key, value = new Uint8Array(0)) {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
mutable.append(1, opcode, key, value);
|
|
45
|
-
const frozen = mutable.freeze();
|
|
46
|
-
frozen.markFlushing();
|
|
47
|
-
const receipt = await replication.submitBatch({
|
|
48
|
-
generationId: frozen.generation,
|
|
49
|
-
firstSequence: frozen.firstSequence,
|
|
50
|
-
lastSequence: frozen.lastSequence,
|
|
51
|
-
entryCount: frozen.entryCount,
|
|
52
|
-
byteLength: frozen.byteLength,
|
|
53
|
-
buffers: frozen.buffers()
|
|
54
|
-
});
|
|
42
|
+
const batch = packSingleMutationBatch(Date.now() & 0x7fffffff, opcode, key, value);
|
|
43
|
+
const receipt = await replication.submitBatch(batch);
|
|
55
44
|
await receipt.delivered();
|
|
56
|
-
frozen.markFlushed();
|
|
57
|
-
frozen.release();
|
|
58
45
|
}
|
|
59
46
|
|
|
60
47
|
/**
|
|
@@ -102,9 +89,9 @@ export async function serve({
|
|
|
102
89
|
replication.onBatch(async (msg) => {
|
|
103
90
|
if (!msg.buffers || msg.buffers.length < 2) return;
|
|
104
91
|
const [metaBuf, payloadBuf] = msg.buffers;
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
const { remotePath } = unpackPullRequest(
|
|
92
|
+
const entries = unpackGeneration(metaBuf, payloadBuf);
|
|
93
|
+
if (entries.length > 0 && entries[0].type === Opcodes.PULL_REQUEST) {
|
|
94
|
+
const { remotePath } = unpackPullRequest(entries[0].key);
|
|
108
95
|
let resolved = path.isAbsolute(remotePath) ? remotePath : path.resolve(destinationDir, remotePath);
|
|
109
96
|
if (!existsSync(resolved)) {
|
|
110
97
|
const alt = path.resolve(destinationDir, remotePath);
|
|
@@ -126,11 +113,21 @@ export async function serve({
|
|
|
126
113
|
manifest,
|
|
127
114
|
metrics
|
|
128
115
|
});
|
|
129
|
-
|
|
116
|
+
setImmediate(async () => {
|
|
117
|
+
try {
|
|
118
|
+
await sender.transfer();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
await submitSingleMutation(
|
|
121
|
+
replication,
|
|
122
|
+
Opcodes.PULL_ERROR,
|
|
123
|
+
packPullError({ errorCode: 500, message: err.message })
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
130
127
|
return;
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
await receiver.
|
|
130
|
+
await receiver.enqueueBatch(msg);
|
|
134
131
|
});
|
|
135
132
|
|
|
136
133
|
return {
|
|
@@ -165,7 +162,7 @@ export async function send({
|
|
|
165
162
|
knownResumeState = new Map()
|
|
166
163
|
}) {
|
|
167
164
|
const metrics = new MetricsCollector();
|
|
168
|
-
const progress = showProgress ? new ProgressReporter() : null;
|
|
165
|
+
const progress = showProgress ? new ProgressReporter({ role: 'sender' }) : null;
|
|
169
166
|
|
|
170
167
|
// Ephemeral sender port
|
|
171
168
|
const senderPort = 10000 + Math.floor(Math.random() * 20000);
|
|
@@ -227,7 +224,7 @@ export async function pull({
|
|
|
227
224
|
showProgress = true
|
|
228
225
|
}) {
|
|
229
226
|
const metrics = new MetricsCollector();
|
|
230
|
-
const progress = showProgress ? new ProgressReporter() : null;
|
|
227
|
+
const progress = showProgress ? new ProgressReporter({ role: 'receiver' }) : null;
|
|
231
228
|
if (progress) progress.start();
|
|
232
229
|
|
|
233
230
|
const clientPort = 10000 + Math.floor(Math.random() * 20000);
|
package/src/receiver/pipeline.js
CHANGED
|
@@ -8,9 +8,9 @@ import fs from 'node:fs/promises';
|
|
|
8
8
|
import { existsSync, createReadStream } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
|
-
import { FrozenArenaStore } from 'muttafa';
|
|
12
11
|
import { crc32 } from 'raptiye';
|
|
13
12
|
import { Opcodes, unpackChunkMeta, unpackFileBegin, unpackTransferBegin, unpackFileEnd, unpackPullError } from '../core/protocol.js';
|
|
13
|
+
import { unpackGeneration } from '../core/generation.js';
|
|
14
14
|
import { FileResumeState } from '../resume/state.js';
|
|
15
15
|
|
|
16
16
|
export class ReceiverPipeline {
|
|
@@ -42,14 +42,58 @@ export class ReceiverPipeline {
|
|
|
42
42
|
this._finishResolvers = [];
|
|
43
43
|
this._errorRejecters = [];
|
|
44
44
|
|
|
45
|
+
// Processing queue to ensure strictly ordered, serialized generation processing
|
|
46
|
+
this._batchQueue = [];
|
|
47
|
+
this._processingQueue = false;
|
|
48
|
+
|
|
45
49
|
// Hook up Raptiye batch handler if autoHookBatch is true
|
|
46
50
|
if (this.replication && autoHookBatch) {
|
|
47
51
|
this.replication.onBatch(async (msg) => {
|
|
48
|
-
|
|
52
|
+
return this.enqueueBatch(msg);
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Enqueue incoming generation batch to guarantee strict FIFO sequential processing.
|
|
59
|
+
* @param {object} msg
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
enqueueBatch(msg) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
this._batchQueue.push({ msg, resolve, reject });
|
|
65
|
+
this._drainQueue();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _drainQueue() {
|
|
70
|
+
if (this._processingQueue) return;
|
|
71
|
+
this._processingQueue = true;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
while (this._batchQueue.length > 0) {
|
|
75
|
+
const item = this._batchQueue.shift();
|
|
76
|
+
try {
|
|
77
|
+
await this._processGenerationBatch(item.msg);
|
|
78
|
+
item.resolve();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.transferError = err;
|
|
81
|
+
for (const reject of this._errorRejecters) {
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
this._finishResolvers = [];
|
|
85
|
+
this._errorRejecters = [];
|
|
86
|
+
item.reject(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
this._processingQueue = false;
|
|
91
|
+
if (this._batchQueue.length > 0) {
|
|
92
|
+
this._drainQueue();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
async waitForCompletion() {
|
|
54
98
|
if (this.transferError) throw this.transferError;
|
|
55
99
|
if (this.transferFinished) return;
|
|
@@ -70,14 +114,18 @@ export class ReceiverPipeline {
|
|
|
70
114
|
this.metrics.generationBytes += (metaBuf.byteLength + payloadBuf.byteLength);
|
|
71
115
|
}
|
|
72
116
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
if (this.progress) {
|
|
118
|
+
this.progress.update({
|
|
119
|
+
generationId: msg.generationId || (this.metrics ? this.metrics.generations : 0)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Zero-copy unpacking of generation entries
|
|
124
|
+
const entries = unpackGeneration(metaBuf, payloadBuf);
|
|
125
|
+
const count = entries.length;
|
|
76
126
|
|
|
77
127
|
for (let i = 0; i < count; i++) {
|
|
78
|
-
const type =
|
|
79
|
-
const key = store.getKey(i);
|
|
80
|
-
const val = store.getValue(i);
|
|
128
|
+
const { type, key, val } = entries[i];
|
|
81
129
|
|
|
82
130
|
if (this.metrics) {
|
|
83
131
|
this.metrics.mutations++;
|
|
@@ -166,9 +214,12 @@ export class ReceiverPipeline {
|
|
|
166
214
|
});
|
|
167
215
|
|
|
168
216
|
if (this.progress) {
|
|
217
|
+
const fileSize = Number(info.fileSize);
|
|
218
|
+
const estimatedChunks = Math.max(1, Math.ceil(fileSize / (1024 * 1024)));
|
|
169
219
|
this.progress.update({
|
|
170
220
|
fileName: info.path,
|
|
171
|
-
totalBytes:
|
|
221
|
+
totalBytes: fileSize,
|
|
222
|
+
totalChunks: estimatedChunks
|
|
172
223
|
});
|
|
173
224
|
}
|
|
174
225
|
}
|
|
@@ -218,10 +269,18 @@ export class ReceiverPipeline {
|
|
|
218
269
|
}
|
|
219
270
|
|
|
220
271
|
if (this.progress) {
|
|
272
|
+
const diskTimeSec = Math.max(0.001, (this.metrics ? this.metrics.diskWriteTimeMs : 1) / 1000);
|
|
273
|
+
const writtenMB = (this.metrics ? this.metrics.fileBytesWritten : 0) / (1024 * 1024);
|
|
274
|
+
const diskMBs = Number((writtenMB / diskTimeSec).toFixed(1));
|
|
275
|
+
file.maxChunkSize = Math.max(file.maxChunkSize || 0, meta.length);
|
|
276
|
+
const realTotalChunks = file.maxChunkSize > 0 && file.fileSize > 0 ? Math.ceil(file.fileSize / file.maxChunkSize) : 0;
|
|
221
277
|
this.progress.update({
|
|
222
278
|
fileName: file.relativePath,
|
|
223
279
|
bytesDone: this.metrics ? this.metrics.fileBytesWritten : 0,
|
|
224
|
-
chunksDone: this.metrics ? this.metrics.chunksVerified : 0
|
|
280
|
+
chunksDone: this.metrics ? this.metrics.chunksVerified : 0,
|
|
281
|
+
totalChunks: realTotalChunks > 0 ? realTotalChunks : undefined,
|
|
282
|
+
resumeHits: this.metrics ? this.metrics.resumeHits : 0,
|
|
283
|
+
diskSpeedMBs: diskMBs
|
|
225
284
|
});
|
|
226
285
|
}
|
|
227
286
|
|
package/src/sender/pipeline.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
packTransferBegin,
|
|
18
18
|
packTransferEnd
|
|
19
19
|
} from '../core/protocol.js';
|
|
20
|
+
import { extractGenerationBuffers } from '../core/generation.js';
|
|
21
|
+
import { FileResumeState } from '../resume/state.js';
|
|
20
22
|
|
|
21
23
|
class RaptiyeSink extends Sink {
|
|
22
24
|
/**
|
|
@@ -36,20 +38,23 @@ class RaptiyeSink extends Sink {
|
|
|
36
38
|
* @param {import('muttafa').ImmutableBuffer} batch
|
|
37
39
|
*/
|
|
38
40
|
async flush(batch) {
|
|
39
|
-
const buffers = batch
|
|
41
|
+
const buffers = extractGenerationBuffers(batch);
|
|
40
42
|
const t0 = Date.now();
|
|
43
|
+
const generationId = batch.generationId || batch.generation || 1;
|
|
44
|
+
const entryCount = batch.entryCount || batch.store?.length || 1;
|
|
45
|
+
const byteLength = batch.byteLength || (buffers[0].byteLength + buffers[1].byteLength);
|
|
41
46
|
|
|
42
47
|
if (this.metrics) {
|
|
43
48
|
this.metrics.generations++;
|
|
44
|
-
this.metrics.generationBytes +=
|
|
49
|
+
this.metrics.generationBytes += byteLength;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
const receipt = await this.replication.submitBatch({
|
|
48
|
-
generationId
|
|
49
|
-
firstSequence: batch.firstSequence,
|
|
50
|
-
lastSequence: batch.lastSequence,
|
|
51
|
-
entryCount
|
|
52
|
-
byteLength
|
|
53
|
+
generationId,
|
|
54
|
+
firstSequence: batch.firstSequence || 1,
|
|
55
|
+
lastSequence: batch.lastSequence || 1,
|
|
56
|
+
entryCount,
|
|
57
|
+
byteLength,
|
|
53
58
|
buffers
|
|
54
59
|
});
|
|
55
60
|
|
|
@@ -99,7 +104,8 @@ export class SenderPipeline {
|
|
|
99
104
|
this.chunkSize = options.chunkSize || 1024 * 1024;
|
|
100
105
|
this.maxEntries = options.maxEntries || 8;
|
|
101
106
|
this.maxBytes = options.maxBytes || 8 * 1024 * 1024;
|
|
102
|
-
this.maxImmutableBuffers = options.maxImmutableBuffers ||
|
|
107
|
+
this.maxImmutableBuffers = options.maxImmutableBuffers || 8;
|
|
108
|
+
this.maxConcurrentFlushes = options.maxConcurrentFlushes || 4;
|
|
103
109
|
this.metrics = metrics;
|
|
104
110
|
this.progress = progress;
|
|
105
111
|
this.knownResumeState = knownResumeState;
|
|
@@ -120,6 +126,8 @@ export class SenderPipeline {
|
|
|
120
126
|
maxEntries: this.maxEntries,
|
|
121
127
|
maxBytes: this.maxBytes,
|
|
122
128
|
maxImmutableBuffers: this.maxImmutableBuffers,
|
|
129
|
+
maxConcurrentFlushes: this.maxConcurrentFlushes,
|
|
130
|
+
ordered: true,
|
|
123
131
|
backpressurePolicy: BackpressurePolicy.WAIT,
|
|
124
132
|
onBackpressure: (active) => {
|
|
125
133
|
if (this.metrics && active) {
|