| ... | ... | @@ -1,8 +1,10 @@ |
| 1 | | import type { |
| 2 | | ServerProcessResourceHistoryBucket, |
| 3 | | ServerProcessResourceHistoryInput, |
| 4 | | ServerProcessResourceHistoryResult, |
| 5 | | ServerProcessResourceHistorySummary, |
| 1 | import { |
| 2 | ServerProcessResourceHistoryFailureTag, |
| 3 | type ServerProcessResourceHistoryBucket, |
| 4 | type ServerProcessResourceHistoryFailureTag as ServerProcessResourceHistoryFailureTagType, |
| 5 | type ServerProcessResourceHistoryInput, |
| 6 | type ServerProcessResourceHistoryResult, |
| 7 | type ServerProcessResourceHistorySummary, |
| 6 | 8 | } from "@t3tools/contracts"; |
| 7 | 9 | import * as Context from "effect/Context"; |
| 8 | 10 | import * as DateTime from "effect/DateTime"; |
| ... | ... | @@ -10,6 +12,7 @@ import * as Effect from "effect/Effect"; |
| 10 | 12 | import * as Layer from "effect/Layer"; |
| 11 | 13 | import * as Option from "effect/Option"; |
| 12 | 14 | import * as Ref from "effect/Ref"; |
| 15 | import * as Schema from "effect/Schema"; |
| 13 | 16 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; |
| 14 | 17 | |
| 15 | 18 | import * as ProcessDiagnostics from "./ProcessDiagnostics.ts"; |
| ... | ... | @@ -31,9 +34,21 @@ export interface ProcessResourceSample { |
| 31 | 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 | 49 | interface MonitorState { |
| 35 | 50 | readonly samples: ReadonlyArray<ProcessResourceSample>; |
| 36 | | readonly lastError: string | null; |
| 51 | readonly lastFailure: ProcessResourceSamplingError | null; |
| 37 | 52 | } |
| 38 | 53 | |
| 39 | 54 | export class ProcessResourceMonitor extends Context.Service< |
| ... | ... | @@ -218,7 +233,7 @@ export function aggregateProcessResourceHistory(input: { |
| 218 | 233 | readonly readAtMs: number; |
| 219 | 234 | readonly windowMs: number; |
| 220 | 235 | readonly bucketMs: number; |
| 221 | | readonly lastError: string | null; |
| 236 | readonly lastFailure: ProcessResourceSamplingError | null; |
| 222 | 237 | }): ServerProcessResourceHistoryResult { |
| 223 | 238 | const windowMs = Math.max(1_000, input.windowMs); |
| 224 | 239 | const bucketMs = Math.max(1_000, input.bucketMs); |
| ... | ... | @@ -239,13 +254,29 @@ export function aggregateProcessResourceHistory(input: { |
| 239 | 254 | totalCpuSecondsApprox, |
| 240 | 255 | buckets: buildBuckets({ samples, nowMs: input.readAtMs, windowMs, bucketMs }), |
| 241 | 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 | 266 | export const make = Effect.gen(function* () { |
| 247 | 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 | 281 | const sampleOnce = Effect.gen(function* () { |
| 251 | 282 | const sampledAt = yield* DateTime.now; |
| ... | ... | @@ -261,15 +292,16 @@ export const make = Effect.gen(function* () { |
| 261 | 292 | }); |
| 262 | 293 | yield* Ref.update(state, (current) => ({ |
| 263 | 294 | samples: trimSamples([...current.samples, ...samples], sampledAtMs), |
| 264 | | lastError: null, |
| 295 | lastFailure: null, |
| 265 | 296 | })); |
| 266 | 297 | }).pipe( |
| 267 | | Effect.catch((error: unknown) => |
| 268 | | Ref.update(state, (current) => ({ |
| 269 | | ...current, |
| 270 | | lastError: error instanceof Error ? error.message : "Failed to sample process resources.", |
| 271 | | })), |
| 272 | | ), |
| 298 | Effect.catchTags({ |
| 299 | ProcessDiagnosticsQueryTimeoutError: recordSamplingFailure, |
| 300 | ProcessDiagnosticsQueryFailedError: recordSamplingFailure, |
| 301 | ProcessDiagnosticsServerProcessSignalError: recordSamplingFailure, |
| 302 | ProcessDiagnosticsNotDescendantError: recordSamplingFailure, |
| 303 | ProcessDiagnosticsSignalFailedError: recordSamplingFailure, |
| 304 | }), |
| 273 | 305 | ); |
| 274 | 306 | |
| 275 | 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 | 319 | readAtMs, |
| 288 | 320 | windowMs: input.windowMs, |
| 289 | 321 | bucketMs: input.bucketMs, |
| 290 | | lastError: current.lastError, |
| 322 | lastFailure: current.lastFailure, |
| 291 | 323 | }); |
| 292 | 324 | }); |
| 293 | 325 | |