@wowoengine/sawitdb 2.4.0 → 2.5.0
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 +16 -11
- package/bin/sawit-server.js +8 -1
- package/cli/benchmark.js +145 -0
- package/cli/local.js +83 -20
- package/cli/remote.js +50 -11
- package/cli/test.js +165 -0
- package/docs/index.html +580 -325
- package/package.json +1 -1
- package/src/SawitServer.js +27 -12
- package/src/WowoEngine.js +619 -129
- package/src/modules/BTreeIndex.js +64 -23
- package/src/modules/Pager.js +212 -6
- package/src/modules/QueryParser.js +93 -50
- package/src/modules/WAL.js +340 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Write-Ahead Logging (WAL) for SawitDB - OPTIMIZED VERSION
|
|
6
|
+
* Redis-level performance with crash safety
|
|
7
|
+
*/
|
|
8
|
+
class WAL {
|
|
9
|
+
constructor(dbPath, options = {}) {
|
|
10
|
+
this.dbPath = dbPath;
|
|
11
|
+
this.walPath = `${dbPath}.wal`;
|
|
12
|
+
this.enabled = options.enabled !== false;
|
|
13
|
+
this.syncMode = options.syncMode || 'normal';
|
|
14
|
+
this.checkpointInterval = options.checkpointInterval || 10000; // Increased from 1000
|
|
15
|
+
|
|
16
|
+
// OPTIMIZATION: Larger buffers and async fsync
|
|
17
|
+
this.groupCommitMs = options.groupCommitMs || 10; // Group commits within 10ms
|
|
18
|
+
this.bufferSize = options.bufferSize || 1024 * 1024; // 1MB buffer
|
|
19
|
+
|
|
20
|
+
this.fd = null;
|
|
21
|
+
this.lsn = 0;
|
|
22
|
+
this.operationCount = 0;
|
|
23
|
+
this.pendingOps = [];
|
|
24
|
+
this.writeBuffer = Buffer.allocUnsafe(this.bufferSize);
|
|
25
|
+
this.bufferOffset = 0;
|
|
26
|
+
this.syncTimer = null;
|
|
27
|
+
this.lastSyncTime = Date.now();
|
|
28
|
+
|
|
29
|
+
if (this.enabled) {
|
|
30
|
+
this._init();
|
|
31
|
+
this._startSyncTimer();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_init() {
|
|
36
|
+
// 1. Synchronous phase for Startup/Recovery check
|
|
37
|
+
if (fs.existsSync(this.walPath)) {
|
|
38
|
+
const fd = fs.openSync(this.walPath, 'r+');
|
|
39
|
+
this.lsn = this._readLastLSN(fd);
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
} else {
|
|
42
|
+
this.lsn = 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Async Write Stream for runtime
|
|
46
|
+
this.stream = fs.createWriteStream(this.walPath, { flags: 'a' });
|
|
47
|
+
|
|
48
|
+
// Capture fd for manual fsync if needed
|
|
49
|
+
this.stream.on('open', (fd) => {
|
|
50
|
+
this.fd = fd;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Handle errors
|
|
54
|
+
this.stream.on('error', (err) => {
|
|
55
|
+
console.error('[WAL] Stream error:', err);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Helper for _init extraction
|
|
60
|
+
_readLastLSN(fd) {
|
|
61
|
+
const stats = fs.fstatSync(fd);
|
|
62
|
+
if (stats.size === 0) return 0;
|
|
63
|
+
|
|
64
|
+
let maxLSN = 0;
|
|
65
|
+
const buffer = Buffer.allocUnsafe(stats.size);
|
|
66
|
+
fs.readSync(fd, buffer, 0, stats.size, 0);
|
|
67
|
+
|
|
68
|
+
let offset = 0;
|
|
69
|
+
while (offset < buffer.length) {
|
|
70
|
+
const magic = buffer.readUInt32LE(offset);
|
|
71
|
+
if (magic !== 0x57414C00) break;
|
|
72
|
+
|
|
73
|
+
const entrySize = buffer.readUInt32LE(offset + 4);
|
|
74
|
+
const lsn = Number(buffer.readBigUInt64LE(offset + 8));
|
|
75
|
+
|
|
76
|
+
if (lsn > maxLSN) maxLSN = lsn;
|
|
77
|
+
offset += entrySize;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return maxLSN;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* OPTIMIZED: Async timer-based flush and fsync
|
|
85
|
+
*/
|
|
86
|
+
_startSyncTimer() {
|
|
87
|
+
if (this.syncMode === 'off' || this.syncMode === 'full') return;
|
|
88
|
+
|
|
89
|
+
this.syncTimer = setInterval(() => {
|
|
90
|
+
// 1. Write any pending application-side buffer to OS
|
|
91
|
+
if (this.bufferOffset > 0) {
|
|
92
|
+
this._flushBuffer();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Force OS to persist to Disk (fsync)
|
|
96
|
+
// We use the raw fd from the stream
|
|
97
|
+
if (this.fd) {
|
|
98
|
+
fs.fsync(this.fd, (err) => {
|
|
99
|
+
if (err) console.error('[WAL] fsync warning:', err.message);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}, this.groupCommitMs);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
logOperation(operation, table, pageId, beforeImage, afterImage) {
|
|
106
|
+
if (!this.enabled) return;
|
|
107
|
+
|
|
108
|
+
this.lsn++;
|
|
109
|
+
this.operationCount++;
|
|
110
|
+
|
|
111
|
+
const opCode = this._getOpCode(operation);
|
|
112
|
+
const entry = this._createLogEntry(this.lsn, opCode, table, pageId, beforeImage, afterImage);
|
|
113
|
+
|
|
114
|
+
if (this.syncMode === 'full') {
|
|
115
|
+
// FULL: Force sync immediately (blocking, safest)
|
|
116
|
+
// We bypass stream buffer for FULL to ensure it hits disk now?
|
|
117
|
+
// Actually, mixing stream and raw sync write is bad.
|
|
118
|
+
// For FULL mode, we just write to stream and sync immediately?
|
|
119
|
+
// Stream write is async.
|
|
120
|
+
// To support 'FULL' (Strict Durability) correctly with Streams, we'd need to write and wait for callback.
|
|
121
|
+
// But logOperation is synchronous in current SawitDB architecture.
|
|
122
|
+
// Trade-off: We write to stream, but we can't block easily without de-optimizing.
|
|
123
|
+
// Fallback for FULL: Use fs.writeSync directly?
|
|
124
|
+
// If we use writeSync, we need a separate FD or ensure stream state is okay.
|
|
125
|
+
// Mixing is risky.
|
|
126
|
+
// Safe approach for FULL: Use _flushBuffer() then explicit sync?
|
|
127
|
+
this._writeToBuffer(entry);
|
|
128
|
+
this._flushBuffer(); // Pushes to stream
|
|
129
|
+
// Force sync on stream fd (might block event loop if we use fsyncSync)
|
|
130
|
+
if (this.fd) {
|
|
131
|
+
try { fs.fsyncSync(this.fd); } catch (e) { }
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// NORMAL / OFF
|
|
135
|
+
this._writeToBuffer(entry);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Auto checkpoint (less frequent)
|
|
139
|
+
if (this.operationCount >= this.checkpointInterval) {
|
|
140
|
+
this.checkpoint();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_writeToBuffer(entry) {
|
|
145
|
+
// If buffer is full, flush it to stream
|
|
146
|
+
if (this.bufferOffset + entry.length > this.bufferSize) {
|
|
147
|
+
this._flushBuffer();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
entry.copy(this.writeBuffer, this.bufferOffset);
|
|
151
|
+
this.bufferOffset += entry.length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_flushBuffer() {
|
|
155
|
+
if (this.bufferOffset === 0) return;
|
|
156
|
+
|
|
157
|
+
// WRITE TO ASYNC STREAM
|
|
158
|
+
const chunk = this.writeBuffer.slice(0, this.bufferOffset);
|
|
159
|
+
this.stream.write(chunk);
|
|
160
|
+
|
|
161
|
+
this.bufferOffset = 0;
|
|
162
|
+
this.lastSyncTime = Date.now();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_getOpCode(operation) {
|
|
166
|
+
const codes = {
|
|
167
|
+
'INSERT': 0x01,
|
|
168
|
+
'UPDATE': 0x02,
|
|
169
|
+
'DELETE': 0x03,
|
|
170
|
+
'CREATE_TABLE': 0x04,
|
|
171
|
+
'DROP_TABLE': 0x05,
|
|
172
|
+
'CHECKPOINT': 0x06
|
|
173
|
+
};
|
|
174
|
+
return codes[operation] || 0x00;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* OPTIMIZED: Simplified entry format (no checksums for speed)
|
|
179
|
+
*/
|
|
180
|
+
_createLogEntry(lsn, opCode, table, pageId, beforeImage, afterImage) {
|
|
181
|
+
const tableNameBuf = Buffer.alloc(32);
|
|
182
|
+
tableNameBuf.write(table);
|
|
183
|
+
|
|
184
|
+
const beforeSize = beforeImage ? beforeImage.length : 0;
|
|
185
|
+
const afterSize = afterImage ? afterImage.length : 0;
|
|
186
|
+
|
|
187
|
+
// OPTIMIZATION: Remove checksum (4 bytes saved, faster)
|
|
188
|
+
const entrySize = 4 + 4 + 8 + 1 + 32 + 4 + 4 + 4 + beforeSize + afterSize;
|
|
189
|
+
const entry = Buffer.allocUnsafe(entrySize);
|
|
190
|
+
|
|
191
|
+
let offset = 0;
|
|
192
|
+
entry.writeUInt32LE(0x57414C00, offset); offset += 4;
|
|
193
|
+
entry.writeUInt32LE(entrySize, offset); offset += 4;
|
|
194
|
+
entry.writeBigUInt64LE(BigInt(lsn), offset); offset += 8;
|
|
195
|
+
entry.writeUInt8(opCode, offset); offset += 1;
|
|
196
|
+
tableNameBuf.copy(entry, offset); offset += 32;
|
|
197
|
+
entry.writeUInt32LE(pageId, offset); offset += 4;
|
|
198
|
+
entry.writeUInt32LE(beforeSize, offset); offset += 4;
|
|
199
|
+
entry.writeUInt32LE(afterSize, offset); offset += 4;
|
|
200
|
+
|
|
201
|
+
if (beforeImage) {
|
|
202
|
+
beforeImage.copy(entry, offset);
|
|
203
|
+
offset += beforeSize;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (afterImage) {
|
|
207
|
+
afterImage.copy(entry, offset);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return entry;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_writeEntry(entry) {
|
|
214
|
+
if (!this.fd) return;
|
|
215
|
+
fs.writeSync(this.fd, entry);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_flushPendingOps() {
|
|
219
|
+
// Flush buffer if any
|
|
220
|
+
this._flushBuffer();
|
|
221
|
+
|
|
222
|
+
// Clear pending ops array
|
|
223
|
+
this.pendingOps = [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
checkpoint() {
|
|
227
|
+
if (!this.enabled) return;
|
|
228
|
+
|
|
229
|
+
this._flushPendingOps();
|
|
230
|
+
|
|
231
|
+
const checkpointEntry = this._createLogEntry(
|
|
232
|
+
++this.lsn,
|
|
233
|
+
this._getOpCode('CHECKPOINT'),
|
|
234
|
+
'',
|
|
235
|
+
0,
|
|
236
|
+
null,
|
|
237
|
+
null
|
|
238
|
+
);
|
|
239
|
+
this._writeEntry(checkpointEntry);
|
|
240
|
+
if (this.fd) {
|
|
241
|
+
try {
|
|
242
|
+
fs.fsyncSync(this.fd);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// Ignore invalid fd if stream closed or not ready
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.operationCount = 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
recover() {
|
|
252
|
+
if (!this.enabled || !fs.existsSync(this.walPath)) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const stats = fs.statSync(this.walPath);
|
|
257
|
+
if (stats.size === 0) return [];
|
|
258
|
+
|
|
259
|
+
const fd = fs.openSync(this.walPath, 'r');
|
|
260
|
+
const buffer = Buffer.allocUnsafe(stats.size);
|
|
261
|
+
fs.readSync(fd, buffer, 0, stats.size, 0);
|
|
262
|
+
fs.closeSync(fd);
|
|
263
|
+
|
|
264
|
+
const operations = [];
|
|
265
|
+
let offset = 0;
|
|
266
|
+
|
|
267
|
+
while (offset < buffer.length) {
|
|
268
|
+
try {
|
|
269
|
+
const magic = buffer.readUInt32LE(offset);
|
|
270
|
+
if (magic !== 0x57414C00) break;
|
|
271
|
+
|
|
272
|
+
const entrySize = buffer.readUInt32LE(offset + 4);
|
|
273
|
+
const lsn = Number(buffer.readBigUInt64LE(offset + 8));
|
|
274
|
+
const opCode = buffer.readUInt8(offset + 16);
|
|
275
|
+
const tableName = buffer.toString('utf8', offset + 17, offset + 49).replace(/\0/g, '');
|
|
276
|
+
const pageId = buffer.readUInt32LE(offset + 49);
|
|
277
|
+
const beforeSize = buffer.readUInt32LE(offset + 53);
|
|
278
|
+
const afterSize = buffer.readUInt32LE(offset + 57);
|
|
279
|
+
|
|
280
|
+
let dataOffset = offset + 61;
|
|
281
|
+
const beforeImage = beforeSize > 0 ? buffer.slice(dataOffset, dataOffset + beforeSize) : null;
|
|
282
|
+
dataOffset += beforeSize;
|
|
283
|
+
const afterImage = afterSize > 0 ? buffer.slice(dataOffset, dataOffset + afterSize) : null;
|
|
284
|
+
|
|
285
|
+
operations.push({
|
|
286
|
+
lsn,
|
|
287
|
+
opCode,
|
|
288
|
+
tableName,
|
|
289
|
+
pageId,
|
|
290
|
+
beforeImage,
|
|
291
|
+
afterImage
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
offset += entrySize;
|
|
295
|
+
} catch (e) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return operations;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
truncate() {
|
|
304
|
+
if (!this.enabled) return;
|
|
305
|
+
|
|
306
|
+
this._flushBuffer(); // Flush any pending first (async)
|
|
307
|
+
// Note: fs.truncateSync works on path or fd.
|
|
308
|
+
// With stream, it might be tricky if stream is writing.
|
|
309
|
+
// Best effort:
|
|
310
|
+
try { fs.truncateSync(this.walPath, 0); } catch (e) { }
|
|
311
|
+
this.lsn = 0;
|
|
312
|
+
this.operationCount = 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
close() {
|
|
316
|
+
if (!this.enabled) return;
|
|
317
|
+
|
|
318
|
+
// Stop sync timer
|
|
319
|
+
if (this.syncTimer) {
|
|
320
|
+
clearInterval(this.syncTimer);
|
|
321
|
+
this.syncTimer = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Final flush
|
|
325
|
+
this._flushBuffer();
|
|
326
|
+
|
|
327
|
+
if (this.stream) {
|
|
328
|
+
this.stream.end();
|
|
329
|
+
this.stream = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (this.syncMode === 'full' && this.fd) {
|
|
333
|
+
try { fs.closeSync(this.fd); } catch (e) { }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.fd = null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = WAL;
|