@agoric/telemetry 0.6.3-other-dev-3eb1a1d.0 → 0.6.3-other-dev-fbe72e7.0.fbe72e7

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.
@@ -1,32 +1,34 @@
1
1
  /* eslint-env node */
2
+ /**
3
+ * @file Run as a child process of {@link ./slog-sender-pipe.js} to isolate an
4
+ * aggregate slog sender (@see {@link ./make-slog-sender.js}). Communicates
5
+ * with its parent via Node.js IPC with advanced (structured clone)
6
+ * serialization.
7
+ * https://nodejs.org/docs/latest/api/child_process.html#advanced-serialization
8
+ */
9
+
2
10
  import '@endo/init';
3
11
 
4
12
  import anylogger from 'anylogger';
13
+ import { Fail } from '@endo/errors';
5
14
  import { makeShutdown } from '@agoric/internal/src/node/shutdown.js';
6
15
 
7
16
  import { makeSlogSender } from './make-slog-sender.js';
8
17
 
9
18
  const logger = anylogger('slog-sender-pipe-entrypoint');
10
19
 
11
- /** @type {(msg: import('./slog-sender-pipe.js').SlogSenderPipeWaitReplies) => void} */
20
+ /** @type {(msg: import('./slog-sender-pipe.js').PipeAPIReply) => void} */
12
21
  const send = Function.prototype.bind.call(process.send, process);
13
22
 
14
23
  /**
15
- * @typedef {object} InitMessage
16
- * @property {'init'} type
17
- * @property {import('./index.js').MakeSlogSenderOptions} options
18
- */
19
- /**
20
- * @typedef {object} SendMessage
21
- * @property {'send'} type
22
- * @property {object} obj
24
+ * @typedef {{type: 'init', options: import('./index.js').MakeSlogSenderOptions }} InitMessage
25
+ * @typedef {{type: 'flush' }} FlushMessage
26
+ * @typedef {{type: 'send', obj: Record<string, unknown> }} SendMessage
27
+ *
28
+ * @typedef {InitMessage | FlushMessage} PipeAPIResponsefulMessage
29
+ * @typedef {SendMessage} PipeAPIResponselessMessage
30
+ * @typedef {PipeAPIResponsefulMessage | PipeAPIResponselessMessage} PipeAPIMessage
23
31
  */
24
- /**
25
- * @typedef {object} FlushMessage
26
- * @property {'flush'} type
27
- */
28
- /** @typedef {InitMessage | FlushMessage} SlogSenderPipeWaitMessages */
29
- /** @typedef {SlogSenderPipeWaitMessages | SendMessage } SlogSenderPipeMessages */
30
32
 
31
33
  const main = async () => {
32
34
  /** @type {import('./index.js').SlogSender | undefined} */
@@ -44,9 +46,7 @@ const main = async () => {
44
46
 
45
47
  /** @param {import('./index.js').MakeSlogSenderOptions} opts */
46
48
  const init = async ({ env, ...otherOpts } = {}) => {
47
- if (slogSender) {
48
- assert.fail('Already initialized');
49
- }
49
+ !slogSender || Fail`Already initialized`;
50
50
 
51
51
  slogSender = await makeSlogSender({
52
52
  ...otherOpts,
@@ -57,9 +57,7 @@ const main = async () => {
57
57
  };
58
58
 
59
59
  const flush = async () => {
60
- if (!slogSender) {
61
- assert.fail('No sender available');
62
- }
60
+ if (!slogSender) throw Fail`No sender available`;
63
61
 
64
62
  await slogSender.forceFlush?.();
65
63
  };
@@ -77,56 +75,55 @@ const main = async () => {
77
75
  return AggregateError(sendErrors.splice(0));
78
76
  };
79
77
 
80
- process.on(
81
- 'message',
82
- /** @param {SlogSenderPipeMessages} msg */ msg => {
83
- if (!msg || typeof msg !== 'object') {
84
- logger.warn('received invalid message', msg);
85
- return;
86
- }
78
+ /** @param {PipeAPIMessage} msg */
79
+ const onMessage = msg => {
80
+ if (!msg || typeof msg !== 'object') {
81
+ logger.warn('Received invalid message', msg);
82
+ return;
83
+ }
87
84
 
88
- switch (msg.type) {
89
- case 'init': {
90
- void init(msg.options).then(
91
- hasSender => {
92
- send({ type: 'initReply', hasSender });
93
- },
94
- error => {
95
- send({ type: 'initReply', hasSender: false, error });
96
- },
97
- );
98
- break;
99
- }
100
- case 'flush': {
101
- void flush().then(
102
- () => {
103
- send({ type: 'flushReply', error: generateFlushError() });
104
- },
105
- error => {
106
- send({ type: 'flushReply', error: generateFlushError(error) });
107
- },
108
- );
109
- break;
110
- }
111
- case 'send': {
112
- if (!slogSender) {
113
- logger.warn('received send with no sender available');
114
- } else {
115
- try {
116
- slogSender(msg.obj);
117
- } catch (e) {
118
- sendErrors.push(e);
119
- }
85
+ switch (msg.type) {
86
+ case 'init': {
87
+ void init(msg.options).then(
88
+ hasSender => {
89
+ send({ type: 'initReply', hasSender });
90
+ },
91
+ error => {
92
+ send({ type: 'initReply', hasSender: false, error });
93
+ },
94
+ );
95
+ break;
96
+ }
97
+ case 'flush': {
98
+ void flush().then(
99
+ () => {
100
+ send({ type: 'flushReply', error: generateFlushError() });
101
+ },
102
+ error => {
103
+ send({ type: 'flushReply', error: generateFlushError(error) });
104
+ },
105
+ );
106
+ break;
107
+ }
108
+ case 'send': {
109
+ if (!slogSender) {
110
+ logger.warn('Received send with no sender available');
111
+ } else {
112
+ try {
113
+ slogSender(harden(msg.obj));
114
+ } catch (e) {
115
+ sendErrors.push(e);
120
116
  }
121
- break;
122
- }
123
- default: {
124
- // @ts-expect-error exhaustive type check
125
- logger.warn('received unknown message type', msg.type);
126
117
  }
118
+ break;
119
+ }
120
+ default: {
121
+ // @ts-expect-error exhaustive type check
122
+ logger.warn('Received unknown message type', msg.type);
127
123
  }
128
- },
129
- );
124
+ }
125
+ };
126
+ process.on('message', onMessage);
130
127
  };
131
128
 
132
129
  process.exitCode = 1;
@@ -1,7 +1,18 @@
1
+ /**
2
+ * @file Export a `makeSlogSender` that spawns a
3
+ * {@link ./slog-sender-pipe-entrypoint.js} child process to which it forwards
4
+ * all slog entries via Node.js IPC with advanced (structured clone)
5
+ * serialization.
6
+ * https://nodejs.org/docs/latest/api/child_process.html#advanced-serialization
7
+ */
8
+
1
9
  import { fork } from 'child_process';
2
10
  import path from 'path';
11
+ import { promisify } from 'util';
3
12
  import anylogger from 'anylogger';
4
13
 
14
+ import { q, Fail } from '@endo/errors';
15
+ import { makePromiseKit } from '@endo/promise-kit';
5
16
  import { makeQueue } from '@endo/stream';
6
17
 
7
18
  import { makeShutdown } from '@agoric/internal/src/node/shutdown.js';
@@ -10,6 +21,8 @@ const dirname = path.dirname(new URL(import.meta.url).pathname);
10
21
 
11
22
  const logger = anylogger('slog-sender-pipe');
12
23
 
24
+ const sink = () => {};
25
+
13
26
  /**
14
27
  * @template {any[]} T
15
28
  * @template R
@@ -23,168 +36,131 @@ const withMutex = operation => {
23
36
  return async (...args) => {
24
37
  await mutex.get();
25
38
  const result = operation(...args);
26
- mutex.put(
27
- result.then(
28
- () => {},
29
- () => {},
30
- ),
31
- );
39
+ mutex.put(result.then(sink, sink));
32
40
  return result;
33
41
  };
34
42
  };
35
43
 
36
44
  /**
37
- * @typedef {object} SlogSenderInitReply
38
- * @property {'initReply'} type
39
- * @property {boolean} hasSender
40
- * @property {Error} [error]
45
+ * @template [P=unknown]
46
+ * @typedef {{ type: string, error?: Error } & P} PipeReply
41
47
  */
48
+
42
49
  /**
43
- * @typedef {object} SlogSenderFlushReply
44
- * @property {'flushReply'} type
45
- * @property {Error} [error]
50
+ * @typedef {{
51
+ * init: {
52
+ * message: import('./slog-sender-pipe-entrypoint.js').InitMessage;
53
+ * reply: PipeReply<{ hasSender: boolean }>;
54
+ * };
55
+ * flush: {
56
+ * message: import('./slog-sender-pipe-entrypoint.js').FlushMessage;
57
+ * reply: PipeReply<{}>;
58
+ * };
59
+ * }} SlogSenderPipeAPI
60
+ *
61
+ * @typedef {keyof SlogSenderPipeAPI} PipeAPICommand
62
+ * @typedef {SlogSenderPipeAPI[PipeAPICommand]["reply"]} PipeAPIReply
46
63
  */
47
- /** @typedef {SlogSenderInitReply | SlogSenderFlushReply} SlogSenderPipeWaitReplies */
48
64
 
49
- /** @param {import('.').MakeSlogSenderOptions} opts */
50
- export const makeSlogSender = async opts => {
65
+ /** @param {import('.').MakeSlogSenderOptions} options */
66
+ export const makeSlogSender = async options => {
67
+ const { env = {} } = options;
51
68
  const { registerShutdown } = makeShutdown();
69
+
52
70
  const cp = fork(path.join(dirname, 'slog-sender-pipe-entrypoint.js'), [], {
53
- stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
71
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
54
72
  serialization: 'advanced',
73
+ env,
55
74
  });
56
75
  // logger.log('done fork');
57
76
 
58
- const pipeSend = withMutex(
59
- /**
60
- * @template {{type: string}} T
61
- * @param {T} msg
62
- */
63
- msg =>
64
- /** @type {Promise<void>} */ (
65
- new Promise((resolve, reject) => {
66
- cp.send(msg, err => {
67
- if (err) {
68
- reject(err);
69
- } else {
70
- resolve();
71
- }
72
- });
73
- })
74
- ),
75
- );
77
+ const exitKit = makePromiseKit();
78
+ cp.on('error', error => {
79
+ // An exit event *might* be coming, so wait a tick.
80
+ setImmediate(() => exitKit.resolve({ error }));
81
+ });
82
+ cp.on('exit', (exitCode, signal) => {
83
+ exitKit.resolve({ exitCode, signal });
84
+ });
85
+
86
+ /** @type {(msg: Record<string, unknown> & {type: string}) => Promise<void>} */
87
+ const rawSend = promisify(cp.send.bind(cp));
88
+ const pipeSend = withMutex(rawSend);
76
89
 
77
- /**
78
- * @typedef {{
79
- * init: {
80
- * message: import('./slog-sender-pipe-entrypoint.js').InitMessage;
81
- * reply: SlogSenderInitReply;
82
- * };
83
- * flush: {
84
- * message: import('./slog-sender-pipe-entrypoint.js').FlushMessage;
85
- * reply: SlogSenderFlushReply;
86
- * };
87
- * }} SlogSenderWaitMessagesAndReplies
88
- */
89
-
90
- /** @typedef {keyof SlogSenderWaitMessagesAndReplies} SendWaitCommands */
91
- /**
92
- * @template {SlogSenderPipeWaitReplies} T
93
- * @typedef {Omit<T, 'type' | 'error'>} ReplyPayload
94
- */
95
-
96
- /** @type {import('@endo/stream').AsyncQueue<SlogSenderPipeWaitReplies>} */
90
+ /** @type {import('@endo/stream').AsyncQueue<PipeAPIReply>} */
97
91
  const sendWaitQueue = makeQueue();
98
- /** @type {SendWaitCommands | undefined} */
92
+ /** @type {PipeAPICommand | undefined} */
99
93
  let sendWaitType;
100
94
 
101
95
  const sendWaitReply = withMutex(
102
96
  /**
103
- * @template {SendWaitCommands} T
97
+ * @template {PipeAPICommand} T
104
98
  * @param {T} type
105
- * @param {Omit<SlogSenderWaitMessagesAndReplies[T]["message"], 'type'>} payload
106
- * @returns {Promise<ReplyPayload<SlogSenderWaitMessagesAndReplies[T]["reply"]>>}
99
+ * @param {Omit<SlogSenderPipeAPI[T]["message"], 'type'>} payload
100
+ * @returns {Promise<Omit<SlogSenderPipeAPI[T]["reply"], keyof PipeReply>>}
107
101
  */
108
102
  async (type, payload) => {
109
- !sendWaitType || assert.fail('Invalid mutex state');
103
+ !sendWaitType || Fail`Invalid mutex state`;
110
104
 
111
105
  const msg = { ...payload, type };
112
106
 
113
107
  sendWaitType = type;
114
- return pipeSend(msg)
115
- .then(async () => sendWaitQueue.get())
116
- .then(
117
- /** @param {SlogSenderWaitMessagesAndReplies[T]["reply"]} reply */ ({
118
- type: replyType,
119
- error,
120
- ...rest
121
- }) => {
122
- replyType === `${type}Reply` ||
123
- assert.fail(`Unexpected reply ${replyType}`);
124
- if (error) {
125
- throw error;
126
- }
127
- return rest;
128
- },
129
- )
130
- .finally(() => {
131
- sendWaitType = undefined;
132
- });
133
- },
134
- );
135
-
136
- cp.on(
137
- 'message',
138
- /** @param { SlogSenderPipeWaitReplies } msg */
139
- msg => {
140
- // logger.log('received', msg);
141
- if (
142
- !msg ||
143
- typeof msg !== 'object' ||
144
- msg.type !== `${sendWaitType}Reply`
145
- ) {
146
- logger.warn('Received unexpected message', msg);
147
- return;
108
+ await null;
109
+ try {
110
+ await pipeSend(msg);
111
+ /** @type {SlogSenderPipeAPI[T]["reply"]} */
112
+ const reply = await sendWaitQueue.get();
113
+ const { type: replyType, error, ...rest } = reply;
114
+ replyType === `${type}Reply` ||
115
+ Fail`Unexpected reply type ${q(replyType)}`;
116
+ if (error) throw error;
117
+ return rest;
118
+ } finally {
119
+ sendWaitType = undefined;
148
120
  }
149
-
150
- sendWaitQueue.put(msg);
151
121
  },
152
122
  );
153
123
 
154
- const flush = async () => sendWaitReply('flush', {});
155
- /** @param {import('./index.js').MakeSlogSenderOptions} options */
156
- const init = async options => sendWaitReply('init', { options });
124
+ /** @param {PipeReply} msg */
125
+ const onMessage = msg => {
126
+ // logger.log('received', msg);
127
+ if (!msg || msg.type !== `${sendWaitType}Reply`) {
128
+ logger.warn('Received unexpected message', msg);
129
+ return;
130
+ }
131
+
132
+ sendWaitQueue.put(msg);
133
+ };
134
+ cp.on('message', onMessage);
157
135
 
158
- const send = obj => {
159
- void pipeSend({ type: 'send', obj }).catch(() => {});
136
+ const flush = async () => {
137
+ await sendWaitReply('flush', {});
160
138
  };
161
139
 
162
140
  const shutdown = async () => {
163
141
  // logger.log('shutdown');
164
- if (!cp.connected) {
165
- return;
166
- }
142
+ if (!cp.connected) return;
167
143
 
168
144
  await flush();
169
145
  cp.disconnect();
146
+ await exitKit.promise;
170
147
  };
171
148
  registerShutdown(shutdown);
172
149
 
173
- const { hasSender } = await init(opts).catch(err => {
150
+ const { hasSender } = await sendWaitReply('init', { options }).catch(err => {
174
151
  cp.disconnect();
175
152
  throw err;
176
153
  });
177
-
178
154
  if (!hasSender) {
179
155
  cp.disconnect();
180
156
  return undefined;
181
157
  }
182
158
 
183
- const slogSender = send;
159
+ const slogSender = obj => {
160
+ void pipeSend({ type: 'send', obj }).catch(sink);
161
+ };
184
162
  return Object.assign(slogSender, {
185
- forceFlush: async () => {
186
- await flush();
187
- },
163
+ forceFlush: flush,
188
164
  shutdown,
189
165
  usesJsonObject: false,
190
166
  });
@@ -12,27 +12,48 @@ const bufferTests = test.macro(
12
12
  /**
13
13
  *
14
14
  * @param {*} t
15
- * @param {{makeBuffer: Function}} input
15
+ * @param {{makeBuffer: typeof makeSimpleCircularBuffer}} input
16
16
  */
17
17
  async (t, input) => {
18
18
  const BUFFER_SIZE = 512;
19
19
 
20
- const { name: tmpFile, removeCallback } = tmp.fileSync();
21
- const { readCircBuf, writeCircBuf } = await input.makeBuffer({
20
+ const { name: tmpFile, removeCallback } = tmp.fileSync({
21
+ discardDescriptor: true,
22
+ });
23
+ t.teardown(removeCallback);
24
+ const { fileHandle, readCircBuf, writeCircBuf } = await input.makeBuffer({
22
25
  circularBufferSize: BUFFER_SIZE,
23
26
  circularBufferFilename: tmpFile,
24
27
  });
25
- const slogSender = makeSlogSenderFromBuffer({ writeCircBuf });
28
+ const realSlogSender = makeSlogSenderFromBuffer({
29
+ fileHandle,
30
+ writeCircBuf,
31
+ });
32
+ let wasShutdown = false;
33
+ const shutdown = () => {
34
+ if (wasShutdown) return;
35
+ wasShutdown = true;
36
+
37
+ return realSlogSender.shutdown();
38
+ };
39
+ t.teardown(shutdown);
40
+ // To verify lack of attempted mutation by the consumer, send only hardened
41
+ // entries.
42
+ /** @type {typeof realSlogSender} */
43
+ const slogSender = Object.assign(
44
+ (obj, serialized) => realSlogSender(harden(obj), serialized),
45
+ realSlogSender,
46
+ );
26
47
  slogSender({ type: 'start' });
27
48
  await slogSender.forceFlush();
28
49
  t.is(fs.readFileSync(tmpFile, { encoding: 'utf8' }).length, BUFFER_SIZE);
29
50
 
30
51
  const len0 = new Uint8Array(BigUint64Array.BYTES_PER_ELEMENT);
31
- const { done: done0 } = readCircBuf(len0);
52
+ const { done: done0 } = await readCircBuf(len0);
32
53
  t.false(done0, 'readCircBuf should not be done');
33
54
  const dv0 = new DataView(len0.buffer);
34
55
  const buf0 = new Uint8Array(Number(dv0.getBigUint64(0)));
35
- const { done: done0b } = readCircBuf(buf0, len0.byteLength);
56
+ const { done: done0b } = await readCircBuf(buf0, len0.byteLength);
36
57
  t.false(done0b, 'readCircBuf should not be done');
37
58
  const buf0Str = new TextDecoder().decode(buf0);
38
59
  t.is(buf0Str, `\n{"type":"start"}`, `start compare failed`);
@@ -51,12 +72,12 @@ const bufferTests = test.macro(
51
72
  let offset = 0;
52
73
  const len1 = new Uint8Array(BigUint64Array.BYTES_PER_ELEMENT);
53
74
  for (let i = 490; i < last; i += 1) {
54
- const { done: done1 } = readCircBuf(len1, offset);
75
+ const { done: done1 } = await readCircBuf(len1, offset);
55
76
  offset += len1.byteLength;
56
77
  t.false(done1, `readCircBuf ${i} should not be done`);
57
78
  const dv1 = new DataView(len1.buffer);
58
79
  const buf1 = new Uint8Array(Number(dv1.getBigUint64(0)));
59
- const { done: done1b } = readCircBuf(buf1, offset);
80
+ const { done: done1b } = await readCircBuf(buf1, offset);
60
81
  offset += buf1.byteLength;
61
82
  t.false(done1b, `readCircBuf ${i} should not be done`);
62
83
  const buf1Str = new TextDecoder().decode(buf1);
@@ -67,14 +88,24 @@ const bufferTests = test.macro(
67
88
  );
68
89
  }
69
90
 
70
- const { done: done2 } = readCircBuf(len1, offset);
91
+ const { done: done2 } = await readCircBuf(len1, offset);
71
92
  t.assert(done2, `readCircBuf ${last} should be done`);
72
93
 
73
94
  slogSender(null, 'PRE-SERIALIZED');
74
95
  await slogSender.forceFlush();
75
96
  t.truthy(fs.readFileSync(tmpFile).includes('PRE-SERIALIZED'));
76
- // console.log({ tmpFile });
77
- removeCallback();
97
+
98
+ slogSender(null, 'PRE_SHUTDOWN');
99
+ const shutdownP = shutdown();
100
+ slogSender(null, 'POST_SHUTDOWN');
101
+ await shutdownP;
102
+ slogSender(null, 'SHUTDOWN_COMPLETED');
103
+
104
+ const finalContent = fs.readFileSync(tmpFile);
105
+
106
+ t.truthy(finalContent.includes('PRE_SHUTDOWN'));
107
+ t.falsy(finalContent.includes('POST_SHUTDOWN'));
108
+ t.falsy(finalContent.includes('SHUTDOWN_COMPLETED'));
78
109
  },
79
110
  );
80
111