| ... | @@ -1,8 +1,10 @@ | ... | @@ -1,8 +1,10 @@ |
| 1 | import type { | 1 | import { |
| 2 | ServerProcessResourceHistoryBucket, | 2 | ServerProcessResourceHistoryFailureTag, |
| 3 | ServerProcessResourceHistoryInput, | 3 | type ServerProcessResourceHistoryBucket, |
| 4 | ServerProcessResourceHistoryResult, | 4 | type ServerProcessResourceHistoryFailureTag as ServerProcessResourceHistoryFailureTagType, |
| 5 | ServerProcessResourceHistorySummary, | 5 | type ServerProcessResourceHistoryInput, |
| | 6 | type ServerProcessResourceHistoryResult, |
| | 7 | type ServerProcessResourceHistorySummary, |
| 6 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 7 | import * as Context from "effect/Context"; | 9 | import * as Context from "effect/Context"; |
| 8 | import * as DateTime from "effect/DateTime"; | 10 | import * as DateTime from "effect/DateTime"; |
| ... | @@ -10,6 +12,7 @@ import * as Effect from "effect/Effect"; | ... | @@ -10,6 +12,7 @@ import * as Effect from "effect/Effect"; |
| 10 | import * as Layer from "effect/Layer"; | 12 | import * as Layer from "effect/Layer"; |
| 11 | import * as Option from "effect/Option"; | 13 | import * as Option from "effect/Option"; |
| 12 | import * as Ref from "effect/Ref"; | 14 | import * as Ref from "effect/Ref"; |
| | 15 | import * as Schema from "effect/Schema"; |
| 13 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; | 16 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; |
| 14 | | 17 | |
| 15 | import * as ProcessDiagnostics from "./ProcessDiagnostics.ts"; | 18 | import * as ProcessDiagnostics from "./ProcessDiagnostics.ts"; |
| ... | @@ -31,9 +34,21 @@ export interface ProcessResourceSample { | ... | @@ -31,9 +34,21 @@ export interface ProcessResourceSample { |
| 31 | readonly isServerRoot: boolean; | 34 | readonly isServerRoot: boolean; |
| 32 | } | 35 | } |
| 33 | | 36 | |
| | 37 | export class ProcessResourceSamplingError extends Schema.TaggedErrorClass<ProcessResourceSamplingError>()( |
| | 38 | "ProcessResourceSamplingError", |
| | 39 | { |
| | 40 | failureTag: ServerProcessResourceHistoryFailureTag, |
| | 41 | cause: Schema.Defect(), |
| | 42 | }, |
| | 43 | ) { |
| | 44 | override get message(): string { |
| | 45 | return `Failed to sample process resources (${this.failureTag}).`; |
| | 46 | } |
| | 47 | } |
| | 48 | |
| 34 | interface MonitorState { | 49 | interface MonitorState { |
| 35 | readonly samples: ReadonlyArray<ProcessResourceSample>; | 50 | readonly samples: ReadonlyArray<ProcessResourceSample>; |
| 36 | readonly lastError: string | null; | 51 | readonly lastFailure: ProcessResourceSamplingError | null; |
| 37 | } | 52 | } |
| 38 | | 53 | |
| 39 | export class ProcessResourceMonitor extends Context.Service< | 54 | export class ProcessResourceMonitor extends Context.Service< |
| ... | @@ -218,7 +233,7 @@ export function aggregateProcessResourceHistory(input: { | ... | @@ -218,7 +233,7 @@ export function aggregateProcessResourceHistory(input: { |
| 218 | readonly readAtMs: number; | 233 | readonly readAtMs: number; |
| 219 | readonly windowMs: number; | 234 | readonly windowMs: number; |
| 220 | readonly bucketMs: number; | 235 | readonly bucketMs: number; |
| 221 | readonly lastError: string | null; | 236 | readonly lastFailure: ProcessResourceSamplingError | null; |
| 222 | }): ServerProcessResourceHistoryResult { | 237 | }): ServerProcessResourceHistoryResult { |
| 223 | const windowMs = Math.max(1_000, input.windowMs); | 238 | const windowMs = Math.max(1_000, input.windowMs); |
| 224 | const bucketMs = Math.max(1_000, input.bucketMs); | 239 | const bucketMs = Math.max(1_000, input.bucketMs); |
| ... | @@ -239,13 +254,29 @@ export function aggregateProcessResourceHistory(input: { | ... | @@ -239,13 +254,29 @@ export function aggregateProcessResourceHistory(input: { |
| 239 | totalCpuSecondsApprox, | 254 | totalCpuSecondsApprox, |
| 240 | buckets: buildBuckets({ samples, nowMs: input.readAtMs, windowMs, bucketMs }), | 255 | buckets: buildBuckets({ samples, nowMs: input.readAtMs, windowMs, bucketMs }), |
| 241 | topProcesses, | 256 | topProcesses, |
| 242 | error: input.lastError ? Option.some({ message: input.lastError }) : Option.none(), | 257 | error: input.lastFailure |
| | 258 | ? Option.some({ |
| | 259 | failureTag: input.lastFailure.failureTag, |
| | 260 | message: input.lastFailure.message, |
| | 261 | }) |
| | 262 | : Option.none(), |
| 243 | }; | 263 | }; |
| 244 | } | 264 | } |
| 245 | | 265 | |
| 246 | export const make = Effect.gen(function* () { | 266 | export const make = Effect.gen(function* () { |
| 247 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 267 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 248 | const state = yield* Ref.make<MonitorState>({ samples: [], lastError: null }); | 268 | const state = yield* Ref.make<MonitorState>({ samples: [], lastFailure: null }); |
| | 269 | |
| | 270 | const recordSamplingFailure = (cause: { |
| | 271 | readonly _tag: ServerProcessResourceHistoryFailureTagType; |
| | 272 | }) => |
| | 273 | Ref.update(state, (current) => ({ |
| | 274 | ...current, |
| | 275 | lastFailure: new ProcessResourceSamplingError({ |
| | 276 | failureTag: cause._tag, |
| | 277 | cause, |
| | 278 | }), |
| | 279 | })); |
| 249 | | 280 | |
| 250 | const sampleOnce = Effect.gen(function* () { | 281 | const sampleOnce = Effect.gen(function* () { |
| 251 | const sampledAt = yield* DateTime.now; | 282 | const sampledAt = yield* DateTime.now; |
| ... | @@ -261,15 +292,16 @@ export const make = Effect.gen(function* () { | ... | @@ -261,15 +292,16 @@ export const make = Effect.gen(function* () { |
| 261 | }); | 292 | }); |
| 262 | yield* Ref.update(state, (current) => ({ | 293 | yield* Ref.update(state, (current) => ({ |
| 263 | samples: trimSamples([...current.samples, ...samples], sampledAtMs), | 294 | samples: trimSamples([...current.samples, ...samples], sampledAtMs), |
| 264 | lastError: null, | 295 | lastFailure: null, |
| 265 | })); | 296 | })); |
| 266 | }).pipe( | 297 | }).pipe( |
| 267 | Effect.catch((error: unknown) => | 298 | Effect.catchTags({ |
| 268 | Ref.update(state, (current) => ({ | 299 | ProcessDiagnosticsQueryTimeoutError: recordSamplingFailure, |
| 269 | ...current, | 300 | ProcessDiagnosticsQueryFailedError: recordSamplingFailure, |
| 270 | lastError: error instanceof Error ? error.message : "Failed to sample process resources.", | 301 | ProcessDiagnosticsServerProcessSignalError: recordSamplingFailure, |
| 271 | })), | 302 | ProcessDiagnosticsNotDescendantError: recordSamplingFailure, |
| 272 | ), | 303 | ProcessDiagnosticsSignalFailedError: recordSamplingFailure, |
| | 304 | }), |
| 273 | ); | 305 | ); |
| 274 | | 306 | |
| 275 | yield* Effect.forever(sampleOnce.pipe(Effect.andThen(Effect.sleep(SAMPLE_INTERVAL_MS)))).pipe( | 307 | yield* Effect.forever(sampleOnce.pipe(Effect.andThen(Effect.sleep(SAMPLE_INTERVAL_MS)))).pipe( |
| ... | @@ -287,7 +319,7 @@ export const make = Effect.gen(function* () { | ... | @@ -287,7 +319,7 @@ export const make = Effect.gen(function* () { |
| 287 | readAtMs, | 319 | readAtMs, |
| 288 | windowMs: input.windowMs, | 320 | windowMs: input.windowMs, |
| 289 | bucketMs: input.bucketMs, | 321 | bucketMs: input.bucketMs, |
| 290 | lastError: current.lastError, | 322 | lastFailure: current.lastFailure, |
| 291 | }); | 323 | }); |
| 292 | }); | 324 | }); |
| 293 | | 325 | |