Skip to content

Commit e621246

Browse files
fix(web-sdk): guard TelemetryService and LlamaCppProvider against concurrent initialization (#402)
1 parent f5c49ff commit e621246

2 files changed

Lines changed: 112 additions & 77 deletions

File tree

sdk/runanywhere-web/packages/llamacpp/src/Foundation/TelemetryService.ts

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ export class TelemetryService {
8989
}
9090

9191
private _module: LlamaCppModule | null = null;
92-
private _handle: number = 0; // rac_telemetry_manager_t*
92+
private _handle: number = 0; // rac_telemetry_manager_t*
9393
private _httpCallbackPtr: number = 0; // Emscripten function table ptr
9494
private _initialized = false;
95+
private _initPromise: Promise<void> | null = null; // guards concurrent initialize() calls
9596

9697
private constructor() {}
9798

@@ -102,6 +103,10 @@ export class TelemetryService {
102103
/**
103104
* Initialize the telemetry manager.
104105
* Called from LlamaCppBridge._doLoad() after WASM is loaded.
106+
*
107+
* Concurrent calls are safe: a second caller awaits the in-flight promise
108+
* rather than starting a duplicate initialization, preventing duplicate
109+
* WASM handles and leaked function-table entries.
105110
*/
106111
async initialize(
107112
module: LlamaCppModule,
@@ -112,54 +117,18 @@ export class TelemetryService {
112117
logger.warning('TelemetryService already initialized');
113118
return;
114119
}
115-
116-
if (typeof module._rac_telemetry_manager_create !== 'function') {
117-
logger.warning('rac_telemetry_manager_create not available — telemetry disabled');
120+
// If initialization is already in flight, wait for it rather than
121+
// starting a second one — mirrors the LlamaCppBridge.ensureLoaded() pattern.
122+
if (this._initPromise) {
123+
await this._initPromise;
118124
return;
119125
}
120-
121-
this._module = module;
122-
123-
// Map TypeScript SDKEnvironment to C++ rac_environment_t
124-
const racEnv = this.mapEnvironment(environment);
125-
126-
const deviceId = getOrCreateDeviceId();
127-
128-
// Alloc C strings
129-
const deviceIdPtr = this.allocString(deviceId);
130-
const platformPtr = this.allocString('web');
131-
const versionPtr = this.allocString(SDK_VERSION);
132-
133-
this._handle = module._rac_telemetry_manager_create!(
134-
racEnv, deviceIdPtr, platformPtr, versionPtr,
135-
);
136-
137-
this.freeAll([deviceIdPtr, platformPtr, versionPtr]);
138-
139-
if (!this._handle) {
140-
logger.warning('rac_telemetry_manager_create returned null — telemetry disabled');
141-
this._module = null;
142-
return;
143-
}
144-
145-
// Set device info
146-
if (typeof module._rac_telemetry_manager_set_device_info === 'function') {
147-
const modelPtr = this.allocString(deviceInfo.model ?? 'Browser');
148-
const osVersionPtr = this.allocString(deviceInfo.osVersion ?? 'unknown');
149-
module._rac_telemetry_manager_set_device_info!(this._handle, modelPtr, osVersionPtr);
150-
this.freeAll([modelPtr, osVersionPtr]);
151-
}
152-
153-
// Register HTTP callback
154-
this.registerHttpCallback(environment);
155-
156-
// Configure HTTPService in dev mode using WASM dev config
157-
if (environment === SDKEnvironment.Development) {
158-
this.configureDevHTTP(module);
126+
this._initPromise = this._doInitialize(module, environment, deviceInfo);
127+
try {
128+
await this._initPromise;
129+
} finally {
130+
this._initPromise = null;
159131
}
160-
161-
this._initialized = true;
162-
logger.info(`TelemetryService initialized (env=${environment}, device=${deviceId.substring(0, 8)}...)`);
163132
}
164133

165134
/**
@@ -231,6 +200,63 @@ export class TelemetryService {
231200
// Private
232201
// ---------------------------------------------------------------------------
233202

203+
/**
204+
* Core initialization logic — only called once, guarded by initialize().
205+
*/
206+
private async _doInitialize(
207+
module: LlamaCppModule,
208+
environment: SDKEnvironment,
209+
deviceInfo: DeviceInfoData,
210+
): Promise<void> {
211+
if (typeof module._rac_telemetry_manager_create !== 'function') {
212+
logger.warning('rac_telemetry_manager_create not available — telemetry disabled');
213+
return;
214+
}
215+
216+
this._module = module;
217+
218+
// Map TypeScript SDKEnvironment to C++ rac_environment_t
219+
const racEnv = this.mapEnvironment(environment);
220+
221+
const deviceId = getOrCreateDeviceId();
222+
223+
// Alloc C strings
224+
const deviceIdPtr = this.allocString(deviceId);
225+
const platformPtr = this.allocString('web');
226+
const versionPtr = this.allocString(SDK_VERSION);
227+
228+
this._handle = module._rac_telemetry_manager_create!(
229+
racEnv, deviceIdPtr, platformPtr, versionPtr,
230+
);
231+
232+
this.freeAll([deviceIdPtr, platformPtr, versionPtr]);
233+
234+
if (!this._handle) {
235+
logger.warning('rac_telemetry_manager_create returned null — telemetry disabled');
236+
this._module = null;
237+
return;
238+
}
239+
240+
// Set device info
241+
if (typeof module._rac_telemetry_manager_set_device_info === 'function') {
242+
const modelPtr = this.allocString(deviceInfo.model ?? 'Browser');
243+
const osVersionPtr = this.allocString(deviceInfo.osVersion ?? 'unknown');
244+
module._rac_telemetry_manager_set_device_info!(this._handle, modelPtr, osVersionPtr);
245+
this.freeAll([modelPtr, osVersionPtr]);
246+
}
247+
248+
// Register HTTP callback
249+
this.registerHttpCallback(environment);
250+
251+
// Configure HTTPService in dev mode using WASM dev config
252+
if (environment === SDKEnvironment.Development) {
253+
this.configureDevHTTP(module);
254+
}
255+
256+
this._initialized = true;
257+
logger.info(`TelemetryService initialized (env=${environment}, device=${deviceId.substring(0, 8)}...)`);
258+
}
259+
234260
/**
235261
* Registers the HTTP callback with the WASM telemetry manager.
236262
* C++ will call this when it wants to POST a telemetry batch.

sdk/runanywhere-web/packages/llamacpp/src/LlamaCppProvider.ts

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@ import {
1515
ExtensionPoint,
1616
BackendCapability,
1717
ExtensionRegistry,
18-
AnalyticsEmitter,
1918
} from '@runanywhere/web';
2019

2120
import { LlamaCppBridge } from './Foundation/LlamaCppBridge';
2221
import { loadOffsets } from './Foundation/LlamaCppOffsets';
23-
import { WASMAnalyticsEmitter } from './Foundation/WASMAnalyticsEmitter';
2422

2523
import { TextGeneration } from './Extensions/RunAnywhere+TextGeneration';
2624
import { VLM } from './Extensions/RunAnywhere+VLM';
@@ -33,6 +31,35 @@ import type { BackendExtension } from '@runanywhere/web';
3331
const logger = new SDKLogger('LlamaCppProvider');
3432

3533
let _isRegistered = false;
34+
let _registeringPromise: Promise<void> | null = null;
35+
36+
async function _doRegister(): Promise<void> {
37+
const bridge = LlamaCppBridge.shared;
38+
await bridge.ensureLoaded();
39+
40+
// Load llama.cpp struct offsets from the WASM module
41+
loadOffsets();
42+
43+
// Register model loaders with ModelManager
44+
ModelManager.setLLMLoader(TextGeneration);
45+
46+
// Register extensions with lifecycle registry (only those with cleanup)
47+
ExtensionRegistry.register(TextGeneration);
48+
ExtensionRegistry.register(VLM);
49+
ExtensionRegistry.register(ToolCalling);
50+
ExtensionRegistry.register(Embeddings);
51+
ExtensionRegistry.register(Diffusion);
52+
53+
// Register with ExtensionPoint for capability lookups
54+
ExtensionPoint.registerBackend(llamacppExtension);
55+
56+
// Register typed provider so VoicePipeline (in core) can access
57+
// the LLM via ExtensionPoint.getProvider('llm') at runtime.
58+
ExtensionPoint.registerProvider('llm', TextGeneration);
59+
60+
_isRegistered = true;
61+
logger.info('LlamaCpp backend registered successfully');
62+
}
3663

3764
const llamacppExtension: BackendExtension = {
3865
id: 'llamacpp',
@@ -51,8 +78,8 @@ const llamacppExtension: BackendExtension = {
5178
Embeddings.cleanup();
5279
Diffusion.cleanup();
5380
ExtensionPoint.removeProvider('llm');
54-
AnalyticsEmitter.removeBackend();
5581
_isRegistered = false;
82+
_registeringPromise = null;
5683
logger.info('LlamaCpp backend cleaned up');
5784
},
5885
};
@@ -79,35 +106,17 @@ export const LlamaCppProvider = {
79106
return;
80107
}
81108

82-
const bridge = LlamaCppBridge.shared;
83-
await bridge.ensureLoaded();
84-
85-
// Load llama.cpp struct offsets from the WASM module
86-
loadOffsets();
87-
88-
// Register model loaders with ModelManager
89-
ModelManager.setLLMLoader(TextGeneration);
90-
91-
// Register extensions with lifecycle registry
92-
// Register extensions with lifecycle registry (only those with cleanup)
93-
ExtensionRegistry.register(TextGeneration);
94-
ExtensionRegistry.register(VLM);
95-
ExtensionRegistry.register(ToolCalling);
96-
ExtensionRegistry.register(Embeddings);
97-
ExtensionRegistry.register(Diffusion);
98-
99-
// Register with ExtensionPoint for capability lookups
100-
ExtensionPoint.registerBackend(llamacppExtension);
101-
102-
// Register typed provider so VoicePipeline (in core) can access
103-
// the LLM via ExtensionPoint.getProvider('llm') at runtime.
104-
ExtensionPoint.registerProvider('llm', TextGeneration);
105-
106-
// Route all analytics events through the C++ telemetry manager
107-
AnalyticsEmitter.registerBackend(new WASMAnalyticsEmitter());
109+
if (_registeringPromise) {
110+
logger.debug('LlamaCpp registration in progress, awaiting...');
111+
return _registeringPromise;
112+
}
108113

109-
_isRegistered = true;
110-
logger.info('LlamaCpp backend registered successfully');
114+
_registeringPromise = _doRegister();
115+
try {
116+
await _registeringPromise;
117+
} finally {
118+
_registeringPromise = null;
119+
}
111120
},
112121

113122
/**

0 commit comments

Comments
 (0)