s3db.js 11.0.2 → 11.0.4
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/dist/s3db.cjs.js +612 -308
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +612 -308
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/concerns/plugin-storage.js +274 -9
- package/src/plugins/audit.plugin.js +94 -18
- package/src/plugins/eventual-consistency/analytics.js +350 -15
- package/src/plugins/eventual-consistency/config.js +3 -0
- package/src/plugins/eventual-consistency/consolidation.js +32 -36
- package/src/plugins/eventual-consistency/garbage-collection.js +11 -13
- package/src/plugins/eventual-consistency/index.js +28 -19
- package/src/plugins/eventual-consistency/install.js +9 -26
- package/src/plugins/eventual-consistency/partitions.js +5 -0
- package/src/plugins/eventual-consistency/transactions.js +1 -0
- package/src/plugins/eventual-consistency/utils.js +36 -1
- package/src/plugins/fulltext.plugin.js +76 -22
- package/src/plugins/metrics.plugin.js +70 -20
- package/src/plugins/s3-queue.plugin.js +21 -120
- package/src/plugins/scheduler.plugin.js +11 -37
|
@@ -139,31 +139,8 @@ export class S3QueuePlugin extends Plugin {
|
|
|
139
139
|
|
|
140
140
|
this.queueResource = this.database.resources[queueName];
|
|
141
141
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
const [okLock, errLock] = await tryFn(() =>
|
|
145
|
-
this.database.createResource({
|
|
146
|
-
name: lockName,
|
|
147
|
-
attributes: {
|
|
148
|
-
id: 'string|required',
|
|
149
|
-
workerId: 'string|required',
|
|
150
|
-
timestamp: 'number|required',
|
|
151
|
-
ttl: 'number|default:5000'
|
|
152
|
-
},
|
|
153
|
-
behavior: 'body-overflow',
|
|
154
|
-
timestamps: false
|
|
155
|
-
})
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
if (okLock || this.database.resources[lockName]) {
|
|
159
|
-
this.lockResource = this.database.resources[lockName];
|
|
160
|
-
} else {
|
|
161
|
-
// Locks disabled if creation fails
|
|
162
|
-
this.lockResource = null;
|
|
163
|
-
if (this.config.verbose) {
|
|
164
|
-
console.log(`[S3QueuePlugin] Lock resource creation failed, locking disabled: ${errLock?.message}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
142
|
+
// Locks are now managed by PluginStorage with TTL - no Resource needed
|
|
143
|
+
// Lock acquisition is handled via storage.acquireLock() with automatic expiration
|
|
167
144
|
|
|
168
145
|
// Add helper methods to target resource
|
|
169
146
|
this.addHelperMethods();
|
|
@@ -273,14 +250,7 @@ export class S3QueuePlugin extends Plugin {
|
|
|
273
250
|
}
|
|
274
251
|
}, 5000);
|
|
275
252
|
|
|
276
|
-
//
|
|
277
|
-
this.lockCleanupInterval = setInterval(() => {
|
|
278
|
-
this.cleanupStaleLocks().catch(err => {
|
|
279
|
-
if (this.config.verbose) {
|
|
280
|
-
console.log(`[lockCleanup] Error: ${err.message}`);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
}, 10000);
|
|
253
|
+
// Lock cleanup no longer needed - TTL handles expiration automatically
|
|
284
254
|
|
|
285
255
|
// Start N workers
|
|
286
256
|
for (let i = 0; i < concurrency; i++) {
|
|
@@ -306,11 +276,7 @@ export class S3QueuePlugin extends Plugin {
|
|
|
306
276
|
this.cacheCleanupInterval = null;
|
|
307
277
|
}
|
|
308
278
|
|
|
309
|
-
//
|
|
310
|
-
if (this.lockCleanupInterval) {
|
|
311
|
-
clearInterval(this.lockCleanupInterval);
|
|
312
|
-
this.lockCleanupInterval = null;
|
|
313
|
-
}
|
|
279
|
+
// Lock cleanup interval no longer exists (TTL handles it)
|
|
314
280
|
|
|
315
281
|
// Wait for workers to finish current tasks
|
|
316
282
|
await Promise.all(this.workers);
|
|
@@ -383,59 +349,21 @@ export class S3QueuePlugin extends Plugin {
|
|
|
383
349
|
}
|
|
384
350
|
|
|
385
351
|
/**
|
|
386
|
-
* Acquire a distributed lock using
|
|
352
|
+
* Acquire a distributed lock using PluginStorage TTL
|
|
387
353
|
* This ensures only one worker can claim a message at a time
|
|
388
|
-
*
|
|
389
|
-
* Uses a two-step process:
|
|
390
|
-
* 1. Create lock resource (similar to queue resource) if not exists
|
|
391
|
-
* 2. Try to claim lock using ETag-based conditional update
|
|
392
354
|
*/
|
|
393
355
|
async acquireLock(messageId) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const lockId = `lock-${messageId}`;
|
|
399
|
-
const now = Date.now();
|
|
356
|
+
const storage = this.getStorage();
|
|
357
|
+
const lockKey = `msg-${messageId}`;
|
|
400
358
|
|
|
401
359
|
try {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (existingLock) {
|
|
408
|
-
// Lock exists - check if expired
|
|
409
|
-
const lockAge = now - existingLock.timestamp;
|
|
410
|
-
if (lockAge < existingLock.ttl) {
|
|
411
|
-
// Lock still valid, owned by another worker
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
// Lock expired - try to claim it with ETag
|
|
415
|
-
const [ok, err, result] = await tryFn(() =>
|
|
416
|
-
this.lockResource.updateConditional(lockId, {
|
|
417
|
-
workerId: this.workerId,
|
|
418
|
-
timestamp: now,
|
|
419
|
-
ttl: 5000
|
|
420
|
-
}, {
|
|
421
|
-
ifMatch: existingLock._etag
|
|
422
|
-
})
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
return ok && result.success;
|
|
426
|
-
}
|
|
360
|
+
const lock = await storage.acquireLock(lockKey, {
|
|
361
|
+
ttl: 5, // 5 seconds
|
|
362
|
+
timeout: 0, // Don't wait if locked
|
|
363
|
+
workerId: this.workerId
|
|
364
|
+
});
|
|
427
365
|
|
|
428
|
-
|
|
429
|
-
const [okCreate, errCreate] = await tryFn(() =>
|
|
430
|
-
this.lockResource.insert({
|
|
431
|
-
id: lockId,
|
|
432
|
-
workerId: this.workerId,
|
|
433
|
-
timestamp: now,
|
|
434
|
-
ttl: 5000
|
|
435
|
-
})
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
return okCreate;
|
|
366
|
+
return lock !== null;
|
|
439
367
|
} catch (error) {
|
|
440
368
|
// On any error, skip this message
|
|
441
369
|
if (this.config.verbose) {
|
|
@@ -446,17 +374,14 @@ export class S3QueuePlugin extends Plugin {
|
|
|
446
374
|
}
|
|
447
375
|
|
|
448
376
|
/**
|
|
449
|
-
* Release a distributed lock
|
|
377
|
+
* Release a distributed lock via PluginStorage
|
|
450
378
|
*/
|
|
451
379
|
async releaseLock(messageId) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const lockId = `lock-${messageId}`;
|
|
380
|
+
const storage = this.getStorage();
|
|
381
|
+
const lockKey = `msg-${messageId}`;
|
|
457
382
|
|
|
458
383
|
try {
|
|
459
|
-
await
|
|
384
|
+
await storage.releaseLock(lockKey);
|
|
460
385
|
} catch (error) {
|
|
461
386
|
// Ignore errors on release (lock may have expired or been cleaned up)
|
|
462
387
|
if (this.config.verbose) {
|
|
@@ -466,36 +391,12 @@ export class S3QueuePlugin extends Plugin {
|
|
|
466
391
|
}
|
|
467
392
|
|
|
468
393
|
/**
|
|
469
|
-
* Clean up stale locks
|
|
470
|
-
*
|
|
394
|
+
* Clean up stale locks - NO LONGER NEEDED
|
|
395
|
+
* TTL handles automatic expiration, no manual cleanup required
|
|
471
396
|
*/
|
|
472
397
|
async cleanupStaleLocks() {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const now = Date.now();
|
|
478
|
-
|
|
479
|
-
try {
|
|
480
|
-
// List all locks
|
|
481
|
-
const locks = await this.lockResource.list();
|
|
482
|
-
|
|
483
|
-
// Delete expired locks
|
|
484
|
-
for (const lock of locks) {
|
|
485
|
-
const lockAge = now - lock.timestamp;
|
|
486
|
-
if (lockAge > lock.ttl) {
|
|
487
|
-
await this.lockResource.delete(lock.id);
|
|
488
|
-
if (this.config.verbose) {
|
|
489
|
-
console.log(`[cleanupStaleLocks] Removed expired lock: ${lock.id}`);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
} catch (error) {
|
|
494
|
-
// Ignore errors in cleanup (non-critical)
|
|
495
|
-
if (this.config.verbose) {
|
|
496
|
-
console.log(`[cleanupStaleLocks] Error during cleanup: ${error.message}`);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
398
|
+
// TTL automatically expires locks - no manual cleanup needed! ✨
|
|
399
|
+
return;
|
|
499
400
|
}
|
|
500
401
|
|
|
501
402
|
async attemptClaim(msg) {
|
|
@@ -163,7 +163,6 @@ export class SchedulerPlugin extends Plugin {
|
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
this.database = null;
|
|
166
|
-
this.lockResource = null;
|
|
167
166
|
this.jobs = new Map();
|
|
168
167
|
this.activeJobs = new Map();
|
|
169
168
|
this.timers = new Map();
|
|
@@ -218,9 +217,7 @@ export class SchedulerPlugin extends Plugin {
|
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
async onInstall() {
|
|
221
|
-
|
|
222
|
-
// Create lock resource for distributed locking
|
|
223
|
-
await this._createLockResource();
|
|
220
|
+
// Locks are now managed by PluginStorage with TTL - no Resource needed
|
|
224
221
|
|
|
225
222
|
// Create job execution history resource
|
|
226
223
|
if (this.config.persistJobs) {
|
|
@@ -258,27 +255,6 @@ export class SchedulerPlugin extends Plugin {
|
|
|
258
255
|
this.emit('initialized', { jobs: this.jobs.size });
|
|
259
256
|
}
|
|
260
257
|
|
|
261
|
-
async _createLockResource() {
|
|
262
|
-
const [ok, err, lockResource] = await tryFn(() =>
|
|
263
|
-
this.database.createResource({
|
|
264
|
-
name: 'plg_scheduler_job_locks',
|
|
265
|
-
attributes: {
|
|
266
|
-
id: 'string|required',
|
|
267
|
-
jobName: 'string|required',
|
|
268
|
-
lockedAt: 'number|required',
|
|
269
|
-
instanceId: 'string|optional'
|
|
270
|
-
},
|
|
271
|
-
behavior: 'body-only',
|
|
272
|
-
timestamps: false
|
|
273
|
-
})
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
if (!ok && !this.database.resources.plg_scheduler_job_locks) {
|
|
277
|
-
throw new Error(`Failed to create lock resource: ${err?.message}`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
this.lockResource = ok ? lockResource : this.database.resources.plg_scheduler_job_locks;
|
|
281
|
-
}
|
|
282
258
|
|
|
283
259
|
async _createJobHistoryResource() {
|
|
284
260
|
const [ok] = await tryFn(() => this.database.createResource({
|
|
@@ -416,19 +392,17 @@ export class SchedulerPlugin extends Plugin {
|
|
|
416
392
|
// Mark as active immediately (will be updated with executionId later)
|
|
417
393
|
this.activeJobs.set(jobName, 'acquiring-lock');
|
|
418
394
|
|
|
419
|
-
// Acquire distributed lock to prevent concurrent execution across instances
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
})
|
|
428
|
-
);
|
|
395
|
+
// Acquire distributed lock with TTL to prevent concurrent execution across instances
|
|
396
|
+
const storage = this.getStorage();
|
|
397
|
+
const lockKey = `job-${jobName}`;
|
|
398
|
+
const lock = await storage.acquireLock(lockKey, {
|
|
399
|
+
ttl: Math.ceil(job.timeout / 1000) + 60, // Job timeout + 60 seconds buffer
|
|
400
|
+
timeout: 0, // Don't wait if locked
|
|
401
|
+
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
402
|
+
});
|
|
429
403
|
|
|
430
404
|
// If lock couldn't be acquired, another instance is executing this job
|
|
431
|
-
if (!
|
|
405
|
+
if (!lock) {
|
|
432
406
|
if (this.config.verbose) {
|
|
433
407
|
console.log(`[SchedulerPlugin] Job '${jobName}' already running on another instance`);
|
|
434
408
|
}
|
|
@@ -577,7 +551,7 @@ export class SchedulerPlugin extends Plugin {
|
|
|
577
551
|
}
|
|
578
552
|
} finally {
|
|
579
553
|
// Always release the distributed lock
|
|
580
|
-
await tryFn(() =>
|
|
554
|
+
await tryFn(() => storage.releaseLock(lockKey));
|
|
581
555
|
}
|
|
582
556
|
}
|
|
583
557
|
|