@hew-lang/sandbox-vm 0.8.0 → 0.9.1

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.
@@ -143,6 +143,11 @@ class ExecutorV1 {
143
143
  this.host = {
144
144
  argv: [...(options.argv ?? ["main.hew"])],
145
145
  unixTimeMillis: () => this.trace.virtualTimeMs,
146
+ monotonicTimeNanos: () => {
147
+ const millis = this.trace.virtualTimeMs;
148
+ return (BigInt(Math.trunc(millis)) * 1000000n +
149
+ BigInt(Math.round((millis % 1) * 1_000_000)));
150
+ },
146
151
  calendarBridge: options.calendarBridge,
147
152
  codecBridge: options.codecBridge,
148
153
  encodingError: (format, message) => {
@@ -154,6 +159,10 @@ class ExecutorV1 {
154
159
  errors[format] = message;
155
160
  return errors[format] ?? "";
156
161
  },
162
+ setFileError: (message) => {
163
+ this.current.context.fileError =
164
+ message === null ? null : { kind: 0, message };
165
+ },
157
166
  fileError: (operation) => {
158
167
  const error = this.current.context.fileError;
159
168
  switch (operation) {
@@ -1162,7 +1171,15 @@ class ExecutorV1 {
1162
1171
  // `self`. A plain function value carries none.
1163
1172
  const callee = this.boundary(act, term.callee);
1164
1173
  const args = term.args.map((operand) => this.boundary(act, operand));
1165
- this.current = this.activate(this.functionAt(calleeFunction(callee, term.specialization)), callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
1174
+ const body = this.functionAt(calleeFunction(callee, term.specialization));
1175
+ this.current = this.activate(body, callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
1176
+ // SIR already distinguishes a consuming body from a borrowed body
1177
+ // exposed through a once-callable view. Only the latter leaves its
1178
+ // receiver cleanup to this invocation boundary.
1179
+ if (callee.kind === "closure" &&
1180
+ term.callee.decision === "move" &&
1181
+ body.params[0]?.own !== "owned")
1182
+ this.current.consumedCallable = callee;
1166
1183
  return;
1167
1184
  }
1168
1185
  case "actor.call":
@@ -1180,6 +1197,23 @@ class ExecutorV1 {
1180
1197
  if (registration.kind === "refused")
1181
1198
  throw new Error(`runtime family ${registration.capability} was refused after admission`);
1182
1199
  switch (registration.handler) {
1200
+ case "context_actor_id":
1201
+ this.completeShim(act, term, {
1202
+ kind: "i64",
1203
+ value: act.context.actor?.identity ?? 0n,
1204
+ });
1205
+ return;
1206
+ case "context_supervisor": {
1207
+ const parent = act.context.actor?.supervisor?.owner;
1208
+ this.completeShim(act, term, parent
1209
+ ? { kind: "supervisor", id: parent.id }
1210
+ : { kind: "i64", value: 0n });
1211
+ return;
1212
+ }
1213
+ case "context_trace_span":
1214
+ // The sandbox installs no external tracing span in actor turns.
1215
+ this.completeShim(act, term, { kind: "i64", value: 0n });
1216
+ return;
1183
1217
  case "actor_caller": {
1184
1218
  const caller = act.context.caller ?? { kind: "Task" };
1185
1219
  const payload = [];
@@ -1683,6 +1717,8 @@ class ExecutorV1 {
1683
1717
  return true;
1684
1718
  }
1685
1719
  returnFrom(act, value) {
1720
+ if (this.finishConsumedCallable(act, value, () => this.returnFrom(act, value)))
1721
+ return;
1686
1722
  if (this.finishConstruction(act, () => this.returnFrom(act, value)))
1687
1723
  return;
1688
1724
  const caller = act.caller;
@@ -1744,6 +1780,8 @@ class ExecutorV1 {
1744
1780
  this.takeEdge(act, unwind);
1745
1781
  }
1746
1782
  resumeUnwind(act, handback) {
1783
+ if (this.finishConsumedCallable(act, null, () => this.resumeUnwind(act, handback)))
1784
+ return;
1747
1785
  if (this.finishConstruction(act, () => this.resumeUnwind(act, handback)))
1748
1786
  return;
1749
1787
  const fault = act.fault;
@@ -1774,6 +1812,34 @@ class ExecutorV1 {
1774
1812
  this.current = caller;
1775
1813
  this.takeEdge(caller, act.unwind);
1776
1814
  }
1815
+ finishConsumedCallable(act, value, done) {
1816
+ const callable = act.consumedCallable;
1817
+ if (!callable)
1818
+ return false;
1819
+ act.consumedCallable = undefined;
1820
+ this.running = false;
1821
+ this.closeValueAsync(callable, this.faultText(act.fault), (fault) => {
1822
+ act.fault = combineFaults(act.fault, fault);
1823
+ const resume = () => {
1824
+ if (value !== null && act.fault)
1825
+ this.resumeUnwind(act, null);
1826
+ else
1827
+ done();
1828
+ const current = this.current;
1829
+ this.scheduler.enqueue(current.context.id, () => this.runFrame(current));
1830
+ };
1831
+ // A successful body result is not observable until receiver cleanup
1832
+ // succeeds. A cleanup fault must release that unpublished result.
1833
+ if (value !== null && act.fault)
1834
+ this.closeValueAsync(value, this.faultText(act.fault), (resultFault) => {
1835
+ act.fault = combineFaults(act.fault, resultFault);
1836
+ resume();
1837
+ }, act.context.actor, act.rcOwner);
1838
+ else
1839
+ resume();
1840
+ }, act.context.actor, act.rcOwner);
1841
+ return true;
1842
+ }
1777
1843
  suspend(act, input) {
1778
1844
  const registration = resolveSuspension(input);
1779
1845
  if (registration.kind === "refused")
@@ -2046,6 +2112,7 @@ class ExecutorV1 {
2046
2112
  // The record is the raw line,
2047
2113
  // so the records concatenate to the stdin the program consumed.
2048
2114
  const line = this.stdin.readLine() ?? new Uint8Array();
2115
+ act.context.fileError = null;
2049
2116
  this.trace.recordReplayInput({ kind: "stdin", data: new TextDecoder().decode(line) }, false);
2050
2117
  this.park(act, term)(bytesValue([...line]));
2051
2118
  return;
@@ -3141,12 +3208,29 @@ class ExecutorV1 {
3141
3208
  const count = isPool ? args[args.length - 1] : undefined;
3142
3209
  if (isPool && count?.kind !== "i64")
3143
3210
  throw new Error("pool construction has no count");
3144
- const config = isPool ? args.slice(0, -1) : args;
3145
- if (count?.kind === "i64" &&
3146
- (count.value <= 0n || count.value > 2147483647n)) {
3211
+ const configured = isPool ? args.slice(0, -1) : args;
3212
+ const initial = layout.backoff
3213
+ ? configured[configured.length - 2]
3214
+ : undefined;
3215
+ const maximum = layout.backoff
3216
+ ? configured[configured.length - 1]
3217
+ : undefined;
3218
+ const backoff = initial?.kind === "i64" && maximum?.kind === "i64"
3219
+ ? { initial: initial.value, maximum: maximum.value }
3220
+ : null;
3221
+ const validBackoff = !layout.backoff ||
3222
+ (backoff !== null &&
3223
+ backoff.initial > 0n &&
3224
+ backoff.maximum >= backoff.initial);
3225
+ const config = layout.backoff ? configured.slice(0, -2) : configured;
3226
+ if (!validBackoff ||
3227
+ (count?.kind === "i64" &&
3228
+ (count.value <= 0n || count.value > 2147483647n))) {
3147
3229
  const fault = {
3148
3230
  kind: "panic",
3149
- message: "supervisor pool count must be between 1 and 2147483647",
3231
+ message: !validBackoff
3232
+ ? "supervisor backoff requires positive initial and maximum >= initial"
3233
+ : "supervisor pool count must be between 1 and 2147483647",
3150
3234
  };
3151
3235
  this.running = false;
3152
3236
  this.closeValueAsync({ kind: "record", typeId: "", fields: config }, fault.message, (closeFault) => {
@@ -3174,6 +3258,9 @@ class ExecutorV1 {
3174
3258
  teardownChild: null,
3175
3259
  teardownCancelTimer: null,
3176
3260
  restartTimes: [],
3261
+ backoff,
3262
+ restartDelays: new Map(),
3263
+ restartTimers: new Map(),
3177
3264
  closed: [],
3178
3265
  };
3179
3266
  this.supervisors.set(owner.id, owner);
@@ -3655,6 +3742,30 @@ class ExecutorV1 {
3655
3742
  for (const timer of [...actor.timers])
3656
3743
  timer.cancel();
3657
3744
  }
3745
+ bindActorStream(context, sink) {
3746
+ let unwatch = () => { };
3747
+ let active = true;
3748
+ const detach = () => {
3749
+ active = false;
3750
+ unwatch();
3751
+ };
3752
+ const closed = () => {
3753
+ if (!active || !this.pipes.peerClosed(sink))
3754
+ return;
3755
+ // Bind the reader to this exact producer turn, including while it waits
3756
+ // on another operation. Later reader closes must not cancel a new turn.
3757
+ detach();
3758
+ context.pendingCancellation ??= {
3759
+ kind: "panic",
3760
+ message: "actor stream reader closed",
3761
+ cancelled: true,
3762
+ };
3763
+ context.cancel?.(context.pendingCancellation);
3764
+ };
3765
+ unwatch = this.pipes.observe(sink, closed);
3766
+ closed();
3767
+ return detach;
3768
+ }
3658
3769
  dispatchActor(actor) {
3659
3770
  if (actor.busy || !actor.alive || actor.drainingMailbox)
3660
3771
  return;
@@ -3723,6 +3834,7 @@ class ExecutorV1 {
3723
3834
  actor_id: actor.id,
3724
3835
  handler: message.handler.name,
3725
3836
  });
3837
+ let unwatchStream = () => { };
3726
3838
  const frame = this.activate(this.functionAt(message.handler.callable), [actor.state, ...message.payload], null, null, undefined, null, {
3727
3839
  id: actor.id,
3728
3840
  actor,
@@ -3730,6 +3842,7 @@ class ExecutorV1 {
3730
3842
  requestDeadline: message.requestDeadline,
3731
3843
  reply: message.reply,
3732
3844
  returned: (value) => {
3845
+ unwatchStream();
3733
3846
  actor.active = null;
3734
3847
  const drained = (fault) => {
3735
3848
  actor.busy = false;
@@ -3765,6 +3878,7 @@ class ExecutorV1 {
3765
3878
  }
3766
3879
  },
3767
3880
  failed: (fault) => {
3881
+ unwatchStream();
3768
3882
  actor.active = null;
3769
3883
  actor.busy = false;
3770
3884
  if (fault.kind === "trap" &&
@@ -3785,6 +3899,13 @@ class ExecutorV1 {
3785
3899
  },
3786
3900
  });
3787
3901
  actor.active = frame.context;
3902
+ if (message.handler.streams) {
3903
+ // StreamStart's checked producer protocol appends its output sink.
3904
+ const sink = message.payload.at(-1);
3905
+ if (sink?.kind !== "sink")
3906
+ throw new Error("actor stream producer has no final sink");
3907
+ unwatchStream = this.bindActorStream(frame.context, sink);
3908
+ }
3788
3909
  this.runFrame(frame);
3789
3910
  });
3790
3911
  }
@@ -4038,7 +4159,7 @@ class ExecutorV1 {
4038
4159
  wake();
4039
4160
  }
4040
4161
  }
4041
- restartChild(owner, child, debts) {
4162
+ restartChild(owner, child, debts, delayElapsed = false) {
4042
4163
  if (!owner.alive ||
4043
4164
  owner.specs[child].restart === "temporary" ||
4044
4165
  owner.spent.has(child)) {
@@ -4052,6 +4173,24 @@ class ExecutorV1 {
4052
4173
  return;
4053
4174
  }
4054
4175
  const now = this.trace.virtualTimeMs;
4176
+ if (owner.backoff && debts.length > 0 && !delayElapsed) {
4177
+ const previous = owner.restartDelays.get(child);
4178
+ const healthy = previous?.publishedAt != null &&
4179
+ now - previous.publishedAt >= owner.layout.window_secs * 1000;
4180
+ const doubled = previous ? previous.delay * 2n : owner.backoff.initial;
4181
+ const delay = !previous || healthy
4182
+ ? owner.backoff.initial
4183
+ : doubled < owner.backoff.maximum
4184
+ ? doubled
4185
+ : owner.backoff.maximum;
4186
+ owner.restartDelays.set(child, { delay, publishedAt: null });
4187
+ owner.recovering.add(child);
4188
+ owner.restartTimers.set(child, this.scheduler.after(delay, () => {
4189
+ owner.restartTimers.delete(child);
4190
+ this.restartChild(owner, child, debts, true);
4191
+ }));
4192
+ return;
4193
+ }
4055
4194
  owner.restartTimes = owner.restartTimes.filter((at) => now - at < owner.layout.window_secs * 1000);
4056
4195
  if (owner.restartTimes.length >= owner.layout.max_restarts) {
4057
4196
  this.escalateSupervisor(owner, debts);
@@ -4106,7 +4245,12 @@ class ExecutorV1 {
4106
4245
  this.restartChild(owner, pending.child, pending.debts);
4107
4246
  return;
4108
4247
  }
4109
- this.startChild(owner, index, start, () => {
4248
+ this.startChild(owner, index, () => {
4249
+ const delay = owner.restartDelays.get(index);
4250
+ if (delay)
4251
+ delay.publishedAt = this.trace.virtualTimeMs;
4252
+ start();
4253
+ }, () => {
4110
4254
  owner.restartBusy = false;
4111
4255
  this.restartChild(owner, index, debts);
4112
4256
  });
@@ -4182,6 +4326,9 @@ class ExecutorV1 {
4182
4326
  if (!owner.alive)
4183
4327
  return;
4184
4328
  owner.alive = false;
4329
+ for (const cancel of owner.restartTimers.values())
4330
+ cancel();
4331
+ owner.restartTimers.clear();
4185
4332
  if (owner.constructor) {
4186
4333
  owner.constructor.pendingCancellation ??= {
4187
4334
  kind: "panic",
@@ -659,7 +659,7 @@ export type RuntimeCallFamily = {
659
659
  format: EncodingFormat;
660
660
  op: EncodingOp;
661
661
  };
662
- } | "ActorDemonitor" | "ActorLink" | "ActorMonitor" | "ActorCaller" | "ActorRefusalLog" | "ActorUnlink" | "AutoMutexAlloc" | "AutoMutexFree" | "AutoMutexLock" | "AutoMutexUnlock" | "DequePopFront" | "DequePopBack" | "BytesAppend" | "BytesClear" | "BytesConcat" | "BytesContains" | "BytesDecodeUtf8" | "BytesDecodeUtf8Lossy" | "BytesEndsWith" | "BytesGet" | "BytesIndex" | "BytesIsEmpty" | "BytesLen" | "BytesPop" | "BytesPush" | "BytesSet" | "BytesSlice" | "BytesSliceFrom" | "BytesStartsWith" | "BytesNew" | "CancelTokenIsRequested" | "CancelTokenRelease" | "CancelTokenRetain" | "ActorRequestRelease" | "ActorCallFree" | "ActorRequestTake" | "DurationAbs" | "DurationHours" | "DurationIsZero" | "DurationMicros" | "DurationMillis" | "DurationMins" | "DurationNanos" | "DurationSecs" | "DynBoxAlloc" | "DynBoxFree" | "HashMapContainsKeyLayout" | "HashMapClearLayout" | "HashMapCloneLayout" | "HashMapEntriesLayout" | "HashMapFreeLayout" | "HashMapGetLayout" | "HashMapInsertLayout" | "HashMapKeysLayout" | "HashMapLenLayout" | "HashMapNew" | "HashMapNewWithLayout" | "HashMapRemoveLayout" | "HashMapValuesLayout" | "HashSetContainsLayout" | "HashSetClearLayout" | "HashSetCloneLayout" | "HashSetFreeLayout" | "HashSetInsertLayout" | "HashSetIsEmptyLayout" | "HashSetLenLayout" | "HashSetNew" | "HashSetNewWithLayout" | "HashSetRemoveLayout" | "HashSetToVecLayout" | "InstantDurationSince" | "InstantElapsed" | "InstantNow" | "TimestampNow" | {
662
+ } | "ActorDemonitor" | "ActorLink" | "ActorMonitor" | "ActorCaller" | "ActorRefusalLog" | "ActorUnlink" | "AutoMutexAlloc" | "AutoMutexFree" | "AutoMutexLock" | "AutoMutexUnlock" | "DequePopFront" | "DequePopBack" | "BytesAppend" | "BytesClear" | "BytesConcat" | "BytesContains" | "BytesDecodeUtf8" | "BytesDecodeUtf8Lossy" | "BytesEndsWith" | "BytesGet" | "BytesIndex" | "BytesIsEmpty" | "BytesLen" | "BytesPop" | "BytesPush" | "BytesSet" | "BytesSlice" | "BytesSliceFrom" | "BytesStartsWith" | "BytesNew" | "CancelTokenIsRequested" | "CancelTokenRelease" | "CancelTokenRetain" | "ContextActorId" | "ContextSupervisor" | "ContextTraceSpan" | "ActorRequestRelease" | "ActorCallFree" | "ActorRequestTake" | "DurationAbs" | "DurationHours" | "DurationIsZero" | "DurationMicros" | "DurationMillis" | "DurationMins" | "DurationNanos" | "DurationSecs" | "DynBoxAlloc" | "DynBoxFree" | "HashMapContainsKeyLayout" | "HashMapClearLayout" | "HashMapCloneLayout" | "HashMapEntriesLayout" | "HashMapFreeLayout" | "HashMapGetLayout" | "HashMapInsertLayout" | "HashMapKeysLayout" | "HashMapLenLayout" | "HashMapNew" | "HashMapNewWithLayout" | "HashMapRemoveLayout" | "HashMapValuesLayout" | "HashSetContainsLayout" | "HashSetClearLayout" | "HashSetCloneLayout" | "HashSetFreeLayout" | "HashSetInsertLayout" | "HashSetIsEmptyLayout" | "HashSetLenLayout" | "HashSetNew" | "HashSetNewWithLayout" | "HashSetRemoveLayout" | "HashSetToVecLayout" | "InstantDurationSince" | "InstantElapsed" | "InstantNow" | "TimestampNow" | {
663
663
  MathIntrinsic: MathIntrinsic;
664
664
  } | {
665
665
  IntMethod: [IntBitOp, IntMethodWidth];
@@ -791,6 +791,7 @@ export type SupervisorShape = {
791
791
  strategy: RestartStrategy;
792
792
  max_restarts: number;
793
793
  window_secs: number;
794
+ backoff?: boolean;
794
795
  children: Array<SupervisorChild>;
795
796
  startup_order?: Array<number>;
796
797
  };