Skip to content

Commit c19a02b

Browse files
authored
Fixes for problems Claude discovered in middle-layer (#1483)
1 parent 6418a85 commit c19a02b

11 files changed

Lines changed: 159 additions & 43 deletions

File tree

.changeset/clean-carpets-hunt.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@milaboratories/pl-middle-layer": patch
3+
"@milaboratories/pl-client": patch
4+
"@milaboratories/pl-tree": patch
5+
---
6+
7+
Fixes for problems Claude discovered

lib/node/pl-client/src/core/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function isAbortedError(err: unknown, nested: boolean = false): boolean {
5353
if (err instanceof DOMException && err.code === DOMException.ABORT_ERR) return true; // WebSocket error
5454
if ((err as any).name == "RpcError" && (err as any).code == "ABORTED") return true;
5555
if ((err as any).name == "RESTError" && (err as any).status.code == Code.ABORTED) return true;
56-
if ((err as any).cause !== undefined && !nested) isAbortedError((err as any).cause, true);
56+
if ((err as any).cause !== undefined && !nested) return isAbortedError((err as any).cause, true);
5757
return false;
5858
}
5959

lib/node/pl-client/src/core/websocket_stream.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export class WebSocketBiDiStream<
6161
// Error tracking
6262
private lastError?: Error;
6363

64+
// Abort listener reference for cleanup
65+
private readonly abortHandler?: () => void;
66+
6467
// === Public API ===
6568

6669
public readonly requests = {
@@ -122,7 +125,8 @@ export class WebSocketBiDiStream<
122125
return;
123126
}
124127

125-
this.options.abortSignal?.addEventListener("abort", () => this.close());
128+
this.abortHandler = () => this.close();
129+
this.options.abortSignal?.addEventListener("abort", this.abortHandler, { once: true });
126130
this.connect();
127131
}
128132

@@ -192,6 +196,11 @@ export class WebSocketBiDiStream<
192196
private onClose(): void {
193197
this.progressConnectionState(ConnectionState.CLOSED);
194198

199+
// Clean up abort listener to prevent memory leaks
200+
if (this.abortHandler) {
201+
this.options.abortSignal?.removeEventListener("abort", this.abortHandler);
202+
}
203+
195204
// If abort signal was triggered, use that as the error source
196205
if (this.options.abortSignal?.aborted && !this.lastError) {
197206
const reason = this.options.abortSignal.reason;

lib/node/pl-middle-layer/src/middle_layer/middle_layer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ export class MiddleLayer {
308308
}),
309309
runtimeCapabilities,
310310
quickJs,
311-
projectHelper: new ProjectHelper(quickJs),
311+
projectHelper: new ProjectHelper(quickJs, logger),
312312
dispose: async () => {
313313
await retryHttpDispatcher.destroy();
314314
await driverKit.dispose();

lib/node/pl-middle-layer/src/middle_layer/project.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ import { getBlockParameters, blockOutputs } from "./block";
2424
import type { FrontendData } from "../model/frontend";
2525
import type { ProjectStructure } from "../model/project_model";
2626
import { projectFieldName } from "../model/project_model";
27-
import { cachedDeserialize, notEmpty, type MiLogger } from "@milaboratories/ts-helpers";
27+
import {
28+
cachedDeserialize,
29+
notEmpty,
30+
type MiLogger,
31+
createInfiniteRetryState,
32+
nextInfiniteRetryState,
33+
type InfiniteRetryState,
34+
} from "@milaboratories/ts-helpers";
2835
import type { BlockPackInfo } from "../model/block_pack";
2936
import type {
3037
ProjectOverview,
@@ -116,6 +123,7 @@ export class Project {
116123
}
117124

118125
private async refreshLoop(): Promise<void> {
126+
let retryState: InfiniteRetryState | undefined;
119127
while (!this.destroyed) {
120128
try {
121129
await withProject(
@@ -128,7 +136,9 @@ export class Project {
128136
{ name: "doRefresh", lockId: this.projectLockId },
129137
);
130138
await this.activeConfigs.getValue();
131-
await setTimeout(this.env.ops.projectRefreshInterval, this.abortController.signal);
139+
await setTimeout(this.env.ops.projectRefreshInterval, undefined, {
140+
signal: this.abortController.signal,
141+
});
132142

133143
// Block computables housekeeping
134144
const overviewLight = await this.overviewLight.getValue();
@@ -141,25 +151,41 @@ export class Project {
141151
this.blockComputables.set(blockId, null);
142152
}
143153
}
154+
retryState = undefined;
144155
} catch (e: unknown) {
145156
// If we're destroyed, exit gracefully regardless of error type
146-
if (this.destroyed) {
147-
// Log just in case, to help with debugging if something unexpected happens during shutdown
148-
this.env.logger.warn(new Error("Error during refresh loop shutdown", { cause: e }));
149-
break;
150-
}
157+
if (this.destroyed) break;
151158

152159
if (isNotFoundError(e)) {
153-
console.warn(
160+
this.env.logger.warn(
154161
"project refresh routine terminated, because project was externally deleted",
155162
);
156163
break;
157164
} else if (isTimeoutOrCancelError(e)) {
158165
// Timeout during normal operation, continue the loop
159166
} else {
160-
// TODO: This stops the refresh loop permanently, leaving the project broken.
161-
// Need to decide how to handle this case.
162-
throw new Error("Unexpected exception", { cause: e });
167+
retryState = retryState
168+
? nextInfiniteRetryState(retryState)
169+
: createInfiniteRetryState({
170+
type: "exponentialWithMaxDelayBackoff",
171+
initialDelay: 1000,
172+
maxDelay: 60_000,
173+
backoffMultiplier: 2,
174+
jitter: 0,
175+
});
176+
this.env.logger.error(
177+
new Error(`[refreshLoop] unexpected exception, retrying in ${retryState.nextDelay}ms`, {
178+
cause: e,
179+
}),
180+
);
181+
try {
182+
await setTimeout(retryState.nextDelay, undefined, {
183+
signal: this.abortController.signal,
184+
});
185+
} catch {
186+
// Aborted during retry delay, will exit via while condition or destroyed check
187+
break;
188+
}
163189
}
164190
}
165191
}

lib/node/pl-middle-layer/src/model/project_helper.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { LRUCache } from "lru-cache";
1616
import type { QuickJSWASMModule } from "quickjs-emscripten";
1717
import { executeSingleLambda } from "../js_render";
1818
import type { ResourceId } from "@milaboratories/pl-client";
19+
import { ConsoleLoggerAdapter, type MiLogger } from "@milaboratories/ts-helpers";
1920

2021
type EnrichmentTargetsRequest = {
2122
blockConfig: () => BlockConfig;
@@ -55,7 +56,10 @@ export class ProjectHelper {
5556
},
5657
});
5758

58-
constructor(private readonly quickJs: QuickJSWASMModule) {}
59+
constructor(
60+
private readonly quickJs: QuickJSWASMModule,
61+
public readonly logger: MiLogger = new ConsoleLoggerAdapter(),
62+
) {}
5963

6064
// =============================================================================
6165
// Args Derivation from Storage (V3+)
@@ -182,7 +186,11 @@ export class ProjectHelper {
182186
) as string;
183187
return result;
184188
} catch (e) {
185-
console.error("[ProjectHelper.getInitialStorageInVM] Initial storage creation failed:", e);
189+
this.logger.error(
190+
new Error("[ProjectHelper.getInitialStorageInVM] Initial storage creation failed", {
191+
cause: e,
192+
}),
193+
);
186194
throw new Error(`Block initial storage creation failed: ${e}`);
187195
}
188196
}
@@ -219,7 +227,9 @@ export class ProjectHelper {
219227
) as string;
220228
return result;
221229
} catch (e) {
222-
console.error("[ProjectHelper.applyStorageUpdateInVM] Storage update failed:", e);
230+
this.logger.error(
231+
new Error("[ProjectHelper.applyStorageUpdateInVM] Storage update failed", { cause: e }),
232+
);
223233
throw new Error(`Block storage update failed: ${e}`);
224234
}
225235
}
@@ -249,7 +259,11 @@ export class ProjectHelper {
249259
) as StringifiedJson<StorageDebugView>;
250260
return result;
251261
} catch (e) {
252-
console.error("[ProjectHelper.getStorageDebugViewInVM] Get storage debug view failed:", e);
262+
this.logger.error(
263+
new Error("[ProjectHelper.getStorageDebugViewInVM] Get storage debug view failed", {
264+
cause: e,
265+
}),
266+
);
253267
return undefined;
254268
}
255269
}
@@ -290,7 +304,9 @@ export class ProjectHelper {
290304
) as MigrationResult;
291305
return result;
292306
} catch (e) {
293-
console.error("[ProjectHelper.migrateStorageInVM] Migration failed:", e);
307+
this.logger.error(
308+
new Error("[ProjectHelper.migrateStorageInVM] Migration failed", { cause: e }),
309+
);
294310
return { error: `VM execution failed: ${e}` };
295311
}
296312
}

lib/node/pl-middle-layer/src/mutator/project.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ import {
6363
notEmpty,
6464
canonicalJsonBytes,
6565
cachedDecode,
66+
type MiLogger,
67+
ConsoleLoggerAdapter,
6668
} from "@milaboratories/ts-helpers";
6769
import type { ProjectHelper } from "../model/project_helper";
6870
import {
@@ -119,6 +121,7 @@ class BlockInfo {
119121
public readonly fields: BlockFieldStates,
120122
public readonly config: BlockConfig,
121123
public readonly source: BlockPackSpec,
124+
private readonly logger: MiLogger = new ConsoleLoggerAdapter(),
122125
) {}
123126

124127
public check() {
@@ -200,7 +203,7 @@ class BlockInfo {
200203
try {
201204
return this.blockStorageC();
202205
} catch (e) {
203-
console.error("Error getting blockStorage:", e);
206+
this.logger.error(new Error(`Error getting blockStorage for ${this.id}`, { cause: e }));
204207
return undefined;
205208
}
206209
}
@@ -595,6 +598,9 @@ export class ProjectMutator {
595598
): { args?: unknown; uiState?: unknown } {
596599
const info = this.getBlockInfo(blockId);
597600
const currentState = info.blockStorage as { args?: unknown; uiState?: unknown } | undefined;
601+
if (currentState === undefined) {
602+
throw new Error(`Cannot merge block state for ${blockId}: blockStorage is unavailable`);
603+
}
598604
return { ...currentState, ...partialUpdate };
599605
}
600606

@@ -653,9 +659,17 @@ export class ProjectMutator {
653659
);
654660
if (prerunArgs !== undefined) {
655661
this.setBlockFieldObj(blockId, "currentPrerunArgs", this.createJsonFieldValue(prerunArgs));
662+
} else {
663+
this.deleteBlockFields(blockId, "currentPrerunArgs");
656664
}
657665
} else {
666+
if (info.fields.currentPrerunArgs !== undefined) {
667+
this.projectHelper.logger.warn(
668+
`[staging] ${blockId}: currentPrerunArgs cleared (args derivation failed)`,
669+
);
670+
}
658671
this.deleteBlockFields(blockId, "currentArgs");
672+
this.deleteBlockFields(blockId, "currentPrerunArgs");
659673
}
660674
}
661675

@@ -756,10 +770,7 @@ export class ProjectMutator {
756770
prerunArgsData,
757771
);
758772
} else {
759-
// prerunArgs is undefined - check if we previously had one
760-
if (info.fields.currentPrerunArgs !== undefined) {
761-
prerunArgsChanged = true;
762-
}
773+
this.deleteBlockFields(req.blockId, "currentPrerunArgs");
763774
}
764775

765776
blockChanged = true;
@@ -903,6 +914,7 @@ export class ProjectMutator {
903914
{},
904915
extractConfig(spec.blockPack.config),
905916
spec.blockPack.source,
917+
this.projectHelper.logger,
906918
);
907919
this.blockInfos.set(blockId, info);
908920

@@ -1006,7 +1018,13 @@ export class ProjectMutator {
10061018
}
10071019

10081020
private initializeBlockDuplicate(blockId: string, originalBlockInfo: BlockInfo) {
1009-
const info = new BlockInfo(blockId, {}, originalBlockInfo.config, originalBlockInfo.source);
1021+
const info = new BlockInfo(
1022+
blockId,
1023+
{},
1024+
originalBlockInfo.config,
1025+
originalBlockInfo.source,
1026+
this.projectHelper.logger,
1027+
);
10101028

10111029
this.blockInfos.set(blockId, info);
10121030

@@ -1210,7 +1228,12 @@ export class ProjectMutator {
12101228
"currentPrerunArgs",
12111229
this.createJsonFieldValue(prerunArgs),
12121230
);
1231+
} else {
1232+
this.deleteBlockFields(blockId, "currentPrerunArgs");
12131233
}
1234+
} else {
1235+
this.deleteBlockFields(blockId, "currentArgs");
1236+
this.deleteBlockFields(blockId, "currentPrerunArgs");
12141237
}
12151238
};
12161239

@@ -1250,7 +1273,9 @@ export class ProjectMutator {
12501273
);
12511274
}
12521275

1253-
console.log(`[migrateBlockPack] Block ${blockId}: ${migrationResult.info}`);
1276+
this.projectHelper.logger.info(
1277+
`[migrateBlockPack] Block ${blockId}: ${migrationResult.info}`,
1278+
);
12541279
applyStorageAndDeriveArgs(migrationResult.newStorageJson);
12551280
} else {
12561281
// Legacy blocks (modelAPIVersion 1): persist block pack, set prerunArgs = currentArgs
@@ -1413,9 +1438,14 @@ export class ProjectMutator {
14131438
// meaning staging already rendered
14141439
return;
14151440
if (lagThreshold === undefined || lag <= lagThreshold) {
1416-
// console.log(`[refreshStagings] RENDER staging for ${blockId} (lag=${lag})`);
1417-
this.renderStagingFor(blockId);
1418-
rendered++;
1441+
try {
1442+
this.renderStagingFor(blockId);
1443+
rendered++;
1444+
} catch (e) {
1445+
this.projectHelper.logger.error(
1446+
new Error(`[refreshStagings] renderStagingFor failed for ${blockId}`, { cause: e }),
1447+
);
1448+
}
14191449
}
14201450
});
14211451
if (rendered > 0) this.resetStagingRefreshTimestamp();
@@ -1654,7 +1684,10 @@ export class ProjectMutator {
16541684

16551685
const blockInfos = new Map<string, BlockInfo>();
16561686
blockInfoStates.forEach(({ id, fields, blockConfig, blockPack }) =>
1657-
blockInfos.set(id, new BlockInfo(id, fields, notEmpty(blockConfig), notEmpty(blockPack))),
1687+
blockInfos.set(
1688+
id,
1689+
new BlockInfo(id, fields, notEmpty(blockConfig), notEmpty(blockPack), projectHelper.logger),
1690+
),
16581691
);
16591692

16601693
// check consistency of project state

lib/node/pl-middle-layer/src/network_check/network_check.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ interface NetworkReports {
6868
}
6969

7070
export interface CheckNetworkOpts {
71+
/** Signal to abort all network checks. */
72+
signal?: AbortSignal;
73+
7174
/** Platforma Backend pings options. */
7275
pingCheckDurationMs: number;
7376
pingTimeoutMs: number;

0 commit comments

Comments
 (0)