@agoric/cosmic-swingset 0.40.0 → 0.40.1-dev-800c2bb.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/src/chain-main.js CHANGED
@@ -1,30 +1,46 @@
1
- import path from 'path';
1
+ // @ts-check
2
+
3
+ import { resolve as pathResolve } from 'path';
2
4
  import v8 from 'node:v8';
3
5
  import process from 'node:process';
4
6
  import fs from 'node:fs';
7
+ import fsPromises from 'node:fs/promises';
5
8
  import { performance } from 'perf_hooks';
6
9
  import { resolve as importMetaResolve } from 'import-meta-resolve';
10
+ import tmpfs from 'tmp';
11
+ import { fork } from 'node:child_process';
12
+
13
+ import { E } from '@endo/far';
7
14
  import engineGC from '@agoric/swingset-vat/src/lib-nodejs/engine-gc.js';
8
15
  import { waitUntilQuiescent } from '@agoric/swingset-vat/src/lib-nodejs/waitUntilQuiescent.js';
9
16
  import {
10
17
  importMailbox,
11
18
  exportMailbox,
12
19
  } from '@agoric/swingset-vat/src/devices/mailbox/mailbox.js';
13
- import { makeBufferedStorage } from '@agoric/swingset-vat/src/lib/storageAPI.js';
14
20
 
15
- import { assert, details as X } from '@agoric/assert';
16
- import { makeSlogSender } from '@agoric/telemetry';
21
+ import { Fail, q } from '@agoric/assert';
22
+ import { makeSlogSender, tryFlushSlogSender } from '@agoric/telemetry';
17
23
 
18
- import { makeChainStorageRoot } from '@agoric/vats/src/lib-chainStorage.js';
24
+ import {
25
+ makeChainStorageRoot,
26
+ makeSerializeToStorage,
27
+ } from '@agoric/internal/src/lib-chainStorage.js';
19
28
  import { makeMarshal } from '@endo/marshal';
20
- import { makeStoredSubscriber, makePublishKit } from '@agoric/notifier';
29
+ import { makeShutdown } from '@agoric/internal/src/node/shutdown.js';
21
30
 
22
- import * as STORAGE_PATH from '@agoric/vats/src/chain-storage-paths.js';
31
+ import * as STORAGE_PATH from '@agoric/internal/src/chain-storage-paths.js';
32
+ import * as ActionType from '@agoric/internal/src/action-types.js';
23
33
  import { BridgeId as BRIDGE_ID } from '@agoric/internal';
24
- import stringify from './json-stable-stringify.js';
34
+ import {
35
+ makeBufferedStorage,
36
+ makeReadCachingStorage,
37
+ } from './helpers/bufferedStorage.js';
38
+ import stringify from './helpers/json-stable-stringify.js';
25
39
  import { launch } from './launch-chain.js';
26
40
  import { getTelemetryProviders } from './kernel-stats.js';
27
- import { makeQueue } from './make-queue.js';
41
+ import { makeProcessValue } from './helpers/process-value.js';
42
+ import { spawnSwingStoreExport } from './export-kernel-db.js';
43
+ import { performStateSyncImport } from './import-kernel-db.js';
28
44
 
29
45
  // eslint-disable-next-line no-unused-vars
30
46
  let whenHellFreezesOver = null;
@@ -36,83 +52,70 @@ const TELEMETRY_SERVICE_NAME = 'agd-cosmos';
36
52
  const toNumber = specimen => {
37
53
  const number = parseInt(specimen, 10);
38
54
  String(number) === String(specimen) ||
39
- assert.fail(X`Could not parse ${JSON.stringify(specimen)} as a number`);
55
+ Fail`Could not parse ${JSON.stringify(specimen)} as a number`;
40
56
  return number;
41
57
  };
42
58
 
43
- const makeChainStorage = (call, prefix = '', options = {}) => {
44
- prefix === '' ||
45
- prefix.endsWith('.') ||
46
- assert.fail(X`prefix ${prefix} must end with a dot`);
47
-
48
- const { fromChainShape, toChainShape } = options;
49
-
50
- // In addition to the wrapping write buffer, keep a simple cache of
51
- // read values for has and get.
52
- let cache;
53
- function resetCache() {
54
- cache = new Map();
55
- }
56
- resetCache();
57
- const storage = {
58
- has(key) {
59
- return storage.get(key) !== undefined;
59
+ /**
60
+ * @template {unknown} [T=unknown]
61
+ * @param {(req: string) => string} call
62
+ * @param {string} prefix
63
+ * @param {"set" | "legacySet" | "setWithoutNotify"} setterMethod
64
+ * @param {(value: string) => T} fromBridgeStringValue
65
+ * @param {(value: T) => string} toBridgeStringValue
66
+ * @returns {import("./helpers/bufferedStorage.js").KVStore<T>}
67
+ */
68
+ const makePrefixedBridgeStorage = (
69
+ call,
70
+ prefix,
71
+ setterMethod,
72
+ fromBridgeStringValue = x => JSON.parse(x),
73
+ toBridgeStringValue = x => stringify(x),
74
+ ) => {
75
+ prefix.endsWith('.') || Fail`prefix ${prefix} must end with a dot`;
76
+
77
+ return harden({
78
+ has: key => {
79
+ const retStr = call(
80
+ stringify({ method: 'has', args: [`${prefix}${key}`] }),
81
+ );
82
+ const ret = JSON.parse(retStr);
83
+ return ret;
60
84
  },
61
- get(key) {
62
- if (cache.has(key)) return cache.get(key);
63
-
64
- // Fetch the value and cache it until the next commit or abort.
65
- const retStr = call(stringify({ method: 'get', key: `${prefix}${key}` }));
85
+ get: key => {
86
+ const retStr = call(
87
+ stringify({ method: 'get', args: [`${prefix}${key}`] }),
88
+ );
66
89
  const ret = JSON.parse(retStr);
67
- const chainShapeValue = ret ? JSON.parse(ret) : undefined;
68
- const value =
69
- chainShapeValue === undefined || !fromChainShape
70
- ? chainShapeValue
71
- : fromChainShape(chainShapeValue);
72
- cache.set(key, value);
73
- return value;
90
+ if (ret == null) {
91
+ return undefined;
92
+ }
93
+ return fromBridgeStringValue(ret);
74
94
  },
75
- set(key, value) {
76
- // Set the value and cache it until the next commit or abort (which is
77
- // expected immediately, since the buffered wrapper only calls set
78
- // *during* a commit).
79
- cache.set(key, value);
80
- const chainShapeValue =
81
- value === undefined || !toChainShape ? value : toChainShape(value);
82
- const valueStr = value === undefined ? '' : stringify(chainShapeValue);
95
+ set: (key, value) => {
96
+ const path = `${prefix}${key}`;
97
+ const entry = [path, toBridgeStringValue(value)];
83
98
  call(
84
99
  stringify({
85
- method: 'set',
86
- key: `${prefix}${key}`,
87
- value: valueStr,
100
+ method: setterMethod,
101
+ args: [entry],
88
102
  }),
89
103
  );
90
104
  },
91
- delete(key) {
92
- // Deletion in chain storage manifests as set-to-undefined.
93
- storage.set(key, undefined);
94
- },
95
- // eslint-disable-next-line require-yield
96
- *getKeys(_start, _end) {
97
- throw new Error('not implemented');
105
+ delete: key => {
106
+ const path = `${prefix}${key}`;
107
+ const entry = [path];
108
+ call(
109
+ stringify({
110
+ method: setterMethod,
111
+ args: [entry],
112
+ }),
113
+ );
98
114
  },
99
- };
100
- const {
101
- kvStore: buffered,
102
- commit,
103
- abort,
104
- } = makeBufferedStorage(storage, {
105
- // Enqueue a write of any retrieved value, to handle callers like mailbox.js
106
- // that expect local mutations to be automatically written back.
107
- onGet(key, value) {
108
- buffered.set(key, value);
115
+ getNextKey(_previousKey) {
116
+ throw Error('not implemented');
109
117
  },
110
-
111
- // Reset the read cache upon commit or abort.
112
- onCommit: resetCache,
113
- onAbort: resetCache,
114
118
  });
115
- return { ...buffered, commit, abort };
116
119
  };
117
120
 
118
121
  export default async function main(progname, args, { env, homedir, agcc }) {
@@ -120,32 +123,16 @@ export default async function main(progname, args, { env, homedir, agcc }) {
120
123
 
121
124
  // TODO: use the 'basedir' pattern
122
125
 
123
- // Try to determine the cosmos chain home.
124
- function getFlagValue(flagName, deflt) {
125
- let flagValue = deflt;
126
- const envValue =
127
- env[`AG_CHAIN_COSMOS_${flagName.toUpperCase().replace(/-/g, '_')}`];
128
- if (envValue !== undefined) {
129
- flagValue = envValue;
130
- }
131
- const flag = `--${flagName}`;
132
- const flagEquals = `--${flagName}=`;
133
- for (let i = 0; i < args.length; i += 1) {
134
- const arg = args[i];
135
- if (arg === flag) {
136
- i += 1;
137
- flagValue = args[i];
138
- } else if (arg.startsWith(flagEquals)) {
139
- flagValue = arg.substr(flagEquals.length);
140
- }
141
- }
142
- return flagValue;
143
- }
126
+ const processValue = makeProcessValue({ env, args });
144
127
 
145
128
  // We try to find the actual cosmos state directory (default=~/.ag-chain-cosmos), which
146
129
  // is better than scribbling into the current directory.
147
- const cosmosHome = getFlagValue('home', `${homedir}/.ag-chain-cosmos`);
148
- const stateDBDir = `${cosmosHome}/data/ag-cosmos-chain-state`;
130
+ const cosmosHome = processValue.getFlag(
131
+ 'home',
132
+ `${homedir}/.ag-chain-cosmos`,
133
+ );
134
+ const stateDBDir = `${cosmosHome}/data/agoric`;
135
+ fs.mkdirSync(stateDBDir, { recursive: true });
149
136
 
150
137
  // console.log('Have AG_COSMOS', agcc);
151
138
 
@@ -154,14 +141,11 @@ export default async function main(progname, args, { env, homedir, agcc }) {
154
141
  function registerPortHandler(portHandler) {
155
142
  lastPort += 1;
156
143
  const port = lastPort;
157
- portHandlers[port] = async (...phArgs) => {
158
- try {
159
- return await portHandler(...phArgs);
160
- } catch (e) {
144
+ portHandlers[port] = async (...phArgs) =>
145
+ E.resolve(portHandler(...phArgs)).catch(e => {
161
146
  console.error('portHandler threw', e);
162
147
  throw e;
163
- }
164
- };
148
+ });
165
149
  return port;
166
150
  }
167
151
 
@@ -174,7 +158,8 @@ export default async function main(progname, args, { env, homedir, agcc }) {
174
158
  }
175
159
  const action = JSON.parse(str);
176
160
  const p = Promise.resolve(handler(action));
177
- p.then(
161
+ void E.when(
162
+ p,
178
163
  res => {
179
164
  // console.error(`Replying in Node to ${str} with`, res);
180
165
  replier.resolve(stringify(res !== undefined ? res : null));
@@ -195,6 +180,23 @@ export default async function main(progname, args, { env, homedir, agcc }) {
195
180
  whenHellFreezesOver = new Promise(() => {});
196
181
  agcc.runAgCosmosDaemon(nodePort, fromGo, [progname, ...args]);
197
182
 
183
+ /**
184
+ * @type {undefined | {
185
+ * blockHeight: number,
186
+ * exporter?: import('./export-kernel-db.js').StateSyncExporter,
187
+ * exportDir?: string,
188
+ * cleanup?: () => Promise<void>,
189
+ * }}
190
+ */
191
+ let stateSyncExport;
192
+
193
+ async function discardStateSyncExport() {
194
+ const exportData = stateSyncExport;
195
+ stateSyncExport = undefined;
196
+ await exportData?.exporter?.stop();
197
+ await exportData?.cleanup?.();
198
+ }
199
+
198
200
  let savedChainSends = [];
199
201
 
200
202
  // Send a chain downcall, recording what we sent and received.
@@ -204,7 +206,10 @@ export default async function main(progname, args, { env, homedir, agcc }) {
204
206
  return ret;
205
207
  }
206
208
 
207
- const clearChainSends = () => {
209
+ const clearChainSends = async () => {
210
+ // Cosmos should have blocked before calling commit, but wait just in case
211
+ await stateSyncExport?.exporter?.onStarted().catch(() => {});
212
+
208
213
  const chainSends = savedChainSends;
209
214
  savedChainSends = [];
210
215
  return chainSends;
@@ -232,37 +237,81 @@ export default async function main(progname, args, { env, homedir, agcc }) {
232
237
  }
233
238
  }
234
239
 
240
+ /** @type {((obj: object) => void) | undefined} */
241
+ let writeSlogObject;
242
+
235
243
  // this storagePort changes for every single message. We define it out here
236
244
  // so the 'externalStorage' object can close over the single mutable
237
245
  // instance, and we update the 'portNums.storage' value each time toSwingSet is called
238
246
  async function launchAndInitializeSwingSet(bootMsg) {
247
+ const sendToChain = msg => chainSend(portNums.storage, msg);
239
248
  // this object is used to store the mailbox state.
240
- const mailboxStorage = makeChainStorage(
241
- msg => chainSend(portNums.storage, msg),
242
- `${STORAGE_PATH.MAILBOX}.`,
243
- {
244
- fromChainShape: data => {
245
- const ack = toNumber(data.ack);
246
- const outbox = data.outbox.map(([seq, msg]) => [toNumber(seq), msg]);
247
- return importMailbox({ outbox, ack });
248
- },
249
- toChainShape: exportMailbox,
250
- },
251
- );
252
- const actionQueue = makeQueue(
253
- makeChainStorage(
254
- msg => chainSend(portNums.storage, msg),
255
- `${STORAGE_PATH.ACTION_QUEUE}.`,
249
+ const fromBridgeMailbox = data => {
250
+ const ack = toNumber(data.ack);
251
+ const outbox = data.outbox.map(([seq, msg]) => [toNumber(seq), msg]);
252
+ return importMailbox({ outbox, ack });
253
+ };
254
+ const mailboxStorage = makeReadCachingStorage(
255
+ makePrefixedBridgeStorage(
256
+ sendToChain,
257
+ `${STORAGE_PATH.MAILBOX}.`,
258
+ 'legacySet',
259
+ val => fromBridgeMailbox(JSON.parse(val)),
260
+ val => stringify(exportMailbox(val)),
256
261
  ),
257
262
  );
258
- function setActivityhash(activityhash) {
259
- const msg = stringify({
260
- method: 'set',
261
- key: STORAGE_PATH.ACTIVITYHASH,
262
- value: activityhash,
263
+ const makeQueueStorage = queuePath => {
264
+ const { kvStore, commit, abort } = makeBufferedStorage(
265
+ makePrefixedBridgeStorage(
266
+ sendToChain,
267
+ `${queuePath}.`,
268
+ 'setWithoutNotify',
269
+ x => x,
270
+ x => x,
271
+ ),
272
+ );
273
+ return harden({ ...kvStore, commit, abort });
274
+ };
275
+ const actionQueueStorage = makeQueueStorage(STORAGE_PATH.ACTION_QUEUE);
276
+ const highPriorityQueueStorage = makeQueueStorage(
277
+ STORAGE_PATH.HIGH_PRIORITY_QUEUE,
278
+ );
279
+ /**
280
+ * Callback invoked during SwingSet execution when new "export data" is
281
+ * generated by swingStore to be saved in the host's verified DB. In our
282
+ * case, we publish these entries in vstorage under a dedicated prefix.
283
+ * This effectively shadows the "export data" of the swingStore so that
284
+ * processes like state-sync can generate a verified "root of trust" to
285
+ * restore SwingSet state.
286
+ *
287
+ * @param {ReadonlyArray<[path: string, value?: string | null]>} updates
288
+ */
289
+ const swingStoreExportCallback = async updates => {
290
+ // Allow I/O to proceed first
291
+ await waitUntilQuiescent();
292
+
293
+ const entries = updates.map(([key, value]) => {
294
+ if (typeof key !== 'string') {
295
+ throw Fail`Unexpected swingStore exported key ${q(key)}`;
296
+ }
297
+ const path = `${STORAGE_PATH.SWING_STORE}.${key}`;
298
+ if (value == null) {
299
+ return [path];
300
+ }
301
+ if (typeof value !== 'string') {
302
+ throw Fail`Unexpected ${typeof value} value for swingStore exported key ${q(
303
+ key,
304
+ )}`;
305
+ }
306
+ return [path, value];
263
307
  });
264
- chainSend(portNums.storage, msg);
265
- }
308
+ sendToChain(
309
+ stringify({
310
+ method: 'setWithoutNotify',
311
+ args: entries,
312
+ }),
313
+ );
314
+ };
266
315
  function doOutboundBridge(dstID, msg) {
267
316
  const portNum = portNums[dstID];
268
317
  if (portNum === undefined) {
@@ -278,7 +327,7 @@ export default async function main(progname, args, { env, homedir, agcc }) {
278
327
  try {
279
328
  return JSON.parse(respStr);
280
329
  } catch (e) {
281
- assert.fail(X`cannot JSON.parse(${JSON.stringify(respStr)}): ${e}`);
330
+ throw Fail`cannot JSON.parse(${JSON.stringify(respStr)}): ${e}`;
282
331
  }
283
332
  }
284
333
 
@@ -289,27 +338,32 @@ export default async function main(progname, args, { env, homedir, agcc }) {
289
338
  const makeInstallationPublisher = () => {
290
339
  const installationStorageNode = makeChainStorageRoot(
291
340
  toStorage,
292
- 'swingset',
293
341
  STORAGE_PATH.BUNDLES,
294
342
  { sequence: true },
295
343
  );
296
- const marshaller = makeMarshal();
297
- const { publisher, subscriber } = makePublishKit();
298
- makeStoredSubscriber(subscriber, installationStorageNode, marshaller);
344
+ const marshaller = makeMarshal(undefined, undefined, {
345
+ serializeBodyFormat: 'smallcaps',
346
+ });
347
+ const publish = makeSerializeToStorage(
348
+ installationStorageNode,
349
+ marshaller,
350
+ );
351
+ const publisher = harden({ publish });
299
352
  return publisher;
300
353
  };
301
354
 
302
355
  const argv = {
303
- ROLE: 'chain',
304
356
  bootMsg,
305
357
  };
306
- const vatconfig = new URL(
307
- await importMetaResolve(
308
- env.CHAIN_BOOTSTRAP_VAT_CONFIG ||
309
- argv.bootMsg.params.bootstrap_vat_config,
310
- import.meta.url,
311
- ),
312
- ).pathname;
358
+ const vatHref = await importMetaResolve(
359
+ env.CHAIN_BOOTSTRAP_VAT_CONFIG ||
360
+ argv.bootMsg.params.bootstrap_vat_config,
361
+ import.meta.url,
362
+ );
363
+ const vatconfig = new URL(vatHref).pathname;
364
+
365
+ // Delay makeShutdown to override the golang interrupts
366
+ const { registerShutdown } = makeShutdown();
313
367
 
314
368
  const { metricsProvider } = getTelemetryProviders({
315
369
  console,
@@ -317,37 +371,18 @@ export default async function main(progname, args, { env, homedir, agcc }) {
317
371
  serviceName: TELEMETRY_SERVICE_NAME,
318
372
  });
319
373
 
320
- const {
321
- LMDB_MAP_SIZE,
322
- SWING_STORE_TRACE,
323
- XSNAP_KEEP_SNAPSHOTS,
324
- NODE_HEAP_SNAPSHOTS = -1,
325
- } = env;
374
+ const { XSNAP_KEEP_SNAPSHOTS, NODE_HEAP_SNAPSHOTS = -1 } = env;
326
375
  const slogSender = await makeSlogSender({
327
376
  stateDir: stateDBDir,
328
377
  env,
329
378
  serviceName: TELEMETRY_SERVICE_NAME,
330
379
  });
331
380
 
332
- const mapSize = (LMDB_MAP_SIZE && parseInt(LMDB_MAP_SIZE, 10)) || undefined;
333
-
334
- const defaultTraceFile = path.resolve(stateDBDir, 'store-trace.log');
335
- let swingStoreTraceFile;
336
- switch (SWING_STORE_TRACE) {
337
- case '0':
338
- case 'false':
339
- break;
340
- case '1':
341
- case 'true':
342
- swingStoreTraceFile = defaultTraceFile;
343
- break;
344
- default:
345
- if (SWING_STORE_TRACE) {
346
- swingStoreTraceFile = path.resolve(SWING_STORE_TRACE);
347
- } else if (getFlagValue('trace-store')) {
348
- swingStoreTraceFile = defaultTraceFile;
349
- }
350
- }
381
+ const swingStoreTraceFile = processValue.getPath({
382
+ envName: 'SWING_STORE_TRACE',
383
+ flagName: 'trace-store',
384
+ trueValue: pathResolve(stateDBDir, 'store-trace.log'),
385
+ });
351
386
 
352
387
  const keepSnapshots =
353
388
  XSNAP_KEEP_SNAPSHOTS === '1' || XSNAP_KEEP_SNAPSHOTS === 'true';
@@ -356,67 +391,73 @@ export default async function main(progname, args, { env, homedir, agcc }) {
356
391
 
357
392
  let lastCommitTime = 0;
358
393
  let commitCallsSinceLastSnapshot = NaN;
359
- const snapshotBaseDir = path.resolve(stateDBDir, 'node-heap-snapshots');
394
+ const snapshotBaseDir = pathResolve(stateDBDir, 'node-heap-snapshots');
360
395
 
361
396
  if (nodeHeapSnapshots >= 0) {
362
397
  fs.mkdirSync(snapshotBaseDir, { recursive: true });
363
398
  }
364
399
 
365
400
  const afterCommitCallback = async () => {
401
+ const slogSenderFlushed = tryFlushSlogSender(slogSender, {
402
+ env,
403
+ log: console.warn,
404
+ });
405
+
366
406
  // delay until all current promise reactions are drained so we can be sure
367
407
  // that the commit-block reply has been sent to agcc through replier.resolve
368
- await waitUntilQuiescent();
408
+ await Promise.all([slogSenderFlushed, waitUntilQuiescent()]);
369
409
 
370
- let heapSnapshot;
371
- let heapSnapshotTime;
372
-
373
- const t0 = performance.now();
374
- engineGC();
375
- const t1 = performance.now();
376
- const memoryUsage = process.memoryUsage();
377
- const t2 = performance.now();
378
- const heapStats = v8.getHeapStatistics();
379
- const t3 = performance.now();
380
-
381
- commitCallsSinceLastSnapshot += 1;
382
- if (
383
- (nodeHeapSnapshots >= 0 && t0 - lastCommitTime > 30 * 1000) ||
384
- (nodeHeapSnapshots > 0 &&
385
- !(nodeHeapSnapshots > commitCallsSinceLastSnapshot))
386
- ) {
387
- commitCallsSinceLastSnapshot = 0;
388
- heapSnapshot = `Heap-${process.pid}-${Date.now()}.heapsnapshot`;
389
- const snapshotPath = path.resolve(snapshotBaseDir, heapSnapshot);
390
- v8.writeHeapSnapshot(snapshotPath);
391
- heapSnapshotTime = performance.now() - t3;
410
+ try {
411
+ let heapSnapshot;
412
+ let heapSnapshotTime;
413
+
414
+ const t0 = performance.now();
415
+ engineGC();
416
+ const t1 = performance.now();
417
+ const memoryUsage = process.memoryUsage();
418
+ const t2 = performance.now();
419
+ const heapStats = v8.getHeapStatistics();
420
+ const t3 = performance.now();
421
+
422
+ commitCallsSinceLastSnapshot += 1;
423
+ if (
424
+ (nodeHeapSnapshots >= 0 && t0 - lastCommitTime > 30 * 1000) ||
425
+ (nodeHeapSnapshots > 0 &&
426
+ !(nodeHeapSnapshots > commitCallsSinceLastSnapshot))
427
+ ) {
428
+ commitCallsSinceLastSnapshot = 0;
429
+ heapSnapshot = `Heap-${process.pid}-${Date.now()}.heapsnapshot`;
430
+ const snapshotPath = pathResolve(snapshotBaseDir, heapSnapshot);
431
+ v8.writeHeapSnapshot(snapshotPath);
432
+ heapSnapshotTime = performance.now() - t3;
433
+ }
434
+ lastCommitTime = t0;
435
+
436
+ return {
437
+ memoryUsage,
438
+ heapStats,
439
+ heapSnapshot,
440
+ statsTime: {
441
+ forcedGc: t1 - t0,
442
+ memoryUsage: t2 - t1,
443
+ heapStats: t3 - t2,
444
+ heapSnapshot: heapSnapshotTime,
445
+ },
446
+ };
447
+ } catch (err) {
448
+ console.warn('Failed to gather memory stats', err);
449
+ return {};
392
450
  }
393
- lastCommitTime = t0;
394
-
395
- await Promise.resolve(slogSender?.forceFlush?.()).catch(err => {
396
- console.warn('Failed to flush slog sender', err);
397
- });
398
-
399
- return {
400
- memoryUsage,
401
- heapStats,
402
- heapSnapshot,
403
- statsTime: {
404
- forcedGc: t1 - t0,
405
- memoryUsage: t2 - t1,
406
- heapStats: t3 - t2,
407
- heapSnapshot: heapSnapshotTime,
408
- },
409
- };
410
451
  };
411
452
 
412
453
  const s = await launch({
413
- actionQueue,
454
+ actionQueueStorage,
455
+ highPriorityQueueStorage,
414
456
  kernelStateDBDir: stateDBDir,
415
457
  makeInstallationPublisher,
416
458
  mailboxStorage,
417
459
  clearChainSends,
418
460
  replayChainSends,
419
- setActivityhash,
420
461
  bridgeOutbound: doOutboundBridge,
421
462
  vatconfig,
422
463
  argv,
@@ -424,18 +465,151 @@ export default async function main(progname, args, { env, homedir, agcc }) {
424
465
  verboseBlocks: true,
425
466
  metricsProvider,
426
467
  slogSender,
427
- mapSize,
468
+ swingStoreExportCallback,
428
469
  swingStoreTraceFile,
429
470
  keepSnapshots,
430
471
  afterCommitCallback,
431
472
  });
432
473
 
433
- savedChainSends = s.savedChainSends;
474
+ const { blockingSend, shutdown } = s;
475
+ ({ writeSlogObject, savedChainSends } = s);
476
+
477
+ let pendingBlockingSend = Promise.resolve();
434
478
 
435
- return s.blockingSend;
479
+ registerShutdown(async interrupted =>
480
+ Promise.all([
481
+ interrupted && pendingBlockingSend.then(shutdown),
482
+ discardStateSyncExport(),
483
+ ]).then(() => {}),
484
+ );
485
+
486
+ return async action => {
487
+ const result = blockingSend(action);
488
+ pendingBlockingSend = Promise.resolve(result).then(
489
+ () => {},
490
+ () => {},
491
+ );
492
+ return result;
493
+ };
436
494
  }
437
495
 
496
+ async function handleCosmosSnapshot(blockHeight, request, requestArgs) {
497
+ switch (request) {
498
+ case 'restore': {
499
+ const exportDir = requestArgs[0];
500
+ if (typeof exportDir !== 'string') {
501
+ throw Fail`Invalid exportDir argument ${q(exportDir)}`;
502
+ }
503
+ console.info(
504
+ 'Restoring SwingSet state from snapshot at block height',
505
+ blockHeight,
506
+ );
507
+ return performStateSyncImport(
508
+ { exportDir, stateDir: stateDBDir, blockHeight },
509
+ { fs: { ...fs, ...fsPromises }, pathResolve, log: null },
510
+ );
511
+ }
512
+ case 'initiate': {
513
+ !stateSyncExport ||
514
+ Fail`Snapshot already in progress for ${stateSyncExport.blockHeight}`;
515
+
516
+ const exportData =
517
+ /** @type {Required<NonNullable<typeof stateSyncExport>>} */ ({
518
+ blockHeight,
519
+ });
520
+ stateSyncExport = exportData;
521
+
522
+ // eslint-disable-next-line @jessie.js/no-nested-await
523
+ await new Promise((resolve, reject) => {
524
+ tmpfs.dir(
525
+ {
526
+ prefix: `agd-state-sync-${blockHeight}-`,
527
+ unsafeCleanup: true,
528
+ },
529
+ (err, exportDir, cleanup) => {
530
+ if (err) {
531
+ reject(err);
532
+ return;
533
+ }
534
+ exportData.exportDir = exportDir;
535
+ /** @type {Promise<void> | undefined} */
536
+ let cleanupResult;
537
+ exportData.cleanup = async () => {
538
+ cleanupResult ||= new Promise(cleanupDone => {
539
+ // If the exporter is still the same, then the retriever
540
+ // is in charge of cleanups
541
+ if (stateSyncExport !== exportData) {
542
+ // @ts-expect-error wrong type definitions
543
+ cleanup(cleanupDone);
544
+ } else {
545
+ console.warn('unexpected call of state-sync cleanup');
546
+ cleanupDone();
547
+ }
548
+ });
549
+ await cleanupResult;
550
+ };
551
+ resolve(null);
552
+ },
553
+ );
554
+ });
555
+
556
+ console.info(
557
+ 'Initiating SwingSet state snapshot at block height',
558
+ blockHeight,
559
+ );
560
+ exportData.exporter = spawnSwingStoreExport(
561
+ {
562
+ stateDir: stateDBDir,
563
+ exportDir: exportData.exportDir,
564
+ blockHeight,
565
+ },
566
+ {
567
+ fork,
568
+ },
569
+ );
570
+
571
+ exportData.exporter.onDone().catch(() => {
572
+ if (exportData === stateSyncExport) {
573
+ stateSyncExport = undefined;
574
+ }
575
+ return exportData.cleanup();
576
+ });
577
+
578
+ return exportData.exporter.onStarted().catch(err => {
579
+ console.warn(
580
+ `State-sync export failed for block ${blockHeight}`,
581
+ err,
582
+ );
583
+ throw err;
584
+ });
585
+ }
586
+ case 'discard': {
587
+ return discardStateSyncExport();
588
+ }
589
+ case 'retrieve': {
590
+ const exportData = stateSyncExport;
591
+ if (!exportData || !exportData.exporter) {
592
+ throw Fail`No snapshot in progress`;
593
+ }
594
+
595
+ return exportData.exporter.onDone().then(() => {
596
+ if (exportData === stateSyncExport) {
597
+ // Don't cleanup, cosmos is now in charge
598
+ stateSyncExport = undefined;
599
+ return exportData.exportDir;
600
+ } else {
601
+ throw Fail`Snapshot was discarded`;
602
+ }
603
+ });
604
+ }
605
+ default:
606
+ throw Fail`Unknown cosmos snapshot request ${request}`;
607
+ }
608
+ }
609
+
610
+ /** @type {Awaited<ReturnType<typeof launch>>['blockingSend'] | undefined} */
438
611
  let blockingSend;
612
+
439
613
  async function toSwingSet(action, _replier) {
440
614
  // console.log(`toSwingSet`, action);
441
615
  if (action.vibcPort) {
@@ -456,16 +630,51 @@ export default async function main(progname, args, { env, homedir, agcc }) {
456
630
  portNums.lien = action.lienPort;
457
631
  }
458
632
 
459
- // Ensure that initialization has completed.
460
- if (!blockingSend) {
461
- blockingSend = await launchAndInitializeSwingSet(action);
633
+ // Snapshot actions are specific to cosmos chains and handled here
634
+ if (action.type === ActionType.COSMOS_SNAPSHOT) {
635
+ const { blockHeight, request, args: requestArgs } = action;
636
+ writeSlogObject?.({
637
+ type: 'cosmic-swingset-snapshot-start',
638
+ blockHeight,
639
+ request,
640
+ args: requestArgs,
641
+ });
642
+
643
+ const resultP = handleCosmosSnapshot(blockHeight, request, requestArgs);
644
+
645
+ resultP.then(
646
+ result => {
647
+ writeSlogObject?.({
648
+ type: 'cosmic-swingset-snapshot-finish',
649
+ blockHeight,
650
+ request,
651
+ args: requestArgs,
652
+ result,
653
+ });
654
+ },
655
+ error => {
656
+ writeSlogObject?.({
657
+ type: 'cosmic-swingset-snapshot-finish',
658
+ blockHeight,
659
+ request,
660
+ args: requestArgs,
661
+ error,
662
+ });
663
+ },
664
+ );
665
+
666
+ return resultP;
462
667
  }
463
668
 
669
+ // Ensure that initialization has completed.
670
+ blockingSend = await (blockingSend || launchAndInitializeSwingSet(action));
671
+
464
672
  if (action.type === AG_COSMOS_INIT) {
465
673
  // console.error('got AG_COSMOS_INIT', action);
466
674
  return true;
467
675
  }
468
676
 
677
+ // Block related actions are processed by `blockingSend`
469
678
  return blockingSend(action);
470
679
  }
471
680
  }