@nosana/node 1.1.22 → 1.1.23
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/package.json +1 -1
- package/dist/src/NodeManager/node/utils/extractLogsAndResultsFromLogBuffer.d.ts +1 -1
- package/dist/src/NodeManager/node/utils/extractLogsAndResultsFromLogBuffer.js +5 -25
- package/dist/src/NodeManager/provider/ContainerStateManager.d.ts +14 -0
- package/dist/src/NodeManager/provider/ContainerStateManager.js +37 -6
- package/dist/src/NodeManager/provider/Provider.js +4 -0
- package/dist/src/NodeManager/provider/utils/parseBuffer.d.ts +13 -0
- package/dist/src/NodeManager/provider/utils/parseBuffer.js +23 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import type { OperationResults, Log } from '@nosana/sdk';
|
|
4
|
-
export declare function extractLogsAndResultsFromLogBuffer(logBuffer: Buffer, operationResults: OperationResults | undefined
|
|
4
|
+
export declare function extractLogsAndResultsFromLogBuffer(logBuffer: Buffer, operationResults: OperationResults | undefined): {
|
|
5
5
|
logs: Log[];
|
|
6
6
|
results: {} | undefined;
|
|
7
7
|
};
|
|
@@ -1,34 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseLogFrames } from '../../provider/utils/parseBuffer.js';
|
|
2
2
|
import { createResultsObject, extractResultFromLog, } from './extractResultsFromLogs.js';
|
|
3
|
-
export function extractLogsAndResultsFromLogBuffer(logBuffer, operationResults
|
|
4
|
-
const logs = [];
|
|
3
|
+
export function extractLogsAndResultsFromLogBuffer(logBuffer, operationResults) {
|
|
5
4
|
const results = operationResults
|
|
6
5
|
? createResultsObject(operationResults)
|
|
7
6
|
: undefined;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
logs.push({
|
|
12
|
-
type: 'nodeerr',
|
|
13
|
-
log: 'Took too long to retrive all logs',
|
|
14
|
-
timestamp: new Date().toISOString(),
|
|
15
|
-
});
|
|
16
|
-
}, expiryTimeout);
|
|
17
|
-
while (index < logBuffer.length && running) {
|
|
18
|
-
const log = parseBuffer(logBuffer, index);
|
|
19
|
-
logs.push(log);
|
|
20
|
-
if (results && operationResults) {
|
|
7
|
+
const { logs } = parseLogFrames(logBuffer);
|
|
8
|
+
if (results && operationResults) {
|
|
9
|
+
for (const log of logs) {
|
|
21
10
|
extractResultFromLog(results, log, operationResults);
|
|
22
11
|
}
|
|
23
|
-
if (logs.length >= 25000) {
|
|
24
|
-
running = false;
|
|
25
|
-
logs.push({
|
|
26
|
-
type: 'nodeerr',
|
|
27
|
-
log: 'Found too many logs...',
|
|
28
|
-
timestamp: new Date().toISOString(),
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
12
|
}
|
|
32
|
-
clearTimeout(timer);
|
|
33
13
|
return { logs, results };
|
|
34
14
|
}
|
|
@@ -11,6 +11,7 @@ export declare class ContainerStateManager {
|
|
|
11
11
|
private state;
|
|
12
12
|
private exitedCheckCount;
|
|
13
13
|
private lastLogTimestamp;
|
|
14
|
+
private logRemainder;
|
|
14
15
|
private readonly EXITED_CHECKS_REQUIRED;
|
|
15
16
|
private currentLogStream;
|
|
16
17
|
private currentStatsStream;
|
|
@@ -20,6 +21,19 @@ export declare class ContainerStateManager {
|
|
|
20
21
|
startMonitoring(): Promise<void>;
|
|
21
22
|
private attachLogStream;
|
|
22
23
|
waitForExit(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Wait for the followed log stream to finish delivering its final buffered
|
|
26
|
+
* frames after the container has exited.
|
|
27
|
+
*
|
|
28
|
+
* `container.wait()` (which drives `waitForExit`) and the log stream are
|
|
29
|
+
* independent connections, so when `wait()` reports the exit the stream may
|
|
30
|
+
* still be flushing the last lines (e.g. an op's final result line). Reading
|
|
31
|
+
* logs or tearing the stream down at that point loses them. The stream's
|
|
32
|
+
* `close` handler nulls `currentLogStream`, and it won't re-attach once the
|
|
33
|
+
* container has exited, so a null stream means the flush is complete. Bounded
|
|
34
|
+
* so a stream that never closes can't stall completion.
|
|
35
|
+
*/
|
|
36
|
+
waitForLogsDrained(timeoutMs?: number): Promise<void>;
|
|
23
37
|
stopMonitoring(): void;
|
|
24
38
|
private readonly STATS_INTERVAL_MS;
|
|
25
39
|
private attachStatsStream;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
2
|
import { createInterface } from 'readline';
|
|
3
|
-
import {
|
|
3
|
+
import { parseLogFrames } from './utils/parseBuffer.js';
|
|
4
4
|
import { parseDockerStat } from './utils/parseDockerStat.js';
|
|
5
5
|
import { liveAbortSignal } from '../utils/liveAbortSignal.js';
|
|
6
6
|
function destroyStream(stream) {
|
|
@@ -20,6 +20,7 @@ export class ContainerStateManager {
|
|
|
20
20
|
this.state = 'starting';
|
|
21
21
|
this.exitedCheckCount = 0;
|
|
22
22
|
this.lastLogTimestamp = 0; // Unix timestamp in seconds from actual container logs
|
|
23
|
+
this.logRemainder = Buffer.alloc(0); // trailing partial frame between chunks
|
|
23
24
|
this.EXITED_CHECKS_REQUIRED = 3; // Require 3 consecutive checks before confirming exit
|
|
24
25
|
this.currentLogStream = null;
|
|
25
26
|
this.currentStatsStream = null;
|
|
@@ -78,6 +79,9 @@ export class ContainerStateManager {
|
|
|
78
79
|
// container we're tearing down. Mirrors `attachStatsStream`.
|
|
79
80
|
if (this.controller.signal.aborted)
|
|
80
81
|
return;
|
|
82
|
+
// Any partial frame buffered from a previous stream can never be completed
|
|
83
|
+
// by this freshly re-attached stream, so start clean.
|
|
84
|
+
this.logRemainder = Buffer.alloc(0);
|
|
81
85
|
try {
|
|
82
86
|
this.currentLogStream = await this.container.logs({
|
|
83
87
|
stdout: true,
|
|
@@ -88,11 +92,18 @@ export class ContainerStateManager {
|
|
|
88
92
|
abortSignal: liveAbortSignal(this.controller.signal),
|
|
89
93
|
});
|
|
90
94
|
this.currentLogStream.on('data', (data) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
// A single 'data' event can carry several multiplexed frames (Docker
|
|
96
|
+
// coalesces output, especially the final flush at container exit) and
|
|
97
|
+
// can split one frame across events. Decode every complete frame and
|
|
98
|
+
// carry any partial trailing frame over to the next chunk.
|
|
99
|
+
const { logs, rest } = parseLogFrames(Buffer.concat([this.logRemainder, data]));
|
|
100
|
+
this.logRemainder = rest;
|
|
101
|
+
for (const { log, type, timestamp } of logs) {
|
|
102
|
+
if (!log)
|
|
103
|
+
continue;
|
|
104
|
+
this.lastLogTimestamp = Math.floor(new Date(timestamp).getTime() / 1000);
|
|
105
|
+
this.emitter.emit('log', log, type, 'container', timestamp);
|
|
106
|
+
}
|
|
96
107
|
});
|
|
97
108
|
this.currentLogStream.on('close', () => {
|
|
98
109
|
this.currentLogStream = null;
|
|
@@ -125,6 +136,26 @@ export class ContainerStateManager {
|
|
|
125
136
|
}, 500);
|
|
126
137
|
});
|
|
127
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Wait for the followed log stream to finish delivering its final buffered
|
|
141
|
+
* frames after the container has exited.
|
|
142
|
+
*
|
|
143
|
+
* `container.wait()` (which drives `waitForExit`) and the log stream are
|
|
144
|
+
* independent connections, so when `wait()` reports the exit the stream may
|
|
145
|
+
* still be flushing the last lines (e.g. an op's final result line). Reading
|
|
146
|
+
* logs or tearing the stream down at that point loses them. The stream's
|
|
147
|
+
* `close` handler nulls `currentLogStream`, and it won't re-attach once the
|
|
148
|
+
* container has exited, so a null stream means the flush is complete. Bounded
|
|
149
|
+
* so a stream that never closes can't stall completion.
|
|
150
|
+
*/
|
|
151
|
+
async waitForLogsDrained(timeoutMs = 3000) {
|
|
152
|
+
const deadline = Date.now() + timeoutMs;
|
|
153
|
+
while (this.currentLogStream !== null) {
|
|
154
|
+
if (this.controller.signal.aborted || Date.now() >= deadline)
|
|
155
|
+
return;
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
128
159
|
stopMonitoring() {
|
|
129
160
|
if (this.pollingInterval) {
|
|
130
161
|
clearInterval(this.pollingInterval);
|
|
@@ -266,6 +266,10 @@ export class Provider {
|
|
|
266
266
|
emitter.emit('healthcheck:startup:success');
|
|
267
267
|
}
|
|
268
268
|
await stateManager.waitForExit();
|
|
269
|
+
// `wait()` reporting the exit and the log stream finishing its final
|
|
270
|
+
// flush are independent; drain the stream before results are read off
|
|
271
|
+
// the logs so the last line (e.g. the op's result) isn't lost.
|
|
272
|
+
await stateManager.waitForLogsDrained();
|
|
269
273
|
}
|
|
270
274
|
const info = await promiseTimeoutWrapper(container.inspect({
|
|
271
275
|
abortSignal: liveAbortSignal(controller.signal),
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { Log } from '@nosana/sdk';
|
|
4
|
+
/**
|
|
5
|
+
* Demultiplex every complete Docker log frame contained in `buffer`.
|
|
6
|
+
*
|
|
7
|
+
* A non-TTY container log stream is a sequence of frames, each an 8-byte header
|
|
8
|
+
* (stream type + big-endian payload length) followed by the payload. The stream
|
|
9
|
+
* layer may pack several frames into one read *and* split a single frame across
|
|
10
|
+
* reads, so a caller following a live stream must feed the returned `rest` (an
|
|
11
|
+
* incomplete trailing frame, if any) back in alongside the next chunk.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseLogFrames(buffer: Buffer): {
|
|
14
|
+
logs: Log[];
|
|
15
|
+
rest: Buffer;
|
|
16
|
+
};
|
|
4
17
|
export declare function parseBuffer(buffer: Buffer, index?: number): Log;
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
const TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\s/;
|
|
2
|
+
const DOCKER_LOG_HEADER_BYTES = 8;
|
|
3
|
+
/**
|
|
4
|
+
* Demultiplex every complete Docker log frame contained in `buffer`.
|
|
5
|
+
*
|
|
6
|
+
* A non-TTY container log stream is a sequence of frames, each an 8-byte header
|
|
7
|
+
* (stream type + big-endian payload length) followed by the payload. The stream
|
|
8
|
+
* layer may pack several frames into one read *and* split a single frame across
|
|
9
|
+
* reads, so a caller following a live stream must feed the returned `rest` (an
|
|
10
|
+
* incomplete trailing frame, if any) back in alongside the next chunk.
|
|
11
|
+
*/
|
|
12
|
+
export function parseLogFrames(buffer) {
|
|
13
|
+
const logs = [];
|
|
14
|
+
let index = 0;
|
|
15
|
+
while (index + DOCKER_LOG_HEADER_BYTES <= buffer.length) {
|
|
16
|
+
const chunkLength = buffer.readUInt32BE(index + 4);
|
|
17
|
+
const end = index + DOCKER_LOG_HEADER_BYTES + chunkLength;
|
|
18
|
+
if (end > buffer.length)
|
|
19
|
+
break; // frame has not fully arrived yet
|
|
20
|
+
logs.push(parseBuffer(buffer, index));
|
|
21
|
+
index = end;
|
|
22
|
+
}
|
|
23
|
+
return { logs, rest: buffer.subarray(index) };
|
|
24
|
+
}
|
|
2
25
|
export function parseBuffer(buffer, index = 0) {
|
|
3
26
|
const head = buffer.subarray(index, (index += 8));
|
|
4
27
|
const chunkType = head.readUInt8(0);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nosana/node",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.23",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@nosana/node",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.23",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@coral-xyz/anchor": "^0.28.1-beta.1",
|