@agoric/telemetry 0.6.3-u19.2 → 0.6.3-u21.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.
@@ -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
  });
@@ -1,5 +1,4 @@
1
1
  import fs from 'node:fs';
2
- import { promisify } from 'node:util';
3
2
  import tmp from 'tmp';
4
3
  import { test } from './prepare-test-env-ava.js';
5
4
 
@@ -13,36 +12,48 @@ const bufferTests = test.macro(
13
12
  /**
14
13
  *
15
14
  * @param {*} t
16
- * @param {{makeBuffer: Function}} input
15
+ * @param {{makeBuffer: typeof makeSimpleCircularBuffer}} input
17
16
  */
18
17
  async (t, input) => {
19
18
  const BUFFER_SIZE = 512;
20
19
 
21
- const {
22
- name: tmpFile,
23
- fd,
24
- removeCallback,
25
- } = tmp.fileSync({ detachDescriptor: true });
26
- t.teardown(removeCallback);
27
- const fileHandle = /** @type {import('fs/promises').FileHandle} */ ({
28
- close: promisify(fs.close.bind(fs, fd)),
20
+ const { name: tmpFile, removeCallback } = tmp.fileSync({
21
+ discardDescriptor: true,
29
22
  });
30
- const { readCircBuf, writeCircBuf } = await input.makeBuffer({
23
+ t.teardown(removeCallback);
24
+ const { fileHandle, readCircBuf, writeCircBuf } = await input.makeBuffer({
31
25
  circularBufferSize: BUFFER_SIZE,
32
26
  circularBufferFilename: tmpFile,
33
27
  });
34
- const slogSender = makeSlogSenderFromBuffer({ fileHandle, writeCircBuf });
35
- t.teardown(slogSender.shutdown);
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
+ );
36
47
  slogSender({ type: 'start' });
37
48
  await slogSender.forceFlush();
38
49
  t.is(fs.readFileSync(tmpFile, { encoding: 'utf8' }).length, BUFFER_SIZE);
39
50
 
40
51
  const len0 = new Uint8Array(BigUint64Array.BYTES_PER_ELEMENT);
41
- const { done: done0 } = readCircBuf(len0);
52
+ const { done: done0 } = await readCircBuf(len0);
42
53
  t.false(done0, 'readCircBuf should not be done');
43
54
  const dv0 = new DataView(len0.buffer);
44
55
  const buf0 = new Uint8Array(Number(dv0.getBigUint64(0)));
45
- const { done: done0b } = readCircBuf(buf0, len0.byteLength);
56
+ const { done: done0b } = await readCircBuf(buf0, len0.byteLength);
46
57
  t.false(done0b, 'readCircBuf should not be done');
47
58
  const buf0Str = new TextDecoder().decode(buf0);
48
59
  t.is(buf0Str, `\n{"type":"start"}`, `start compare failed`);
@@ -61,12 +72,12 @@ const bufferTests = test.macro(
61
72
  let offset = 0;
62
73
  const len1 = new Uint8Array(BigUint64Array.BYTES_PER_ELEMENT);
63
74
  for (let i = 490; i < last; i += 1) {
64
- const { done: done1 } = readCircBuf(len1, offset);
75
+ const { done: done1 } = await readCircBuf(len1, offset);
65
76
  offset += len1.byteLength;
66
77
  t.false(done1, `readCircBuf ${i} should not be done`);
67
78
  const dv1 = new DataView(len1.buffer);
68
79
  const buf1 = new Uint8Array(Number(dv1.getBigUint64(0)));
69
- const { done: done1b } = readCircBuf(buf1, offset);
80
+ const { done: done1b } = await readCircBuf(buf1, offset);
70
81
  offset += buf1.byteLength;
71
82
  t.false(done1b, `readCircBuf ${i} should not be done`);
72
83
  const buf1Str = new TextDecoder().decode(buf1);
@@ -77,12 +88,24 @@ const bufferTests = test.macro(
77
88
  );
78
89
  }
79
90
 
80
- const { done: done2 } = readCircBuf(len1, offset);
91
+ const { done: done2 } = await readCircBuf(len1, offset);
81
92
  t.assert(done2, `readCircBuf ${last} should be done`);
82
93
 
83
94
  slogSender(null, 'PRE-SERIALIZED');
84
95
  await slogSender.forceFlush();
85
96
  t.truthy(fs.readFileSync(tmpFile).includes('PRE-SERIALIZED'));
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'));
86
109
  },
87
110
  );
88
111