1024 lines · 34.3 KB · TypeScript
RawDownload
import type {  HostPowerSnapshot,  ResourceMonitorCapabilities,  ResourceMonitorCommand,  ResourceMonitorEvent,  ResourceMonitorExternalProcess,  ResourceMonitorHelloEvent,  ResourceMonitorSnapshotEvent,  ResourceTelemetrySourceStatus,} from "@t3tools/contracts";import {  RESOURCE_MONITOR_PROTOCOL_VERSION,  ResourceMonitorCommand as ResourceMonitorCommandSchema,  ResourceMonitorEvent as ResourceMonitorEventSchema,} from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as PubSub from "effect/PubSub";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Result from "effect/Result";import * as Schema from "effect/Schema";import * as Semaphore from "effect/Semaphore";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as Ndjson from "effect/unstable/encoding/Ndjson";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts";import { ServerConfig } from "../config.ts";import { subscribeBeforeSnapshotWithoutMutex } from "../utils/subscribeBeforeSnapshot.ts"; const SAMPLE_INTERVAL_MS = 1_000;const UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS = 5_000;const BATTERY_SAMPLE_INTERVAL_MS = 5_000;const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000;const HANDSHAKE_TIMEOUT = Duration.seconds(5);const SAMPLE_REQUEST_TIMEOUT = Duration.seconds(5);const HISTORY_REQUEST_TIMEOUT = Duration.seconds(15);const INITIAL_RESTART_DELAY = Duration.millis(500);const MAX_RESTART_DELAY = Duration.seconds(10);const FAILURE_WINDOW_MS = 60_000;const MAX_FAILURES_PER_WINDOW = 5; export class NativeTelemetrySpawnFailed extends Schema.TaggedErrorClass<NativeTelemetrySpawnFailed>()(  "NativeTelemetrySpawnFailed",  {    path: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Failed to start resource monitor '${this.path}'.`;  }} export class NativeTelemetryHandshakeTimedOut extends Schema.TaggedErrorClass<NativeTelemetryHandshakeTimedOut>()(  "NativeTelemetryHandshakeTimedOut",  {    timeoutMs: Schema.Number,  },) {  override get message(): string {    return `Resource monitor handshake timed out after ${this.timeoutMs}ms.`;  }} export class NativeTelemetryRequestTimedOut extends Schema.TaggedErrorClass<NativeTelemetryRequestTimedOut>()(  "NativeTelemetryRequestTimedOut",  {    operation: Schema.Literals(["readHistory", "sampleNow"]),    timeoutMs: Schema.Number,  },) {  override get message(): string {    return `Resource monitor '${this.operation}' request timed out after ${this.timeoutMs}ms.`;  }} export class NativeTelemetryProtocolMismatch extends Schema.TaggedErrorClass<NativeTelemetryProtocolMismatch>()(  "NativeTelemetryProtocolMismatch",  {    expectedVersion: Schema.Number,    receivedVersion: Schema.Number,  },) {  override get message(): string {    return `Resource monitor protocol ${this.receivedVersion} is incompatible with expected protocol ${this.expectedVersion}.`;  }} export class NativeTelemetryDecodeFailed extends Schema.TaggedErrorClass<NativeTelemetryDecodeFailed>()(  "NativeTelemetryDecodeFailed",  {    cause: Schema.Defect(),  },) {  override get message(): string {    return "Failed to decode resource monitor output.";  }} export class NativeTelemetryCommandFailed extends Schema.TaggedErrorClass<NativeTelemetryCommandFailed>()(  "NativeTelemetryCommandFailed",  {    operation: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Resource monitor command '${this.operation}' failed.`;  }} export class NativeTelemetryExited extends Schema.TaggedErrorClass<NativeTelemetryExited>()(  "NativeTelemetryExited",  {    exitCode: Schema.Number,  },) {  override get message(): string {    return `Resource monitor exited with code ${this.exitCode}.`;  }} export class NativeTelemetryStreamClosed extends Schema.TaggedErrorClass<NativeTelemetryStreamClosed>()(  "NativeTelemetryStreamClosed",  {},) {  override get message(): string {    return "Resource monitor event stream closed unexpectedly.";  }} export class NativeTelemetryUnavailable extends Schema.TaggedErrorClass<NativeTelemetryUnavailable>()(  "NativeTelemetryUnavailable",  {    reason: Schema.String,  },) {  override get message(): string {    return `Resource monitor is unavailable: ${this.reason}`;  }} export type NativeTelemetryClientError =  | ResourceMonitorBinary.ResourceMonitorBinaryError  | NativeTelemetrySpawnFailed  | NativeTelemetryHandshakeTimedOut  | NativeTelemetryRequestTimedOut  | NativeTelemetryProtocolMismatch  | NativeTelemetryDecodeFailed  | NativeTelemetryCommandFailed  | NativeTelemetryExited  | NativeTelemetryStreamClosed  | NativeTelemetryUnavailable; export interface NativeTelemetryClientHealth {  readonly status: ResourceTelemetrySourceStatus;  readonly hello: Option.Option<ResourceMonitorHelloEvent>;  readonly lastSampleAt: Option.Option<DateTime.Utc>;  readonly lastError: Option.Option<string>;  readonly restartCount: number;  readonly sampleIntervalMs: number;} export interface NativeTelemetrySnapshot {  readonly generation: number;  readonly snapshot: ResourceMonitorSnapshotEvent;} export class NativeTelemetryClient extends Context.Service<  NativeTelemetryClient,  {    readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>;    readonly snapshots: Stream.Stream<NativeTelemetrySnapshot, NativeTelemetryClientError>;    readonly readHistory: (      windowMs: number,    ) => Effect.Effect<ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError>;    readonly setExternalProcesses: (      processes: ReadonlyArray<ResourceMonitorExternalProcess>,    ) => Effect.Effect<void, NativeTelemetryClientError>;    readonly setHostPowerState: (      snapshot: HostPowerSnapshot,    ) => Effect.Effect<void, NativeTelemetryClientError>;    readonly sampleNow: Effect.Effect<NativeTelemetrySnapshot, NativeTelemetryClientError>;    readonly retry: Effect.Effect<boolean>;    readonly health: Effect.Effect<NativeTelemetryClientHealth>;    readonly subscribeHealth: Effect.Effect<      {        readonly latest: NativeTelemetryClientHealth;        readonly changes: Stream.Stream<NativeTelemetryClientHealth>;      },      never,      Scope.Scope    >;  }>()("t3/resourceTelemetry/NativeTelemetryClient") {} interface ClientState {  readonly status: ResourceTelemetrySourceStatus;  readonly handle: Option.Option<ChildProcessSpawner.ChildProcessHandle>;  readonly hello: Option.Option<ResourceMonitorHelloEvent>;  readonly lastSampleAt: Option.Option<DateTime.Utc>;  readonly lastError: Option.Option<string>;  readonly restartCount: number;} export interface CollectionControl {  readonly hostPower: HostPowerSnapshot;  readonly liveSubscriberCount: number;  readonly sampleIntervalMs: number;} interface PendingHistoryRequest {  readonly deferred: Deferred.Deferred<    ReadonlyArray<ResourceMonitorSnapshotEvent>,    NativeTelemetryClientError  >;  readonly snapshots: ReadonlyArray<ResourceMonitorSnapshotEvent>;} const initialState: ClientState = {  status: "starting",  handle: Option.none(),  hello: Option.none(),  lastSampleAt: Option.none(),  lastError: Option.none(),  restartCount: 0,}; function toHealth(state: ClientState, sampleIntervalMs: number): NativeTelemetryClientHealth {  return {    status: state.status,    hello: state.hello,    lastSampleAt: state.lastSampleAt,    lastError: state.lastError,    restartCount: state.restartCount,    sampleIntervalMs,  };} function isThermallyConstrained(snapshot: HostPowerSnapshot): boolean {  return snapshot.thermalState === "serious" || snapshot.thermalState === "critical";} export function resolveNativeSampleIntervalMs(  snapshot: HostPowerSnapshot,  liveSubscriberCount: number,): number {  if (snapshot.stale || snapshot.source === "unknown") {    return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS;  }  if (    snapshot.suspended ||    snapshot.locked === "true" ||    snapshot.lowPowerMode === "true" ||    isThermallyConstrained(snapshot)  ) {    return CONSTRAINED_SAMPLE_INTERVAL_MS;  }  if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS;  return SAMPLE_INTERVAL_MS;} export function commitCollectionControlUpdate<E, R>(  desiredState: Ref.Ref<CollectionControl>,  appliedState: Ref.Ref<CollectionControl>,  update: (current: CollectionControl) => CollectionControl,  apply: (previous: CollectionControl, next: CollectionControl) => Effect.Effect<void, E, R>,): Effect.Effect<readonly [CollectionControl, CollectionControl], E, R> {  return Effect.gen(function* () {    const [previousDesired, next] = yield* Ref.modify(desiredState, (previous) => {      const next = update(previous);      return [[previous, next] as const, next];    });    const previousApplied = yield* Ref.get(appliedState);    yield* apply(previousApplied, next);    yield* Ref.set(appliedState, next);    return [previousDesired, next] as const;  });} export function synchronizeCollectionControlOnStart<E1, R1, E2, R2>(  mutex: Semaphore.Semaphore,  desiredState: Ref.Ref<CollectionControl>,  appliedState: Ref.Ref<CollectionControl>,  apply: (control: CollectionControl) => Effect.Effect<void, E1, R1>,  markReady: Effect.Effect<void, E2, R2>,) {  return mutex.withPermits(1)(    Effect.gen(function* () {      const control = yield* Ref.get(desiredState);      yield* apply(control);      yield* Ref.set(appliedState, control);      yield* markReady;      return control;    }),  );} const decodeMonitorEvent: (  value: unknown,) => Effect.Effect<ResourceMonitorEvent, Schema.SchemaError> = Schema.decodeUnknownEffect(  ResourceMonitorEventSchema,);const encodeMonitorCommand = Schema.encodeEffect(  Schema.fromJsonString(ResourceMonitorCommandSchema),);const isProtocolMismatch = Schema.is(NativeTelemetryProtocolMismatch);const isDecodeFailed = Schema.is(NativeTelemetryDecodeFailed);const isCommandFailed = Schema.is(NativeTelemetryCommandFailed); function eventVersion(value: unknown): number | undefined {  if (typeof value !== "object" || value === null) return undefined;  const version = Reflect.get(value, "version");  return typeof version === "number" ? version : undefined;} function restartDelay(attempt: number): Duration.Duration {  return Duration.min(Duration.times(INITIAL_RESTART_DELAY, 2 ** attempt), MAX_RESTART_DELAY);} export function retainRecentNativeTelemetryFailures(  failures: ReadonlyArray<number>,  now: number,): ReadonlyArray<number> {  return failures.filter((failedAt) => now - failedAt <= FAILURE_WINDOW_MS);} function errorMessage(error: NativeTelemetryClientError): string {  return error.message;} export function nativeTelemetrySupervisorFailureMessage(_cause: Cause.Cause<unknown>): string {  return "Resource monitor supervisor stopped unexpectedly.";} export function canRequestNativeTelemetryRetry(  status: ResourceTelemetrySourceStatus,  hasHandle: boolean,): boolean {  return status !== "healthy" && status !== "starting" && !hasHandle;} export function canCommandNativeTelemetrySidecar(  status: ResourceTelemetrySourceStatus,  hasHandle: boolean,): boolean {  return hasHandle && (status === "healthy" || status === "degraded");} export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(function* () {  const binary = yield* ResourceMonitorBinary.ResourceMonitorBinary;  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;  const crypto = yield* Crypto.Crypto;  const config = yield* ServerConfig;  const initializedAt = yield* DateTime.now;  const state = yield* Ref.make(initialState);  const collectionControl = yield* Ref.make<CollectionControl>({    hostPower: {      source: "unknown",      idle: "unknown",      idleSeconds: null,      locked: "unknown",      suspended: false,      onBattery: "unknown",      lowPowerMode: "unknown",      thermalState: "unknown",      stale: true,      updatedAt: initializedAt,    },    liveSubscriberCount: 0,    sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS,  });  const appliedCollectionControl = yield* Ref.make(yield* Ref.get(collectionControl));  const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]);  const pendingSamples = yield* Ref.make(    new Map<string, Deferred.Deferred<NativeTelemetrySnapshot, NativeTelemetryClientError>>(),  );  const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>());  const snapshots = yield* PubSub.sliding<NativeTelemetrySnapshot>(8);  const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4);  const retryQueue = yield* Queue.sliding<void>(1);  const commandMutex = yield* Semaphore.make(1);  const controlMutex = yield* Semaphore.make(1);  const currentHealth = Effect.all([Ref.get(state), Ref.get(collectionControl)]).pipe(    Effect.map(([current, control]) => toHealth(current, control.sampleIntervalMs)),  );  const publishHealth = currentHealth.pipe(    Effect.flatMap((health) => PubSub.publish(healthChanges, health)),    Effect.asVoid,  );   const failPending = (error: NativeTelemetryClientError) =>    Effect.gen(function* () {      const samples = yield* Ref.getAndSet(pendingSamples, new Map());      const histories = yield* Ref.getAndSet(pendingHistories, new Map());      yield* Effect.forEach(samples.values(), (deferred) => Deferred.fail(deferred, error), {        discard: true,      });      yield* Effect.forEach(        histories.values(),        (request) => Deferred.fail(request.deferred, error),        { discard: true },      );    });   const writeCommand = (    handle: ChildProcessSpawner.ChildProcessHandle,    command: ResourceMonitorCommand,  ): Effect.Effect<void, NativeTelemetryClientError> =>    commandMutex.withPermits(1)(      encodeMonitorCommand(command).pipe(        Effect.map((encoded) => `${encoded}\n`),        Effect.mapError(          (cause) =>            new NativeTelemetryCommandFailed({              operation: command.type,              cause,            }),        ),        Effect.flatMap((encoded) =>          Stream.run(Stream.encodeText(Stream.make(encoded)), handle.stdin),        ),        Effect.mapError(          (cause) =>            new NativeTelemetryCommandFailed({              operation: command.type,              cause,            }),        ),      ),    );   const processEvent = (    event: ResourceMonitorEvent,    helloDeferred: Deferred.Deferred<ResourceMonitorHelloEvent>,    generation: number,  ): Effect.Effect<void, NativeTelemetryClientError> => {    switch (event.type) {      case "hello":        return Ref.update(state, (current) => ({          ...current,          status: "starting" as const,          hello: Option.some(event),          lastError: Option.none(),        })).pipe(          Effect.andThen(publishHealth),          Effect.andThen(Deferred.succeed(helloDeferred, event)),          Effect.asVoid,        );      case "snapshot":        return Effect.gen(function* () {          const nativeSnapshot = { generation, snapshot: event } satisfies NativeTelemetrySnapshot;          const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs);          yield* Ref.update(state, (current) => ({            ...current,            status: "healthy" as const,            lastSampleAt: Option.some(sampledAt),            lastError: Option.none(),          }));          yield* publishHealth;          yield* PubSub.publish(snapshots, nativeSnapshot);          if (event.requestId) {            const deferred = yield* Ref.modify(pendingSamples, (pending) => {              const next = new Map(pending);              const current = next.get(event.requestId!);              next.delete(event.requestId!);              return [Option.fromUndefinedOr(current), next];            });            if (Option.isSome(deferred)) {              yield* Deferred.succeed(deferred.value, nativeSnapshot);            }          }        });      case "historyChunk":        return Effect.gen(function* () {          const latestSnapshot = event.snapshots.at(-1);          yield* Ref.update(state, (current) => ({            ...current,            status: "healthy" as const,            lastSampleAt: latestSnapshot              ? Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs))              : current.lastSampleAt,            lastError: Option.none(),          }));          yield* publishHealth;          const completed = yield* Ref.modify(pendingHistories, (pending) => {            const request = pending.get(event.requestId);            if (!request) return [Option.none(), pending] as const;            const snapshots = [...request.snapshots, ...event.snapshots];

Lines 1–500 of 1024.