bazis 0.97.9 → 0.97.10
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.
- package/core/background/BackgroundService.ts +47 -15
- package/core/di/extensions/hosted-service.ts +11 -0
- package/core/di/extensions/index.ts +1 -1
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/kernel/LifecycleCoordinator.ts +5 -0
- package/core/kernel/SupervisedHostedService.ts +8 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HostedService } from "../di";
|
|
1
|
+
import type { HostedService, HostedServiceDiagnostics } from "../di";
|
|
2
2
|
import { backgroundOptionsOf } from "./decorator";
|
|
3
3
|
import { delay } from "./delay";
|
|
4
4
|
import { redactSensitive } from "../../library/redaction";
|
|
@@ -54,6 +54,9 @@ export abstract class BackgroundService implements HostedService {
|
|
|
54
54
|
private controller?: AbortController;
|
|
55
55
|
private runPromise?: Promise<void>;
|
|
56
56
|
|
|
57
|
+
/** The application logger, passed by the kernel; console until then. */
|
|
58
|
+
private diagnostics?: HostedServiceDiagnostics;
|
|
59
|
+
|
|
57
60
|
public constructor(options?: BackgroundServiceOptions) {
|
|
58
61
|
// Explicit `super(...)` options win over `@Background({...})` metadata.
|
|
59
62
|
const resolved = { ...backgroundOptionsOf(this.constructor), ...options };
|
|
@@ -65,13 +68,22 @@ export abstract class BackgroundService implements HostedService {
|
|
|
65
68
|
/** The background workload. Must observe `signal` for cooperative shutdown. */
|
|
66
69
|
protected abstract execute(signal: AbortSignal): void | Promise<void>;
|
|
67
70
|
|
|
71
|
+
public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
|
|
72
|
+
this.diagnostics = diagnostics;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Reports a failure of this service: the application logger, or the console without one. */
|
|
76
|
+
protected reportFailure(message: string, error: unknown, fields: Readonly<Record<string, unknown>> = {}): void {
|
|
77
|
+
report(this.diagnostics, "error", this.constructor.name, message, { ...fields, error: redactSensitive(error) });
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
public start(): void {
|
|
69
81
|
if (this.controller) {
|
|
70
82
|
return;
|
|
71
83
|
}
|
|
72
84
|
this.controller = new AbortController();
|
|
73
85
|
this.runPromise = this.runSupervised(this.controller.signal)
|
|
74
|
-
.catch((error: unknown) =>
|
|
86
|
+
.catch((error: unknown) => this.reportFailure("supervisor failed", error));
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
public async stop(): Promise<void> {
|
|
@@ -83,7 +95,9 @@ export abstract class BackgroundService implements HostedService {
|
|
|
83
95
|
this.controller = undefined;
|
|
84
96
|
this.runPromise = undefined;
|
|
85
97
|
if (pending) {
|
|
86
|
-
await waitWithTimeout(pending, this.stopTimeoutMs, this.constructor.name
|
|
98
|
+
await waitWithTimeout(pending, this.stopTimeoutMs, () => report(this.diagnostics, "warn", this.constructor.name,
|
|
99
|
+
`did not stop within ${this.stopTimeoutMs}ms: shutdown continues, but its unfinished work keeps the process alive until it ends`,
|
|
100
|
+
{ stopTimeoutMs: this.stopTimeoutMs }));
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
|
|
@@ -104,6 +118,10 @@ export abstract class BackgroundService implements HostedService {
|
|
|
104
118
|
}
|
|
105
119
|
this.reportError(error, restarts);
|
|
106
120
|
if (restarts >= maxRestarts) {
|
|
121
|
+
// Without this line the service would just go quiet.
|
|
122
|
+
report(this.diagnostics, "error", this.constructor.name, maxRestarts === 0
|
|
123
|
+
? "stopped after a crash and will not run again (no restart policy)"
|
|
124
|
+
: `stopped after ${maxRestarts} restart${maxRestarts === 1 ? "" : "s"} and will not run again`, { restarts });
|
|
107
125
|
return;
|
|
108
126
|
}
|
|
109
127
|
restarts += 1;
|
|
@@ -117,9 +135,9 @@ export abstract class BackgroundService implements HostedService {
|
|
|
117
135
|
|
|
118
136
|
private reportError(error: unknown, restarts: number): void {
|
|
119
137
|
if (this.restart.onError) {
|
|
120
|
-
observeDiagnostic(() => this.restart.onError!(error, restarts), this.
|
|
138
|
+
observeDiagnostic(() => this.restart.onError!(error, restarts), (failure) => this.reportFailure("onError callback failed", failure));
|
|
121
139
|
} else {
|
|
122
|
-
|
|
140
|
+
this.reportFailure("crashed", error, { restarts });
|
|
123
141
|
}
|
|
124
142
|
}
|
|
125
143
|
}
|
|
@@ -162,7 +180,7 @@ export abstract class PeriodicBackgroundService extends BackgroundService {
|
|
|
162
180
|
|
|
163
181
|
/** Hook for tick failures; default logs. The loop continues regardless. */
|
|
164
182
|
protected onTickError(error: unknown): void {
|
|
165
|
-
|
|
183
|
+
this.reportFailure("tick failed", error);
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
protected override async execute(signal: AbortSignal): Promise<void> {
|
|
@@ -176,7 +194,7 @@ export abstract class PeriodicBackgroundService extends BackgroundService {
|
|
|
176
194
|
if (signal.aborted) {
|
|
177
195
|
return;
|
|
178
196
|
}
|
|
179
|
-
observeDiagnostic(() => this.onTickError(error), this.
|
|
197
|
+
observeDiagnostic(() => this.onTickError(error), (failure) => this.reportFailure("onTickError failed", failure));
|
|
180
198
|
}
|
|
181
199
|
if (signal.aborted) {
|
|
182
200
|
return;
|
|
@@ -187,20 +205,34 @@ export abstract class PeriodicBackgroundService extends BackgroundService {
|
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
/** Diagnostics are best-effort; even an accidentally async observer cannot own the loop. */
|
|
190
|
-
function observeDiagnostic(callback: () => unknown,
|
|
208
|
+
function observeDiagnostic(callback: () => unknown, onFailure: (error: unknown) => void): void {
|
|
191
209
|
try {
|
|
192
|
-
void Promise.resolve(callback()).catch(
|
|
210
|
+
void Promise.resolve(callback()).catch(onFailure);
|
|
193
211
|
} catch (error) {
|
|
194
|
-
|
|
212
|
+
onFailure(error);
|
|
195
213
|
}
|
|
196
214
|
}
|
|
197
215
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
/** One line per event: "background Crasher crashed" with the service name in the fields. */
|
|
217
|
+
function report(
|
|
218
|
+
diagnostics: HostedServiceDiagnostics | undefined,
|
|
219
|
+
level: "error" | "warn",
|
|
220
|
+
name: string,
|
|
221
|
+
message: string,
|
|
222
|
+
fields: Readonly<Record<string, unknown>>,
|
|
223
|
+
): void {
|
|
224
|
+
try {
|
|
225
|
+
if (diagnostics) {
|
|
226
|
+
diagnostics[level](`background ${name} ${message}`, { service: name, ...fields });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const { error, ...rest } = fields;
|
|
230
|
+
const details = error === undefined ? (Object.keys(rest).length ? [rest] : []) : [error];
|
|
231
|
+
(level === "error" ? console.error : console.warn)(`[background:${name}] ${message}`, ...details);
|
|
232
|
+
} catch { /* A broken diagnostic sink must not terminate the workload supervisor. */ }
|
|
201
233
|
}
|
|
202
234
|
|
|
203
|
-
async function waitWithTimeout(promise: Promise<void>, timeoutMs: number,
|
|
235
|
+
async function waitWithTimeout(promise: Promise<void>, timeoutMs: number, onTimeout: () => void): Promise<void> {
|
|
204
236
|
const guarded = promise.catch(() => undefined);
|
|
205
237
|
if (timeoutMs <= 0) {
|
|
206
238
|
await guarded;
|
|
@@ -209,7 +241,7 @@ async function waitWithTimeout(promise: Promise<void>, timeoutMs: number, name:
|
|
|
209
241
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
210
242
|
const timeout = new Promise<void>((resolve) => {
|
|
211
243
|
timer = setTimeout(() => {
|
|
212
|
-
|
|
244
|
+
onTimeout();
|
|
213
245
|
resolve();
|
|
214
246
|
}, timeoutMs);
|
|
215
247
|
});
|
|
@@ -13,9 +13,20 @@ export interface HostedServicePlanValidator {
|
|
|
13
13
|
validate(services: readonly HostedService[], signal?: AbortSignal): void | Promise<void>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Where a hosted service reports what it handles itself: crashes, restarts, a
|
|
18
|
+
* slow stop. The application logger fits it structurally.
|
|
19
|
+
*/
|
|
20
|
+
export interface HostedServiceDiagnostics {
|
|
21
|
+
error(message: string, fields?: Readonly<Record<string, unknown>>): void;
|
|
22
|
+
warn(message: string, fields?: Readonly<Record<string, unknown>>): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
export interface HostedService {
|
|
17
26
|
/** Shared validators see real service identities, including inside supervised wrappers. */
|
|
18
27
|
readonly planValidator?: HostedServicePlanValidator;
|
|
28
|
+
/** Receives the application logger before start, when one is registered. */
|
|
29
|
+
useDiagnostics?(diagnostics: HostedServiceDiagnostics): void;
|
|
19
30
|
start(signal?: AbortSignal): void | Promise<void>;
|
|
20
31
|
stop(): void | Promise<void>;
|
|
21
32
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createInstance, type ActivatorDependency } from "./activator";
|
|
2
2
|
export { Application, runApplication, type RunApplicationOptions } from "./application";
|
|
3
|
-
export { addHostedService, HOSTED_SERVICE, startHostedServices, stopHostedServices, type HostedService, type HostedServicePlanValidator } from "./hosted-service";
|
|
3
|
+
export { addHostedService, HOSTED_SERVICE, startHostedServices, stopHostedServices, type HostedService, type HostedServiceDiagnostics, type HostedServicePlanValidator } from "./hosted-service";
|
|
4
4
|
export { SERVICE_PROVIDER } from "./service-provider-token";
|
|
5
5
|
export {
|
|
6
6
|
addOptions,
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.97.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.97.10" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateOptionsOnStart, type HostedService } from "../di";
|
|
2
2
|
import type { Token } from "../di";
|
|
3
3
|
import { LIFECYCLE_HOOK } from "./lifecycleHooks";
|
|
4
|
+
import { LOGGER } from "./logging/Logger";
|
|
4
5
|
import type { LifecycleHook } from "./types";
|
|
5
6
|
import { ShutdownTimeoutError } from "./errors";
|
|
6
7
|
import { awaitAbortable } from "./internal/awaitAbortable";
|
|
@@ -9,6 +10,7 @@ import { awaitShutdown } from "./internal/awaitShutdown";
|
|
|
9
10
|
|
|
10
11
|
interface LifecycleResolver {
|
|
11
12
|
resolveAll<T>(token: Token<T>): readonly T[];
|
|
13
|
+
tryResolve?<T>(token: Token<T>): T | undefined;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -57,6 +59,9 @@ export class LifecycleCoordinator {
|
|
|
57
59
|
signal?.throwIfAborted();
|
|
58
60
|
validateOptionsOnStart(this.resolver);
|
|
59
61
|
const hosted = resolveHostedServices(this.resolver);
|
|
62
|
+
// Background failures go to the application logger, like HTTP errors.
|
|
63
|
+
const logger = this.resolver.tryResolve?.(LOGGER);
|
|
64
|
+
if (logger) for (const service of hosted) service.useDiagnostics?.(logger);
|
|
60
65
|
const validation = validateHostedServicePlan(hosted, signal);
|
|
61
66
|
if (validation) await awaitAbortable(validation, signal);
|
|
62
67
|
this.hooks = this.resolver.resolveAll(LIFECYCLE_HOOK);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HostedService, HostedServicePlanValidator } from "../di";
|
|
1
|
+
import type { HostedService, HostedServiceDiagnostics, HostedServicePlanValidator } from "../di";
|
|
2
2
|
import { validateHostedServicePlan, registerHostedServiceWrapper, type HostedServicePlanAdmission } from "../di/extensions/hosted-service";
|
|
3
3
|
import { awaitAbortable } from "./internal/awaitAbortable";
|
|
4
4
|
import { reportDiagnosticFailure } from "./internal/reportDiagnosticFailure";
|
|
@@ -34,6 +34,7 @@ export class SupervisedHostedService implements HostedService {
|
|
|
34
34
|
private admittedPhase?: number;
|
|
35
35
|
private starting?: Promise<void>;
|
|
36
36
|
private stopping?: Promise<void>;
|
|
37
|
+
private diagnostics?: HostedServiceDiagnostics;
|
|
37
38
|
|
|
38
39
|
public constructor(
|
|
39
40
|
private readonly factory: () => HostedService,
|
|
@@ -46,6 +47,11 @@ export class SupervisedHostedService implements HostedService {
|
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** Passed on to every attempt's service before it starts. */
|
|
51
|
+
public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
|
|
52
|
+
this.diagnostics = diagnostics;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
public start(signal?: AbortSignal): Promise<void> {
|
|
50
56
|
if (this.stopping) {
|
|
51
57
|
const stopping = this.stopping;
|
|
@@ -71,6 +77,7 @@ export class SupervisedHostedService implements HostedService {
|
|
|
71
77
|
this.prepared = undefined;
|
|
72
78
|
try {
|
|
73
79
|
signal?.throwIfAborted();
|
|
80
|
+
if (this.diagnostics) service.useDiagnostics?.(this.diagnostics);
|
|
74
81
|
await service.start(signal);
|
|
75
82
|
signal?.throwIfAborted();
|
|
76
83
|
this.inner = service;
|
package/package.json
CHANGED