Skip to content

Commit f94c114

Browse files
committed
feat(webapp): re-broadcast LLM pricing to OTLP workers on registry reload
The pricing registry now notifies subscribers after every load/reload (interval + pub/sub); the OTLP worker pool subscribes and re-broadcasts the compiled model rows to all workers, so their in-memory pricing stays fresh instead of being a static snapshot from pool creation. Respawned workers also inherit the latest snapshot via workerData. Verified on the isolated stack: broadcast fires on init then every reload interval to all workers; registry-sourced cost enrichment kept landing in ClickHouse across multiple reload cycles under load (no break).
1 parent 507bdf5 commit f94c114

3 files changed

Lines changed: 63 additions & 12 deletions

File tree

apps/webapp/app/v3/llmPricingRegistry.server.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ModelPricingRegistry, seedLlmPricing } from "@internal/llm-model-catalog";
2+
import type { LlmModelWithPricing } from "@internal/llm-model-catalog";
23
import { prisma, $replica } from "~/db.server";
34
import { env } from "~/env.server";
45
import { logger } from "~/services/logger.server";
@@ -7,6 +8,37 @@ import { createRedisClient } from "~/redis.server";
78
import { singleton } from "~/utils/singleton";
89
import { setLlmPricingRegistry } from "./utils/enrichCreatableEvents.server";
910

11+
type PricingReloadListener = (models: LlmModelWithPricing[]) => void;
12+
const pricingReloadListeners = new Set<PricingReloadListener>();
13+
14+
// Notify subscribers (e.g. the OTLP worker pool) after each load/reload so they can rebuild
15+
// their own in-memory copy. No-op until something subscribes.
16+
function emitPricingReload() {
17+
if (!llmPricingRegistry || !llmPricingRegistry.isLoaded || pricingReloadListeners.size === 0) {
18+
return;
19+
}
20+
const models = llmPricingRegistry.toSerializable();
21+
for (const listener of pricingReloadListeners) {
22+
try {
23+
listener(models);
24+
} catch (err) {
25+
logger.warn("LLM pricing reload listener failed", {
26+
error: err instanceof Error ? err.message : String(err),
27+
});
28+
}
29+
}
30+
}
31+
32+
export function subscribeToPricingReload(listener: PricingReloadListener): () => void {
33+
pricingReloadListeners.add(listener);
34+
if (llmPricingRegistry?.isLoaded) {
35+
listener(llmPricingRegistry.toSerializable());
36+
}
37+
return () => {
38+
pricingReloadListeners.delete(listener);
39+
};
40+
}
41+
1042
async function initRegistry(registry: ModelPricingRegistry) {
1143
if (env.LLM_PRICING_SEED_ON_STARTUP) {
1244
await seedLlmPricing(prisma);
@@ -25,16 +57,21 @@ export const llmPricingRegistry = singleton("llmPricingRegistry", () => {
2557
// Wire up the registry so enrichCreatableEvents can use it
2658
setLlmPricingRegistry(registry);
2759

28-
initRegistry(registry).catch((err) => {
29-
console.error("Failed to initialize LLM pricing registry", err);
30-
});
60+
initRegistry(registry)
61+
.then(() => emitPricingReload())
62+
.catch((err) => {
63+
console.error("Failed to initialize LLM pricing registry", err);
64+
});
3165

3266
// Periodic reload (backstop for the pub/sub path below)
3367
const reloadInterval = env.LLM_PRICING_RELOAD_INTERVAL_MS;
3468
const interval = setInterval(() => {
35-
registry.reload().catch((err) => {
36-
console.error("Failed to reload LLM pricing registry", err);
37-
});
69+
registry
70+
.reload()
71+
.then(() => emitPricingReload())
72+
.catch((err) => {
73+
console.error("Failed to reload LLM pricing registry", err);
74+
});
3875
}, reloadInterval);
3976

4077
// Pub/sub reload is opt-in per process (default off). Without it, the
@@ -73,11 +110,14 @@ export const llmPricingRegistry = singleton("llmPricingRegistry", () => {
73110
if (pendingReloadTimer) return;
74111
pendingReloadTimer = setTimeout(() => {
75112
pendingReloadTimer = null;
76-
registry.reload().catch((err) => {
77-
logger.warn("Failed to reload LLM pricing registry from pub/sub", {
78-
error: err instanceof Error ? err.message : String(err),
113+
registry
114+
.reload()
115+
.then(() => emitPricingReload())
116+
.catch((err) => {
117+
logger.warn("Failed to reload LLM pricing registry from pub/sub", {
118+
error: err instanceof Error ? err.message : String(err),
119+
});
79120
});
80-
});
81121
}, debounceMs);
82122
}
83123

apps/webapp/app/v3/otlpExporter.server.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
} from "./otlpTransform.server";
3333
import os from "node:os";
3434
import { getOtlpWorkerPool } from "./otlpWorkerPool.server";
35-
import { llmPricingRegistry } from "./llmPricingRegistry.server";
35+
import { llmPricingRegistry, subscribeToPricingReload } from "./llmPricingRegistry.server";
3636

3737
// When enabled, decode+convert+enrich run in a worker pool; the main thread keeps the single
3838
// consolidated insert path (batching/part-count unchanged). Off = today's single-thread path.
@@ -54,6 +54,7 @@ class OTLPExporter {
5454
private readonly _clickhouseFactory: ClickhouseFactory;
5555
private readonly _verbose: boolean;
5656
private readonly _spanAttributeValueLengthLimit: number;
57+
#pricingSubscribed = false;
5758

5859
constructor(config: OTLPExporterConfig) {
5960
this._tracer = trace.getTracer("otlp-exporter");
@@ -161,7 +162,13 @@ class OTLPExporter {
161162
await waitForLlmPricingReady();
162163
const models =
163164
llmPricingRegistry && llmPricingRegistry.isLoaded ? llmPricingRegistry.toSerializable() : [];
164-
return getOtlpWorkerPool(OTEL_TRANSFORM_WORKER_POOL_SIZE, models);
165+
const pool = getOtlpWorkerPool(OTEL_TRANSFORM_WORKER_POOL_SIZE, models);
166+
if (!this.#pricingSubscribed) {
167+
this.#pricingSubscribed = true;
168+
// Re-broadcast pricing to workers on every registry reload so their cost math stays fresh.
169+
subscribeToPricingReload((updated) => pool.broadcastPricing(updated));
170+
}
171+
return pool;
165172
}
166173

167174
async #exportEvents(

apps/webapp/app/v3/otlpWorkerPool.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ export class OtlpWorkerPool {
133133
for (const worker of this.workers) {
134134
worker.postMessage({ type: "pricing", models });
135135
}
136+
logger.info("OtlpWorkerPool broadcast pricing", {
137+
models: models.length,
138+
workers: this.workers.length,
139+
});
136140
}
137141

138142
get queueDepth() {

0 commit comments

Comments
 (0)