trac-msb 0.2.24 → 0.2.26

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-msb",
3
3
  "main": "msb.mjs",
4
- "version": "0.2.24",
4
+ "version": "0.2.26",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -219,6 +219,11 @@ export class Config {
219
219
  return this.#config.validatorHealthCheckFailureThreshold
220
220
  }
221
221
 
222
+ get confirmationStallTimeout() {
223
+ if (this.#isOverriden('confirmationStallTimeout')) return this.#options.confirmationStallTimeout
224
+ return this.#config.confirmationStallTimeout
225
+ }
226
+
222
227
  get maxPendingRequestsInPendingRequestsService() {
223
228
  return this.#config.maxPendingRequestsInPendingRequestsService
224
229
  }
package/src/config/env.js CHANGED
@@ -57,6 +57,7 @@ const configData = {
57
57
  txPoolSize: 1000, // size of transaction pool
58
58
  validatorHealthCheckInterval: 5 * 60 * 1000, // How often to check validator health (ms)
59
59
  validatorHealthCheckFailureThreshold: 3, // Consecutive failed health checks before a validator is removed
60
+ confirmationStallTimeout: 120_000, // Force a fast-forward when the confirmed length has not moved for this long while unconfirmed entries wait (ms, 0 = off)
60
61
  storesDirectory: 'stores/',
61
62
  storeName: 'testnet',
62
63
  },
@@ -107,6 +108,7 @@ const configData = {
107
108
  txPoolSize: 1000, // size of transaction pool
108
109
  validatorHealthCheckInterval: 5 * 60 * 1000, // How often to check validator health (ms)
109
110
  validatorHealthCheckFailureThreshold: 3, // Consecutive failed health checks before a validator is removed
111
+ confirmationStallTimeout: 120_000, // Force a fast-forward when the confirmed length has not moved for this long while unconfirmed entries wait (ms, 0 = off)
110
112
  storesDirectory: 'stores/',
111
113
  storeName: 'mainnet',
112
114
  },
@@ -157,6 +159,7 @@ const configData = {
157
159
  txPoolSize: 1000, // size of transaction pool
158
160
  validatorHealthCheckInterval: 1_000, // How often to check validator health (ms)
159
161
  validatorHealthCheckFailureThreshold: 3, // Consecutive failed health checks before a validator is removed
162
+ confirmationStallTimeout: 120_000, // Force a fast-forward when the confirmed length has not moved for this long while unconfirmed entries wait (ms, 0 = off)
160
163
  storesDirectory: 'stores/',
161
164
  storeName: 'development',
162
165
  }
@@ -0,0 +1,82 @@
1
+ const DEFAULT_CHECK_INTERVAL = 15_000;
2
+
3
+ /**
4
+ * Autobase starts a follower's fast-forward only when the network's signed length is at least
5
+ * 16 entries past the follower's total length, unconfirmed entries included. After a writer is
6
+ * removed in the middle of a batch, a follower that already holds unconfirmed entries can stay
7
+ * above that mark: its confirmed length then stops for good, and a restart does not help.
8
+ *
9
+ * This watch forces one fast-forward when the confirmed length has not moved for `timeout` ms
10
+ * while unconfirmed entries wait. It never runs on an indexer and does not change how entries are
11
+ * applied.
12
+ */
13
+ export class ConfirmationWatch {
14
+ #base;
15
+ #timeout;
16
+ #interval;
17
+ #now;
18
+ #log;
19
+ #timer = null;
20
+ #mark = null;
21
+ #running = false;
22
+
23
+ constructor(base, { timeout, interval = DEFAULT_CHECK_INTERVAL, now = Date.now, log = console.log } = {}) {
24
+ this.#base = base;
25
+ this.#timeout = timeout;
26
+ this.#interval = Math.min(interval, timeout);
27
+ this.#now = now;
28
+ this.#log = log;
29
+ }
30
+
31
+ get enabled() {
32
+ return Number.isFinite(this.#timeout) && this.#timeout > 0;
33
+ }
34
+
35
+ start() {
36
+ if (!this.enabled || this.#timer !== null) return;
37
+ this.#timer = setInterval(() => { this.check().catch(() => {}) }, this.#interval);
38
+ this.#timer.unref?.();
39
+ }
40
+
41
+ stop() {
42
+ if (this.#timer !== null) clearInterval(this.#timer);
43
+ this.#timer = null;
44
+ }
45
+
46
+ /** @returns {Promise<boolean>} true when a fast-forward was forced */
47
+ async check() {
48
+ const base = this.#base;
49
+ if (this.#running || base.closing || base.isIndexer) return false;
50
+ if (base.isFastForwarding?.()) {
51
+ this.#mark = null;
52
+ return false;
53
+ }
54
+
55
+ const confirmed = base.view.core.signedLength;
56
+ const total = base.view.core.length;
57
+ const now = this.#now();
58
+
59
+ if (total <= confirmed) {
60
+ this.#mark = null;
61
+ return false;
62
+ }
63
+ if (this.#mark === null || this.#mark.confirmed !== confirmed) {
64
+ this.#mark = { confirmed, since: now };
65
+ return false;
66
+ }
67
+ if (now - this.#mark.since < this.#timeout) return false;
68
+
69
+ this.#running = true;
70
+ try {
71
+ this.#log(`[MSB] Confirmed length stuck at ${confirmed} (local ${total}) for ${Math.round((now - this.#mark.since) / 1000)} s; forcing a fast-forward`);
72
+ await base.forceFastForward();
73
+ this.#log(`[MSB] Fast-forward finished: confirmed ${base.view.core.signedLength}, local ${base.view.core.length}`);
74
+ } catch (error) {
75
+ this.#log(`[MSB] Fast-forward failed: ${error?.message ?? error}`);
76
+ } finally {
77
+ this.#running = false;
78
+ this.#mark = { confirmed: base.view.core.signedLength, since: this.#now() };
79
+ }
80
+ return true;
81
+ }
82
+ }
@@ -42,6 +42,7 @@ import { deepCopyBuffer } from '../../utils/buffer.js';
42
42
  import { Status } from './utils/transaction.js';
43
43
  import remote from 'hypercore/lib/fully-remote-proof.js'
44
44
  import PQueue from 'p-queue';
45
+ import { ConfirmationWatch } from './ConfirmationWatch.js';
45
46
 
46
47
  const OVERSIZED_BATCH_PENALTY_MULTIPLIER = BATCH_SIZE;
47
48
 
@@ -56,6 +57,7 @@ class State extends ReadyResource {
56
57
  #config
57
58
  #wallet
58
59
  #activeWriterCountCache = new Map();
60
+ #confirmationWatch = null;
59
61
 
60
62
  /**
61
63
  * @param {Corestore} store
@@ -96,10 +98,13 @@ class State extends ReadyResource {
96
98
  console.log("State initialization...")
97
99
  await this.#base.ready();
98
100
  this.#writingKey = this.#base.local.key;
101
+ this.#confirmationWatch = new ConfirmationWatch(this.#base, { timeout: this.#config.confirmationStallTimeout });
102
+ this.#confirmationWatch.start();
99
103
  }
100
104
 
101
105
  async _close() {
102
106
  console.log("State: closing gracefully...");
107
+ this.#confirmationWatch?.stop();
103
108
  if (this.#bee !== null) {
104
109
  await this.#bee.close();
105
110
  }