ydb-qdrant 4.7.1 → 4.8.0

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.
@@ -19,3 +19,6 @@ export declare const SEARCH_MODE: SearchMode;
19
19
  export declare const OVERFETCH_MULTIPLIER: number;
20
20
  export declare const CLIENT_SIDE_SERIALIZATION_ENABLED: boolean;
21
21
  export declare const UPSERT_BATCH_SIZE: number;
22
+ export declare const SESSION_POOL_MIN_SIZE: number;
23
+ export declare const SESSION_POOL_MAX_SIZE: number;
24
+ export declare const SESSION_KEEPALIVE_PERIOD_MS: number;
@@ -78,3 +78,12 @@ export const SEARCH_MODE = resolveSearchModeEnv(COLLECTION_STORAGE_MODE);
78
78
  export const OVERFETCH_MULTIPLIER = parseIntegerEnv(process.env.YDB_QDRANT_OVERFETCH_MULTIPLIER, 10, { min: 1 });
79
79
  export const CLIENT_SIDE_SERIALIZATION_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_CLIENT_SIDE_SERIALIZATION_ENABLED, false);
80
80
  export const UPSERT_BATCH_SIZE = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_BATCH_SIZE, 100, { min: 1 });
81
+ // Session pool configuration
82
+ const RAW_SESSION_POOL_MIN_SIZE = parseIntegerEnv(process.env.YDB_SESSION_POOL_MIN_SIZE, 5, { min: 1, max: 500 });
83
+ const RAW_SESSION_POOL_MAX_SIZE = parseIntegerEnv(process.env.YDB_SESSION_POOL_MAX_SIZE, 100, { min: 1, max: 500 });
84
+ const NORMALIZED_SESSION_POOL_MIN_SIZE = RAW_SESSION_POOL_MIN_SIZE > RAW_SESSION_POOL_MAX_SIZE
85
+ ? RAW_SESSION_POOL_MAX_SIZE
86
+ : RAW_SESSION_POOL_MIN_SIZE;
87
+ export const SESSION_POOL_MIN_SIZE = NORMALIZED_SESSION_POOL_MIN_SIZE;
88
+ export const SESSION_POOL_MAX_SIZE = RAW_SESSION_POOL_MAX_SIZE;
89
+ export const SESSION_KEEPALIVE_PERIOD_MS = parseIntegerEnv(process.env.YDB_SESSION_KEEPALIVE_PERIOD_MS, 5000, { min: 1000, max: 60000 });
@@ -231,195 +231,113 @@ async function searchPointsOneTableApproximate(tableName, queryVector, top, with
231
231
  const rawCandidateLimit = safeTop * overfetchMultiplier;
232
232
  const candidateLimit = Math.max(safeTop, rawCandidateLimit);
233
233
  const results = await withSession(async (s) => {
234
+ let yql;
235
+ let params;
234
236
  if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
235
237
  const binaries = buildVectorBinaryParams(queryVector);
236
- // Phase 1: approximate candidate selection using embedding_quantized
237
- const phase1Query = `
238
+ yql = `
238
239
  DECLARE $qbin_bit AS String;
239
- DECLARE $k AS Uint32;
240
+ DECLARE $qbinf AS String;
241
+ DECLARE $candidateLimit AS Uint32;
242
+ DECLARE $safeTop AS Uint32;
240
243
  DECLARE $uid AS Utf8;
241
- SELECT point_id
244
+
245
+ $candidates = (
246
+ SELECT point_id
247
+ FROM ${tableName}
248
+ WHERE uid = $uid AND embedding_quantized IS NOT NULL
249
+ ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
250
+ LIMIT $candidateLimit
251
+ );
252
+
253
+ SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
242
254
  FROM ${tableName}
243
- WHERE uid = $uid AND embedding_quantized IS NOT NULL
244
- ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
245
- LIMIT $k;
255
+ WHERE uid = $uid
256
+ AND point_id IN $candidates
257
+ ORDER BY score ${order}
258
+ LIMIT $safeTop;
246
259
  `;
260
+ params = {
261
+ $qbin_bit: typeof TypedValues.bytes === "function"
262
+ ? TypedValues.bytes(binaries.bit)
263
+ : binaries.bit,
264
+ $qbinf: typeof TypedValues.bytes === "function"
265
+ ? TypedValues.bytes(binaries.float)
266
+ : binaries.float,
267
+ $candidateLimit: TypedValues.uint32(candidateLimit),
268
+ $safeTop: TypedValues.uint32(safeTop),
269
+ $uid: TypedValues.utf8(uid),
270
+ };
247
271
  logger.debug({
248
272
  tableName,
249
273
  distance,
250
274
  top,
251
275
  safeTop,
252
276
  candidateLimit,
253
- mode: "one_table_approximate_phase1_client_side_serialization",
254
- yql: phase1Query,
277
+ mode: "one_table_approximate_client_side_serialization",
278
+ yql,
255
279
  params: {
256
280
  uid,
257
- top: candidateLimit,
281
+ safeTop,
282
+ candidateLimit,
258
283
  vectorLength: queryVector.length,
259
284
  vectorPreview: queryVector.slice(0, 3),
260
285
  },
261
- }, "one_table search (approximate, phase 1): executing YQL");
262
- const phase1Params = {
263
- $qbin_bit: typeof TypedValues.bytes === "function"
264
- ? TypedValues.bytes(binaries.bit)
265
- : binaries.bit,
266
- $k: TypedValues.uint32(candidateLimit),
267
- $uid: TypedValues.utf8(uid),
268
- };
269
- const rs1 = await s.executeQuery(phase1Query, phase1Params);
270
- const rowset1 = rs1.resultSets?.[0];
271
- const rows1 = (rowset1?.rows ?? []);
272
- const candidateIds = rows1
273
- .map((row) => row.items?.[0]?.textValue)
274
- .filter((id) => typeof id === "string");
275
- if (candidateIds.length === 0) {
276
- return [];
277
- }
278
- // Phase 2: exact re-ranking on full-precision embedding for candidates only
279
- const phase2Query = `
280
- DECLARE $qbinf AS String;
281
- DECLARE $k AS Uint32;
286
+ }, "one_table search (approximate): executing YQL with client-side serialization");
287
+ }
288
+ else {
289
+ const qf = buildVectorParam(queryVector);
290
+ yql = `
291
+ DECLARE $qf AS List<Float>;
292
+ DECLARE $candidateLimit AS Uint32;
293
+ DECLARE $safeTop AS Uint32;
282
294
  DECLARE $uid AS Utf8;
283
- DECLARE $ids AS List<Utf8>;
295
+
296
+ $qbin_bit = Knn::ToBinaryStringBit($qf);
297
+ $qbinf = Knn::ToBinaryStringFloat($qf);
298
+
299
+ $candidates = (
300
+ SELECT point_id
301
+ FROM ${tableName}
302
+ WHERE uid = $uid AND embedding_quantized IS NOT NULL
303
+ ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
304
+ LIMIT $candidateLimit
305
+ );
306
+
284
307
  SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
285
308
  FROM ${tableName}
286
- WHERE uid = $uid AND point_id IN $ids
309
+ WHERE uid = $uid
310
+ AND point_id IN $candidates
287
311
  ORDER BY score ${order}
288
- LIMIT $k;
312
+ LIMIT $safeTop;
289
313
  `;
314
+ params = {
315
+ $qf: qf,
316
+ $candidateLimit: TypedValues.uint32(candidateLimit),
317
+ $safeTop: TypedValues.uint32(safeTop),
318
+ $uid: TypedValues.utf8(uid),
319
+ };
290
320
  logger.debug({
291
321
  tableName,
292
322
  distance,
293
323
  top,
294
324
  safeTop,
295
- candidateCount: candidateIds.length,
296
- mode: "one_table_approximate_phase2_client_side_serialization",
297
- yql: phase2Query,
325
+ candidateLimit,
326
+ mode: "one_table_approximate",
327
+ yql,
298
328
  params: {
299
329
  uid,
300
- top: safeTop,
330
+ safeTop,
331
+ candidateLimit,
301
332
  vectorLength: queryVector.length,
302
333
  vectorPreview: queryVector.slice(0, 3),
303
- ids: candidateIds,
304
334
  },
305
- }, "one_table search (approximate, phase 2): executing YQL");
306
- const idsParam = TypedValues.list(Types.UTF8, candidateIds);
307
- const phase2Params = {
308
- $qbinf: typeof TypedValues.bytes === "function"
309
- ? TypedValues.bytes(binaries.float)
310
- : binaries.float,
311
- $k: TypedValues.uint32(safeTop),
312
- $uid: TypedValues.utf8(uid),
313
- $ids: idsParam,
314
- };
315
- const rs2 = await s.executeQuery(phase2Query, phase2Params);
316
- const rowset2 = rs2.resultSets?.[0];
317
- const rows2 = (rowset2?.rows ?? []);
318
- return rows2.map((row) => {
319
- const id = row.items?.[0]?.textValue;
320
- if (typeof id !== "string") {
321
- throw new Error("point_id is missing in YDB search result");
322
- }
323
- let payload;
324
- let scoreIdx = 1;
325
- if (withPayload) {
326
- const payloadText = row.items?.[1]?.textValue;
327
- if (payloadText) {
328
- try {
329
- payload = JSON.parse(payloadText);
330
- }
331
- catch {
332
- payload = undefined;
333
- }
334
- }
335
- scoreIdx = 2;
336
- }
337
- const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
338
- return { id, score, ...(payload ? { payload } : {}) };
339
- });
340
- }
341
- const qf = buildVectorParam(queryVector);
342
- // Phase 1: approximate candidate selection using embedding_quantized
343
- const phase1Query = `
344
- DECLARE $qf AS List<Float>;
345
- DECLARE $k AS Uint32;
346
- DECLARE $uid AS Utf8;
347
- $qbin_bit = Knn::ToBinaryStringBit($qf);
348
- SELECT point_id
349
- FROM ${tableName}
350
- WHERE uid = $uid AND embedding_quantized IS NOT NULL
351
- ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
352
- LIMIT $k;
353
- `;
354
- logger.debug({
355
- tableName,
356
- distance,
357
- top,
358
- safeTop,
359
- candidateLimit,
360
- mode: "one_table_approximate_phase1",
361
- yql: phase1Query,
362
- params: {
363
- uid,
364
- top: candidateLimit,
365
- vectorLength: queryVector.length,
366
- vectorPreview: queryVector.slice(0, 3),
367
- },
368
- }, "one_table search (approximate, phase 1): executing YQL");
369
- const phase1Params = {
370
- $qf: qf,
371
- $k: TypedValues.uint32(candidateLimit),
372
- $uid: TypedValues.utf8(uid),
373
- };
374
- const rs1 = await s.executeQuery(phase1Query, phase1Params);
375
- const rowset1 = rs1.resultSets?.[0];
376
- const rows1 = (rowset1?.rows ?? []);
377
- const candidateIds = rows1
378
- .map((row) => row.items?.[0]?.textValue)
379
- .filter((id) => typeof id === "string");
380
- if (candidateIds.length === 0) {
381
- return [];
335
+ }, "one_table search (approximate): executing YQL");
382
336
  }
383
- // Phase 2: exact re-ranking on full-precision embedding for candidates only
384
- const phase2Query = `
385
- DECLARE $qf AS List<Float>;
386
- DECLARE $k AS Uint32;
387
- DECLARE $uid AS Utf8;
388
- DECLARE $ids AS List<Utf8>;
389
- $qbinf = Knn::ToBinaryStringFloat($qf);
390
- SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
391
- FROM ${tableName}
392
- WHERE uid = $uid AND point_id IN $ids
393
- ORDER BY score ${order}
394
- LIMIT $k;
395
- `;
396
- logger.debug({
397
- tableName,
398
- distance,
399
- top,
400
- safeTop,
401
- candidateCount: candidateIds.length,
402
- mode: "one_table_approximate_phase2",
403
- yql: phase2Query,
404
- params: {
405
- uid,
406
- top: safeTop,
407
- vectorLength: queryVector.length,
408
- vectorPreview: queryVector.slice(0, 3),
409
- ids: candidateIds,
410
- },
411
- }, "one_table search (approximate, phase 2): executing YQL");
412
- const idsParam = TypedValues.list(Types.UTF8, candidateIds);
413
- const phase2Params = {
414
- $qf: qf,
415
- $k: TypedValues.uint32(safeTop),
416
- $uid: TypedValues.utf8(uid),
417
- $ids: idsParam,
418
- };
419
- const rs2 = await s.executeQuery(phase2Query, phase2Params);
420
- const rowset2 = rs2.resultSets?.[0];
421
- const rows2 = (rowset2?.rows ?? []);
422
- return rows2.map((row) => {
337
+ const rs = await s.executeQuery(yql, params);
338
+ const rowset = rs.resultSets?.[0];
339
+ const rows = (rowset?.rows ?? []);
340
+ return rows.map((row) => {
423
341
  const id = row.items?.[0]?.textValue;
424
342
  if (typeof id !== "string") {
425
343
  throw new Error("point_id is missing in YDB search result");
@@ -118,13 +118,23 @@ async function executeSearch(ctx, normalizedSearch, source) {
118
118
  throw err;
119
119
  }
120
120
  const threshold = normalizedSearch.scoreThreshold;
121
+ // For Cosine, repository hits use distance scores; convert to a
122
+ // similarity-like score so API consumers and IDE thresholds see
123
+ // "higher is better". This keeps ranking identical (monotonic 1 - d).
124
+ const normalizedHits = meta.distance === "Cosine"
125
+ ? hits.map((hit) => ({
126
+ ...hit,
127
+ score: 1 - hit.score,
128
+ }))
129
+ : hits;
121
130
  const filtered = threshold === undefined
122
- ? hits
123
- : hits.filter((hit) => {
124
- const isSimilarity = meta.distance === "Cosine" || meta.distance === "Dot";
125
- if (isSimilarity) {
131
+ ? normalizedHits
132
+ : normalizedHits.filter((hit) => {
133
+ if (meta.distance === "Dot" || meta.distance === "Cosine") {
134
+ // Similarity metrics: threshold is minimum similarity.
126
135
  return hit.score >= threshold;
127
136
  }
137
+ // Euclid / Manhattan: pure distance metrics; threshold is max distance.
128
138
  return hit.score <= threshold;
129
139
  });
130
140
  logger.info({
@@ -5,13 +5,13 @@ export declare function mapDistanceToKnnFn(distance: DistanceKind): {
5
5
  };
6
6
  export declare function mapDistanceToIndexParam(distance: DistanceKind): string;
7
7
  /**
8
- * Maps a user-specified distance metric to a YDB Knn distance function
8
+ * Maps a user-specified distance metric to a YDB Knn function
9
9
  * suitable for bit-quantized vectors (Phase 1 approximate candidate selection).
10
- * Always returns a distance function (lower is better, ASC ordering).
10
+ * Cosine uses similarity (DESC); other metrics use distance (ASC).
11
11
  * For Dot, falls back to CosineDistance as a proxy since there is no
12
12
  * direct distance equivalent for inner product.
13
13
  */
14
14
  export declare function mapDistanceToBitKnnFn(distance: DistanceKind): {
15
15
  fn: string;
16
- order: "ASC";
16
+ order: "ASC" | "DESC";
17
17
  };
@@ -1,7 +1,7 @@
1
1
  export function mapDistanceToKnnFn(distance) {
2
2
  switch (distance) {
3
3
  case "Cosine":
4
- return { fn: "Knn::CosineSimilarity", order: "DESC" };
4
+ return { fn: "Knn::CosineDistance", order: "ASC" };
5
5
  case "Dot":
6
6
  return { fn: "Knn::InnerProductSimilarity", order: "DESC" };
7
7
  case "Euclid":
@@ -9,7 +9,7 @@ export function mapDistanceToKnnFn(distance) {
9
9
  case "Manhattan":
10
10
  return { fn: "Knn::ManhattanDistance", order: "ASC" };
11
11
  default:
12
- return { fn: "Knn::CosineSimilarity", order: "DESC" };
12
+ return { fn: "Knn::CosineDistance", order: "ASC" };
13
13
  }
14
14
  }
15
15
  export function mapDistanceToIndexParam(distance) {
@@ -27,18 +27,17 @@ export function mapDistanceToIndexParam(distance) {
27
27
  }
28
28
  }
29
29
  /**
30
- * Maps a user-specified distance metric to a YDB Knn distance function
30
+ * Maps a user-specified distance metric to a YDB Knn function
31
31
  * suitable for bit-quantized vectors (Phase 1 approximate candidate selection).
32
- * Always returns a distance function (lower is better, ASC ordering).
32
+ * Cosine uses similarity (DESC); other metrics use distance (ASC).
33
33
  * For Dot, falls back to CosineDistance as a proxy since there is no
34
34
  * direct distance equivalent for inner product.
35
35
  */
36
36
  export function mapDistanceToBitKnnFn(distance) {
37
37
  switch (distance) {
38
38
  case "Cosine":
39
- return { fn: "Knn::CosineDistance", order: "ASC" };
39
+ return { fn: "Knn::CosineSimilarity", order: "DESC" };
40
40
  case "Dot":
41
- // No direct distance equivalent; use Cosine as proxy
42
41
  return { fn: "Knn::CosineDistance", order: "ASC" };
43
42
  case "Euclid":
44
43
  return { fn: "Knn::EuclideanDistance", order: "ASC" };
@@ -8,7 +8,19 @@ type DriverConfig = {
8
8
  authService?: IAuthService;
9
9
  };
10
10
  export declare function __setDriverForTests(fake: unknown): void;
11
+ export declare function __setDriverFactoryForTests(factory: ((config: unknown) => unknown) | undefined): void;
12
+ export declare function __resetRefreshStateForTests(): void;
11
13
  export declare function configureDriver(config: DriverConfig): void;
12
14
  export declare function readyOrThrow(): Promise<void>;
13
15
  export declare function withSession<T>(fn: (s: Session) => Promise<T>): Promise<T>;
14
16
  export declare function isYdbAvailable(timeoutMs?: number): Promise<boolean>;
17
+ /**
18
+ * Destroys the current driver and its session pool.
19
+ * Next call to withSession or readyOrThrow will create a new driver.
20
+ */
21
+ export declare function destroyDriver(): Promise<void>;
22
+ /**
23
+ * Destroys the current driver and immediately creates a fresh one.
24
+ * Use this to recover from session pool exhaustion or zombie sessions.
25
+ */
26
+ export declare function refreshDriver(): Promise<void>;
@@ -1,16 +1,72 @@
1
1
  import { createRequire } from "module";
2
- import { YDB_DATABASE, YDB_ENDPOINT } from "../config/env.js";
2
+ import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, } from "../config/env.js";
3
+ import { logger } from "../logging/logger.js";
3
4
  const require = createRequire(import.meta.url);
4
5
  const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, } = require("ydb-sdk");
5
6
  export { Types, TypedValues, TableDescription, Column };
6
7
  const DRIVER_READY_TIMEOUT_MS = 15000;
7
8
  const TABLE_SESSION_TIMEOUT_MS = 20000;
8
9
  const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
10
+ const DRIVER_REFRESH_COOLDOWN_MS = 30000;
9
11
  let overrideConfig;
10
12
  let driver;
13
+ let lastDriverRefreshAt = 0;
14
+ let driverRefreshInFlight = null;
15
+ // Test-only: allows injecting a mock Driver factory
16
+ let driverFactoryOverride;
17
+ function shouldTriggerDriverRefresh(error) {
18
+ if (!(error instanceof Error)) {
19
+ return false;
20
+ }
21
+ const msg = error.message ?? "";
22
+ if (/No session became available within timeout/i.test(msg)) {
23
+ return true;
24
+ }
25
+ if (/SESSION_POOL_EMPTY|session pool empty/i.test(msg)) {
26
+ return true;
27
+ }
28
+ if (/SessionExpired|SESSION_EXPIRED|session.*expired/i.test(msg)) {
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ async function maybeRefreshDriverOnSessionError(error) {
34
+ if (!shouldTriggerDriverRefresh(error)) {
35
+ return;
36
+ }
37
+ const now = Date.now();
38
+ if (now - lastDriverRefreshAt < DRIVER_REFRESH_COOLDOWN_MS) {
39
+ logger.warn({ lastDriverRefreshAt, cooldownMs: DRIVER_REFRESH_COOLDOWN_MS }, "YDB driver refresh skipped due to cooldown");
40
+ return;
41
+ }
42
+ if (driverRefreshInFlight) {
43
+ logger.warn({ lastDriverRefreshAt, cooldownMs: DRIVER_REFRESH_COOLDOWN_MS }, "YDB driver refresh already in flight; skipping");
44
+ return;
45
+ }
46
+ lastDriverRefreshAt = now;
47
+ logger.warn({ err: error }, "YDB session-related error detected; refreshing driver");
48
+ try {
49
+ const refreshPromise = refreshDriver();
50
+ driverRefreshInFlight = refreshPromise;
51
+ await refreshPromise;
52
+ }
53
+ catch (refreshErr) {
54
+ logger.error({ err: refreshErr }, "YDB driver refresh failed; keeping current driver");
55
+ }
56
+ finally {
57
+ driverRefreshInFlight = null;
58
+ }
59
+ }
11
60
  export function __setDriverForTests(fake) {
12
61
  driver = fake;
13
62
  }
63
+ export function __setDriverFactoryForTests(factory) {
64
+ driverFactoryOverride = factory;
65
+ }
66
+ export function __resetRefreshStateForTests() {
67
+ lastDriverRefreshAt = 0;
68
+ driverRefreshInFlight = null;
69
+ }
14
70
  export function configureDriver(config) {
15
71
  if (driver) {
16
72
  // Driver already created; keep existing connection settings.
@@ -28,10 +84,23 @@ function getOrCreateDriver() {
28
84
  endpoint: overrideConfig?.endpoint ?? YDB_ENDPOINT,
29
85
  database: overrideConfig?.database ?? YDB_DATABASE,
30
86
  };
31
- driver = new Driver({
87
+ const driverConfig = {
32
88
  ...base,
33
89
  authService: overrideConfig?.authService ?? getCredentialsFromEnv(),
34
- });
90
+ poolSettings: {
91
+ minLimit: SESSION_POOL_MIN_SIZE,
92
+ maxLimit: SESSION_POOL_MAX_SIZE,
93
+ keepAlivePeriod: SESSION_KEEPALIVE_PERIOD_MS,
94
+ },
95
+ };
96
+ driver = driverFactoryOverride
97
+ ? driverFactoryOverride(driverConfig)
98
+ : new Driver(driverConfig);
99
+ logger.info({
100
+ poolMinSize: SESSION_POOL_MIN_SIZE,
101
+ poolMaxSize: SESSION_POOL_MAX_SIZE,
102
+ keepAlivePeriodMs: SESSION_KEEPALIVE_PERIOD_MS,
103
+ }, "YDB driver created with session pool settings");
35
104
  return driver;
36
105
  }
37
106
  export async function readyOrThrow() {
@@ -43,7 +112,13 @@ export async function readyOrThrow() {
43
112
  }
44
113
  export async function withSession(fn) {
45
114
  const d = getOrCreateDriver();
46
- return await d.tableClient.withSession(fn, TABLE_SESSION_TIMEOUT_MS);
115
+ try {
116
+ return await d.tableClient.withSession(fn, TABLE_SESSION_TIMEOUT_MS);
117
+ }
118
+ catch (err) {
119
+ void maybeRefreshDriverOnSessionError(err);
120
+ throw err;
121
+ }
47
122
  }
48
123
  export async function isYdbAvailable(timeoutMs = YDB_HEALTHCHECK_READY_TIMEOUT_MS) {
49
124
  const d = getOrCreateDriver();
@@ -54,3 +129,30 @@ export async function isYdbAvailable(timeoutMs = YDB_HEALTHCHECK_READY_TIMEOUT_M
54
129
  return false;
55
130
  }
56
131
  }
132
+ /**
133
+ * Destroys the current driver and its session pool.
134
+ * Next call to withSession or readyOrThrow will create a new driver.
135
+ */
136
+ export async function destroyDriver() {
137
+ if (!driver) {
138
+ return;
139
+ }
140
+ logger.info("Destroying YDB driver and session pool");
141
+ try {
142
+ await driver.destroy();
143
+ }
144
+ catch (err) {
145
+ logger.warn({ err }, "Error during driver destruction (ignored)");
146
+ }
147
+ driver = undefined;
148
+ }
149
+ /**
150
+ * Destroys the current driver and immediately creates a fresh one.
151
+ * Use this to recover from session pool exhaustion or zombie sessions.
152
+ */
153
+ export async function refreshDriver() {
154
+ logger.info("Refreshing YDB driver");
155
+ await destroyDriver();
156
+ await readyOrThrow();
157
+ logger.info("YDB driver refreshed successfully");
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "4.7.1",
3
+ "version": "4.8.0",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {