Skip to content

Commit ce02f45

Browse files
rahuls-dbIsaac
andcommitted
Address review: reuse one fallback KernelBackend, guard cause overwrite
- ThriftBackend: reuse a single fallback KernelBackend across all Reyden (KP001) fallback sessions on the connection instead of constructing one per openSession. connectionOptions are fixed after connect, so it is created + connected once (lazily, memoized; the attempt is cleared on connect failure so a later open can retry) and released in close() — this stops per-session accumulation of backends and process-global log-bridge listeners. - On the double-failure path, only set the kernel error's `cause` when it is absent, so a cause the kernel error may already carry is not clobbered. - Test now opens two fallback sessions and asserts a single KernelBackend is created and connected once, reused for both, and closed once on close(). Co-authored-by: Isaac <no-reply@databricks.com> Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
1 parent 34412e1 commit ce02f45

2 files changed

Lines changed: 52 additions & 21 deletions

File tree

‎lib/thrift-backend/ThriftBackend.ts‎

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ export default class ThriftBackend implements IBackend {
3737

3838
private connectionOptions?: ConnectionOptions;
3939

40-
// KernelBackend(s) created for Reyden (KP001) fallback. Tracked so their
41-
// process-global log-bridge listeners are released on close() — otherwise each
42-
// fallback session would leak an onLevelChange listener for the process lifetime.
43-
private fallbackKernelBackends: KernelBackend[] = [];
40+
// A single KernelBackend reused for every Reyden (KP001) fallback session on this
41+
// connection. connect() installs a process-global log-bridge listener, so it is created
42+
// once (connectionOptions are fixed after connect) and released in close() — rather than
43+
// constructing one per openSession and leaking a listener each time.
44+
private fallbackKernelBackend?: KernelBackend;
45+
46+
private fallbackKernelBackendConnect?: Promise<KernelBackend>;
4447

4548
constructor({ context, onConnectionEvent }: ThriftBackendOptions) {
4649
this.context = context;
@@ -127,7 +130,13 @@ export default class ThriftBackend implements IBackend {
127130
try {
128131
return await this.openSessionWithKernelBackend(request);
129132
} catch (kernelError) {
130-
if (kernelError && typeof kernelError === 'object') {
133+
// Preserve the Thrift KP001 as the kernel error's cause, but don't clobber a cause
134+
// the kernel error may already carry.
135+
if (
136+
kernelError &&
137+
typeof kernelError === 'object' &&
138+
(kernelError as { cause?: unknown }).cause === undefined
139+
) {
131140
(kernelError as { cause?: unknown }).cause = error;
132141
}
133142
logger.log(LogLevel.error, 'Reyden: both Thrift (KP001) and SEA fallback failed');
@@ -189,24 +198,41 @@ export default class ThriftBackend implements IBackend {
189198
const logger = this.context.getLogger();
190199
logger.log(LogLevel.debug, 'Reyden: opening session via KernelBackend (SEA)');
191200

192-
// Create a KernelBackend and connect/open. Track it so close() releases the
193-
// log-bridge listener that connect() installs.
194-
const kernelBackend = this.createKernelBackend();
195-
this.fallbackKernelBackends.push(kernelBackend);
196-
await kernelBackend.connect(this.connectionOptions);
201+
const kernelBackend = await this.getFallbackKernelBackend(this.connectionOptions);
197202
return kernelBackend.openSession(request);
198203
}
199204

205+
// Lazily creates and connects the single fallback KernelBackend, reused across every
206+
// fallback session so repeated opens don't accumulate backends / log-bridge listeners.
207+
// On a connect failure the memoized attempt is cleared so a later open can retry.
208+
private getFallbackKernelBackend(connectionOptions: ConnectionOptions): Promise<KernelBackend> {
209+
if (!this.fallbackKernelBackendConnect) {
210+
this.fallbackKernelBackendConnect = (async () => {
211+
const kernelBackend = this.createKernelBackend();
212+
await kernelBackend.connect(connectionOptions);
213+
this.fallbackKernelBackend = kernelBackend;
214+
return kernelBackend;
215+
})().catch((error) => {
216+
this.fallbackKernelBackendConnect = undefined;
217+
throw error;
218+
});
219+
}
220+
return this.fallbackKernelBackendConnect;
221+
}
222+
200223
// Seam so tests can inject a fake KernelBackend without the native binding.
201224
protected createKernelBackend(): KernelBackend {
202225
return new KernelBackend({ context: this.context });
203226
}
204227

205228
public async close(): Promise<void> {
206-
// Release the process-global log-bridge listener(s) held by any Reyden-fallback
207-
// KernelBackend. DBSQLClient owns the rest of the connection lifecycle and clears
208-
// its own state (connectionProvider, authProvider, thrift client) after this returns.
209-
await Promise.all(this.fallbackKernelBackends.map((backend) => backend.close()));
210-
this.fallbackKernelBackends = [];
229+
// Release the process-global log-bridge listener held by the Reyden-fallback KernelBackend.
230+
// DBSQLClient owns the rest of the connection lifecycle and clears its own state
231+
// (connectionProvider, authProvider, thrift client) after this returns.
232+
if (this.fallbackKernelBackend) {
233+
await this.fallbackKernelBackend.close();
234+
this.fallbackKernelBackend = undefined;
235+
this.fallbackKernelBackendConnect = undefined;
236+
}
211237
}
212238
}

‎tests/unit/thrift-backend/ReydenThriftRecoveryOrchestration.test.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe('Reyden Thrift Auto-Recovery — Orchestration', () => {
126126
expect(thrown.cause).to.equal(thriftError);
127127
});
128128

129-
it('closes the fallback KernelBackend on close(), releasing its log-bridge listener', async () => {
129+
it('reuses one fallback KernelBackend across sessions and closes it on close()', async () => {
130130
const backend = makeBackend();
131131
const fakeKernel = {
132132
connect: sandbox.stub().resolves(),
@@ -135,14 +135,19 @@ describe('Reyden Thrift Auto-Recovery — Orchestration', () => {
135135
};
136136
sandbox.stub(backend as any, 'openSessionWithThrift').rejects(kp001Error());
137137
// Inject the fake via the createKernelBackend seam and let the REAL
138-
// openSessionWithKernelBackend run (connect + track), so close() must release it.
139-
sandbox.stub(backend as any, 'createKernelBackend').returns(fakeKernel as any);
138+
// openSessionWithKernelBackend run (connect + reuse), so close() must release it.
139+
const createStub = sandbox.stub(backend as any, 'createKernelBackend').returns(fakeKernel as any);
140140

141-
await backend.openSession({} as any);
141+
await backend.openSession({} as any); // reactive recovery marks the cache
142+
await backend.openSession({} as any); // second open hits the pre-check → same fallback backend
143+
144+
// A single KernelBackend is created and connected once, not one per session.
145+
expect(createStub.calledOnce).to.be.true;
142146
expect(fakeKernel.connect.calledOnce).to.be.true;
143-
expect(fakeKernel.close.called).to.be.false; // still open
147+
expect(fakeKernel.openSession.calledTwice).to.be.true;
148+
expect(fakeKernel.close.called).to.be.false; // not closed until close()
144149

145150
await backend.close();
146-
expect(fakeKernel.close.calledOnce).to.be.true; // released on ThriftBackend.close()
151+
expect(fakeKernel.close.calledOnce).to.be.true; // released once on ThriftBackend.close()
147152
});
148153
});

0 commit comments

Comments
 (0)