ydb-qdrant 7.0.1 → 8.1.1
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/README.md +2 -2
- package/dist/config/env.d.ts +0 -8
- package/dist/config/env.js +2 -29
- package/dist/package/api.d.ts +5 -2
- package/dist/package/api.js +2 -2
- package/dist/qdrant/QdrantRestTypes.d.ts +35 -0
- package/dist/repositories/collectionsRepo.d.ts +1 -2
- package/dist/repositories/collectionsRepo.js +62 -103
- package/dist/repositories/collectionsRepo.one-table.js +103 -47
- package/dist/repositories/collectionsRepo.shared.d.ts +2 -0
- package/dist/repositories/collectionsRepo.shared.js +32 -0
- package/dist/repositories/pointsRepo.d.ts +4 -8
- package/dist/repositories/pointsRepo.one-table/Delete.js +122 -67
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +5 -2
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +7 -6
- package/dist/repositories/pointsRepo.one-table/Search.d.ts +4 -0
- package/dist/repositories/pointsRepo.one-table/Search.js +208 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Upsert.js +51 -66
- package/dist/repositories/pointsRepo.one-table.d.ts +1 -1
- package/dist/repositories/pointsRepo.one-table.js +1 -1
- package/dist/routes/collections.js +7 -61
- package/dist/routes/points.js +15 -66
- package/dist/services/PointsService.d.ts +3 -8
- package/dist/services/PointsService.js +19 -23
- package/dist/types.d.ts +23 -33
- package/dist/types.js +18 -20
- package/dist/utils/normalization.js +13 -14
- package/dist/utils/retry.js +19 -29
- package/dist/utils/vectorBinary.js +10 -5
- package/dist/ydb/bootstrapMetaTable.d.ts +7 -0
- package/dist/ydb/bootstrapMetaTable.js +75 -0
- package/dist/ydb/client.d.ts +23 -17
- package/dist/ydb/client.js +82 -423
- package/dist/ydb/schema.js +88 -148
- package/package.json +2 -10
- package/dist/qdrant/QdrantTypes.d.ts +0 -19
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +0 -18
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +0 -119
- package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +0 -17
- package/dist/repositories/pointsRepo.one-table/Search/Exact.js +0 -101
- package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +0 -8
- package/dist/repositories/pointsRepo.one-table/Search/index.js +0 -30
- package/dist/utils/typeGuards.d.ts +0 -1
- package/dist/utils/typeGuards.js +0 -3
- package/dist/ydb/QueryDiagnostics.d.ts +0 -6
- package/dist/ydb/QueryDiagnostics.js +0 -52
- package/dist/ydb/SessionPool.d.ts +0 -36
- package/dist/ydb/SessionPool.js +0 -248
- package/dist/ydb/bulkUpsert.d.ts +0 -6
- package/dist/ydb/bulkUpsert.js +0 -52
- /package/dist/qdrant/{QdrantTypes.js → QdrantRestTypes.js} +0 -0
package/dist/ydb/client.js
CHANGED
|
@@ -1,194 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { CredentialsProvider } from "@ydbjs/auth";
|
|
4
|
-
import { AnonymousCredentialsProvider } from "@ydbjs/auth/anonymous";
|
|
5
|
-
import { AccessTokenCredentialsProvider } from "@ydbjs/auth/access-token";
|
|
6
|
-
import { MetadataCredentialsProvider } from "@ydbjs/auth/metadata";
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
-
import crypto from "node:crypto";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { createRequire } from "node:module";
|
|
11
|
-
import { pathToFileURL } from "node:url";
|
|
12
|
-
import { YDB_DATABASE, YDB_ENDPOINT, STARTUP_PROBE_SESSION_TIMEOUT_MS, } from "../config/env.js";
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, STARTUP_PROBE_SESSION_TIMEOUT_MS, } from "../config/env.js";
|
|
13
3
|
import { logger } from "../logging/logger.js";
|
|
14
|
-
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, OperationParams, Ydb, } = require("ydb-sdk");
|
|
6
|
+
export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
|
|
7
|
+
export function createExecuteQuerySettings(options) {
|
|
8
|
+
const { keepInCache = true, idempotent = true } = options ?? {};
|
|
9
|
+
const settings = new ExecuteQuerySettings();
|
|
10
|
+
if (keepInCache) {
|
|
11
|
+
settings.withKeepInCache(true);
|
|
12
|
+
}
|
|
13
|
+
if (idempotent) {
|
|
14
|
+
settings.withIdempotent(true);
|
|
15
|
+
}
|
|
16
|
+
return settings;
|
|
17
|
+
}
|
|
18
|
+
export function createExecuteQuerySettingsWithTimeout(options) {
|
|
19
|
+
const settings = createExecuteQuerySettings(options);
|
|
20
|
+
const op = new OperationParams();
|
|
21
|
+
const seconds = Math.max(1, Math.ceil(options.timeoutMs / 1000));
|
|
22
|
+
// Limit both overall operation processing time and cancellation time on the
|
|
23
|
+
// server side so the probe fails fast instead of hanging for the default.
|
|
24
|
+
op.withOperationTimeoutSeconds(seconds);
|
|
25
|
+
op.withCancelAfterSeconds(seconds);
|
|
26
|
+
settings.withOperationParams(op);
|
|
27
|
+
return settings;
|
|
28
|
+
}
|
|
29
|
+
export function createBulkUpsertSettingsWithTimeout(options) {
|
|
30
|
+
const settings = new BulkUpsertSettings();
|
|
31
|
+
const op = new OperationParams();
|
|
32
|
+
const seconds = Math.max(1, Math.ceil(options.timeoutMs / 1000));
|
|
33
|
+
op.withOperationTimeoutSeconds(seconds);
|
|
34
|
+
op.withCancelAfterSeconds(seconds);
|
|
35
|
+
settings.withOperationParams(op);
|
|
36
|
+
return settings;
|
|
37
|
+
}
|
|
15
38
|
const DRIVER_READY_TIMEOUT_MS = 15000;
|
|
39
|
+
const TABLE_SESSION_TIMEOUT_MS = 20000;
|
|
16
40
|
const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
|
|
17
41
|
const DRIVER_REFRESH_COOLDOWN_MS = 30000;
|
|
18
|
-
// Bound `withSession()` so `d.ready()` / session acquire can't hang forever under YDB stalls.
|
|
19
|
-
// Matches historical table client session timeout behavior (~20s).
|
|
20
|
-
const WITH_SESSION_TIMEOUT_MS = 20000;
|
|
21
42
|
let overrideConfig;
|
|
22
43
|
let driver;
|
|
23
|
-
let sqlClient;
|
|
24
|
-
let sessionPool;
|
|
25
44
|
let lastDriverRefreshAt = 0;
|
|
26
45
|
let driverRefreshInFlight = null;
|
|
27
|
-
// Test-only: allows injecting a mock Driver factory
|
|
46
|
+
// Test-only: allows injecting a mock Driver factory
|
|
28
47
|
let driverFactoryOverride;
|
|
29
|
-
let queryCtxPromise = null;
|
|
30
|
-
async function getQueryCtx() {
|
|
31
|
-
if (queryCtxPromise) {
|
|
32
|
-
return await queryCtxPromise;
|
|
33
|
-
}
|
|
34
|
-
const require = createRequire(import.meta.url);
|
|
35
|
-
const entry = require.resolve("@ydbjs/query");
|
|
36
|
-
const ctxPath = path.join(path.dirname(entry), "ctx.js");
|
|
37
|
-
queryCtxPromise = import(pathToFileURL(ctxPath).href);
|
|
38
|
-
return await queryCtxPromise;
|
|
39
|
-
}
|
|
40
|
-
const IAM_TOKEN_URL = "https://iam.api.cloud.yandex.net/iam/v1/tokens";
|
|
41
|
-
const SA_JWT_MAX_AGE_SECONDS = 3600;
|
|
42
|
-
const SA_TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
43
|
-
// Yandex Cloud IAM tokens: lifetime does not exceed 12 hours, but the docs recommend
|
|
44
|
-
// requesting a token more often (e.g., every hour). We refresh a bit earlier as a
|
|
45
|
-
// best-effort fallback when the API response doesn't include expiresAt.
|
|
46
|
-
// Source: https://cloud.yandex.com/en/docs/iam/operations/iam-token/create-for-sa
|
|
47
|
-
const SA_TOKEN_REFRESH_FALLBACK_MS = 55 * 60_000;
|
|
48
|
-
function parseTruthyEnv(value) {
|
|
49
|
-
if (value === undefined)
|
|
50
|
-
return false;
|
|
51
|
-
const normalized = value.trim().toLowerCase();
|
|
52
|
-
if (normalized === "" ||
|
|
53
|
-
normalized === "0" ||
|
|
54
|
-
normalized === "false" ||
|
|
55
|
-
normalized === "no" ||
|
|
56
|
-
normalized === "off") {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
function toBase64Url(input) {
|
|
62
|
-
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
|
|
63
|
-
return buf
|
|
64
|
-
.toString("base64")
|
|
65
|
-
.replace(/\+/g, "-")
|
|
66
|
-
.replace(/\//g, "_")
|
|
67
|
-
.replace(/=+$/g, "");
|
|
68
|
-
}
|
|
69
|
-
function jsonToBase64Url(obj) {
|
|
70
|
-
return toBase64Url(Buffer.from(JSON.stringify(obj), "utf8"));
|
|
71
|
-
}
|
|
72
|
-
function normalizeServiceAccountPrivateKeyPem(raw) {
|
|
73
|
-
const trimmed = raw.trim();
|
|
74
|
-
if (trimmed.startsWith("PLEASE DO NOT REMOVE THIS LINE!")) {
|
|
75
|
-
return trimmed.split("\n").slice(1).join("\n").trim();
|
|
76
|
-
}
|
|
77
|
-
return trimmed;
|
|
78
|
-
}
|
|
79
|
-
async function readServiceAccountKeyFile(absPath) {
|
|
80
|
-
const raw = await readFile(absPath, "utf8");
|
|
81
|
-
const parsed = JSON.parse(raw);
|
|
82
|
-
if (typeof parsed.id !== "string" ||
|
|
83
|
-
typeof parsed.service_account_id !== "string" ||
|
|
84
|
-
typeof parsed.private_key !== "string") {
|
|
85
|
-
throw new Error("Invalid service account key JSON: expected fields id, service_account_id, private_key");
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
id: parsed.id,
|
|
89
|
-
service_account_id: parsed.service_account_id,
|
|
90
|
-
private_key: parsed.private_key,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
function createServiceAccountJwt(args) {
|
|
94
|
-
// Per Yandex Cloud IAM docs, the service account JWT must be PS256 with kid
|
|
95
|
-
// and audience set to the IAM token endpoint.
|
|
96
|
-
const header = { typ: "JWT", alg: "PS256", kid: args.keyId };
|
|
97
|
-
const payload = {
|
|
98
|
-
iss: args.serviceAccountId,
|
|
99
|
-
aud: IAM_TOKEN_URL,
|
|
100
|
-
iat: args.nowSec,
|
|
101
|
-
exp: args.nowSec + SA_JWT_MAX_AGE_SECONDS,
|
|
102
|
-
};
|
|
103
|
-
const encodedHeader = jsonToBase64Url(header);
|
|
104
|
-
const encodedPayload = jsonToBase64Url(payload);
|
|
105
|
-
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
106
|
-
const signature = crypto.sign("RSA-SHA256", Buffer.from(signingInput), {
|
|
107
|
-
key: normalizeServiceAccountPrivateKeyPem(args.privateKeyPem),
|
|
108
|
-
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
|
109
|
-
saltLength: 32,
|
|
110
|
-
});
|
|
111
|
-
return `${signingInput}.${toBase64Url(signature)}`;
|
|
112
|
-
}
|
|
113
|
-
async function exchangeJwtForIamToken(args) {
|
|
114
|
-
const res = await fetch(IAM_TOKEN_URL, {
|
|
115
|
-
method: "POST",
|
|
116
|
-
headers: { "content-type": "application/json" },
|
|
117
|
-
body: JSON.stringify({ jwt: args.jwt }),
|
|
118
|
-
signal: args.signal,
|
|
119
|
-
});
|
|
120
|
-
if (!res.ok) {
|
|
121
|
-
const text = await res.text().catch(() => "");
|
|
122
|
-
throw new Error(`Failed to exchange JWT for IAM token: HTTP ${res.status}${text ? `: ${text}` : ""}`);
|
|
123
|
-
}
|
|
124
|
-
const body = (await res.json());
|
|
125
|
-
const token = body.iamToken;
|
|
126
|
-
if (typeof token !== "string" || token.length === 0) {
|
|
127
|
-
throw new Error("IAM token exchange response is missing iamToken");
|
|
128
|
-
}
|
|
129
|
-
const expiresAtRaw = body.expiresAt ?? body.expires_at;
|
|
130
|
-
const expiresAtMs = typeof expiresAtRaw === "string" ? Date.parse(expiresAtRaw) : NaN;
|
|
131
|
-
return {
|
|
132
|
-
iamToken: token,
|
|
133
|
-
expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : null,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
class ServiceAccountKeyFileCredentialsProvider extends CredentialsProvider {
|
|
137
|
-
keyFilePath;
|
|
138
|
-
cached = null;
|
|
139
|
-
constructor(keyFilePath) {
|
|
140
|
-
super();
|
|
141
|
-
this.keyFilePath = keyFilePath;
|
|
142
|
-
}
|
|
143
|
-
async getToken(force = false, signal) {
|
|
144
|
-
const nowMs = Date.now();
|
|
145
|
-
if (!force && this.cached) {
|
|
146
|
-
const { token, expiresAtMs, cachedAtMs } = this.cached;
|
|
147
|
-
if (expiresAtMs !== null) {
|
|
148
|
-
if (nowMs + SA_TOKEN_REFRESH_SKEW_MS < expiresAtMs) {
|
|
149
|
-
return token;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
// If the API doesn't provide expiresAt, refresh hourly (best-effort).
|
|
154
|
-
if (nowMs - cachedAtMs < SA_TOKEN_REFRESH_FALLBACK_MS) {
|
|
155
|
-
return token;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const key = await readServiceAccountKeyFile(this.keyFilePath);
|
|
160
|
-
const nowSec = Math.floor(nowMs / 1000);
|
|
161
|
-
const jwt = createServiceAccountJwt({
|
|
162
|
-
keyId: key.id,
|
|
163
|
-
serviceAccountId: key.service_account_id,
|
|
164
|
-
privateKeyPem: key.private_key,
|
|
165
|
-
nowSec,
|
|
166
|
-
});
|
|
167
|
-
const { iamToken, expiresAtMs } = await exchangeJwtForIamToken({
|
|
168
|
-
jwt,
|
|
169
|
-
signal,
|
|
170
|
-
});
|
|
171
|
-
this.cached = { token: iamToken, expiresAtMs, cachedAtMs: nowMs };
|
|
172
|
-
return iamToken;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function resolveCredentialsProviderFromEnv() {
|
|
176
|
-
const saKeyPath = process.env.YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS?.trim();
|
|
177
|
-
if (saKeyPath) {
|
|
178
|
-
return new ServiceAccountKeyFileCredentialsProvider(saKeyPath);
|
|
179
|
-
}
|
|
180
|
-
if (parseTruthyEnv(process.env.YDB_METADATA_CREDENTIALS)) {
|
|
181
|
-
return new MetadataCredentialsProvider();
|
|
182
|
-
}
|
|
183
|
-
const accessToken = process.env.YDB_ACCESS_TOKEN_CREDENTIALS?.trim();
|
|
184
|
-
if (accessToken) {
|
|
185
|
-
return new AccessTokenCredentialsProvider({ token: accessToken });
|
|
186
|
-
}
|
|
187
|
-
if (parseTruthyEnv(process.env.YDB_ANONYMOUS_CREDENTIALS)) {
|
|
188
|
-
return new AnonymousCredentialsProvider();
|
|
189
|
-
}
|
|
190
|
-
throw new Error("No YDB credentials configured. Set one of: YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS, YDB_METADATA_CREDENTIALS=1, YDB_ACCESS_TOKEN_CREDENTIALS, YDB_ANONYMOUS_CREDENTIALS=1");
|
|
191
|
-
}
|
|
192
48
|
export function isCompilationTimeoutError(error) {
|
|
193
49
|
if (!(error instanceof Error)) {
|
|
194
50
|
return false;
|
|
@@ -207,54 +63,6 @@ export function isCompilationTimeoutError(error) {
|
|
|
207
63
|
}
|
|
208
64
|
return false;
|
|
209
65
|
}
|
|
210
|
-
function getErrorCause(error) {
|
|
211
|
-
return error.cause;
|
|
212
|
-
}
|
|
213
|
-
function getCauseName(cause) {
|
|
214
|
-
if (cause instanceof Error) {
|
|
215
|
-
return cause.name;
|
|
216
|
-
}
|
|
217
|
-
if (typeof cause === "object" && cause !== null) {
|
|
218
|
-
const n = cause.name;
|
|
219
|
-
if (typeof n === "string") {
|
|
220
|
-
return n;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
export function isTimeoutAbortError(error) {
|
|
226
|
-
// `@ydbjs/query` implements `.timeout(ms)` by adding `AbortSignal.timeout(ms)` to the query
|
|
227
|
-
// signal chain. When that fires, @ydbjs/retry wraps it via `@ydbjs/abortable` and we end up
|
|
228
|
-
// observing a DOMException `{ name: "AbortError" }`, often with an underlying `cause`
|
|
229
|
-
// `{ name: "TimeoutError" }`.
|
|
230
|
-
if (!(error instanceof Error)) {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
if (error.name === "TimeoutError") {
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
if (error.name !== "AbortError") {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
const cause = getErrorCause(error);
|
|
240
|
-
const causeName = getCauseName(cause);
|
|
241
|
-
if (causeName === "TimeoutError") {
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
// Node versions differ in how abort reasons are propagated through `AbortSignal.any(...)`.
|
|
245
|
-
// In our codebase we currently only use AbortError signals for query timeouts / cancellations,
|
|
246
|
-
// so treat an AbortError without a usable cause as timeout-like.
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
export function getAbortErrorCause(error) {
|
|
250
|
-
if (!(error instanceof Error)) {
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
if (error.name !== "AbortError") {
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
|
-
return getErrorCause(error);
|
|
257
|
-
}
|
|
258
66
|
function shouldTriggerDriverRefresh(error) {
|
|
259
67
|
if (!(error instanceof Error)) {
|
|
260
68
|
return false;
|
|
@@ -269,8 +77,9 @@ function shouldTriggerDriverRefresh(error) {
|
|
|
269
77
|
if (/SessionExpired|SESSION_EXPIRED|session.*expired/i.test(msg)) {
|
|
270
78
|
return true;
|
|
271
79
|
}
|
|
272
|
-
//
|
|
273
|
-
// subsequent attempts use a
|
|
80
|
+
// YDB query compilation timeout (TIMEOUT code 400090) – treat as a signal
|
|
81
|
+
// to refresh the driver/session pool so that subsequent attempts use a
|
|
82
|
+
// fresh connection state.
|
|
274
83
|
if (isCompilationTimeoutError(error)) {
|
|
275
84
|
return true;
|
|
276
85
|
}
|
|
@@ -306,12 +115,6 @@ async function maybeRefreshDriverOnSessionError(error) {
|
|
|
306
115
|
}
|
|
307
116
|
export function __setDriverForTests(fake) {
|
|
308
117
|
driver = fake;
|
|
309
|
-
sqlClient = undefined;
|
|
310
|
-
sessionPool = undefined;
|
|
311
|
-
queryCtxPromise = null;
|
|
312
|
-
}
|
|
313
|
-
export function __setSessionPoolForTests(fake) {
|
|
314
|
-
sessionPool = fake;
|
|
315
118
|
}
|
|
316
119
|
export function __setDriverFactoryForTests(factory) {
|
|
317
120
|
driverFactoryOverride = factory;
|
|
@@ -327,207 +130,81 @@ export function configureDriver(config) {
|
|
|
327
130
|
}
|
|
328
131
|
overrideConfig = config;
|
|
329
132
|
}
|
|
330
|
-
function buildConnectionString(args) {
|
|
331
|
-
const endpoint = args.endpoint.trim();
|
|
332
|
-
const database = args.database.trim();
|
|
333
|
-
if (!endpoint) {
|
|
334
|
-
throw new Error("YDB endpoint is empty; set YDB_ENDPOINT");
|
|
335
|
-
}
|
|
336
|
-
if (!database) {
|
|
337
|
-
throw new Error("YDB database is empty; set YDB_DATABASE");
|
|
338
|
-
}
|
|
339
|
-
const url = new URL(endpoint);
|
|
340
|
-
const dbPath = database.startsWith("/") ? database : `/${database}`;
|
|
341
|
-
url.pathname = dbPath;
|
|
342
|
-
return url.toString();
|
|
343
|
-
}
|
|
344
133
|
function getOrCreateDriver() {
|
|
345
134
|
if (driver) {
|
|
346
135
|
return driver;
|
|
347
136
|
}
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
: "";
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
connectionString = buildConnectionString({ endpoint, database });
|
|
366
|
-
}
|
|
367
|
-
const credentialsProvider = overrideConfig?.credentialsProvider
|
|
368
|
-
? overrideConfig.credentialsProvider
|
|
369
|
-
: driverFactoryOverride
|
|
370
|
-
? undefined
|
|
371
|
-
: resolveCredentialsProviderFromEnv();
|
|
372
|
-
const driverOptions = {
|
|
373
|
-
...(credentialsProvider ? { credentialsProvider } : {}),
|
|
374
|
-
"ydb.sdk.ready_timeout_ms": DRIVER_READY_TIMEOUT_MS,
|
|
137
|
+
const base = overrideConfig?.connectionString != null
|
|
138
|
+
? { connectionString: overrideConfig.connectionString }
|
|
139
|
+
: {
|
|
140
|
+
endpoint: overrideConfig?.endpoint ?? YDB_ENDPOINT,
|
|
141
|
+
database: overrideConfig?.database ?? YDB_DATABASE,
|
|
142
|
+
};
|
|
143
|
+
const driverConfig = {
|
|
144
|
+
...base,
|
|
145
|
+
authService: overrideConfig?.authService ?? getCredentialsFromEnv(),
|
|
146
|
+
poolSettings: {
|
|
147
|
+
minLimit: SESSION_POOL_MIN_SIZE,
|
|
148
|
+
maxLimit: SESSION_POOL_MAX_SIZE,
|
|
149
|
+
keepAlivePeriod: SESSION_KEEPALIVE_PERIOD_MS,
|
|
150
|
+
},
|
|
375
151
|
};
|
|
376
152
|
driver = driverFactoryOverride
|
|
377
|
-
? driverFactoryOverride(
|
|
378
|
-
: new Driver(
|
|
379
|
-
logger.info({
|
|
153
|
+
? driverFactoryOverride(driverConfig)
|
|
154
|
+
: new Driver(driverConfig);
|
|
155
|
+
logger.info({
|
|
156
|
+
poolMinSize: SESSION_POOL_MIN_SIZE,
|
|
157
|
+
poolMaxSize: SESSION_POOL_MAX_SIZE,
|
|
158
|
+
keepAlivePeriodMs: SESSION_KEEPALIVE_PERIOD_MS,
|
|
159
|
+
}, "YDB driver created with session pool settings");
|
|
380
160
|
return driver;
|
|
381
161
|
}
|
|
382
|
-
|
|
383
|
-
* @internal
|
|
384
|
-
* Exposes the singleton YDB `Driver` for internal modules that need non-QueryService RPCs
|
|
385
|
-
* (e.g. TableService BulkUpsert).
|
|
386
|
-
*/
|
|
387
|
-
export function __getDriverForInternalUse() {
|
|
388
|
-
return getOrCreateDriver();
|
|
389
|
-
}
|
|
390
|
-
function getOrCreateQueryClient() {
|
|
391
|
-
if (sqlClient) {
|
|
392
|
-
return sqlClient;
|
|
393
|
-
}
|
|
162
|
+
export async function readyOrThrow() {
|
|
394
163
|
const d = getOrCreateDriver();
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
function getOrCreateSessionPool() {
|
|
399
|
-
if (sessionPool) {
|
|
400
|
-
return sessionPool;
|
|
164
|
+
const ok = await d.ready(DRIVER_READY_TIMEOUT_MS);
|
|
165
|
+
if (!ok) {
|
|
166
|
+
throw new Error(`YDB driver is not ready in ${DRIVER_READY_TIMEOUT_MS / 1000}s. Check connectivity and credentials.`);
|
|
401
167
|
}
|
|
402
|
-
const d = getOrCreateDriver();
|
|
403
|
-
sessionPool = new SessionPool(d);
|
|
404
|
-
// Keepalive is best-effort; don't block startup.
|
|
405
|
-
sessionPool.start();
|
|
406
|
-
return sessionPool;
|
|
407
168
|
}
|
|
408
|
-
async function
|
|
409
|
-
const
|
|
410
|
-
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
169
|
+
export async function withSession(fn) {
|
|
170
|
+
const d = getOrCreateDriver();
|
|
411
171
|
try {
|
|
412
|
-
return await fn
|
|
172
|
+
return await d.tableClient.withSession(fn, TABLE_SESSION_TIMEOUT_MS);
|
|
413
173
|
}
|
|
414
|
-
|
|
415
|
-
|
|
174
|
+
catch (err) {
|
|
175
|
+
void maybeRefreshDriverOnSessionError(err);
|
|
176
|
+
throw err;
|
|
416
177
|
}
|
|
417
178
|
}
|
|
418
|
-
export async function
|
|
419
|
-
const d = getOrCreateDriver();
|
|
420
|
-
await withTimeoutSignal(DRIVER_READY_TIMEOUT_MS, async (signal) => {
|
|
421
|
-
await d.ready(signal);
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
export async function withSession(fn) {
|
|
425
|
-
const sql = getOrCreateQueryClient();
|
|
179
|
+
export async function withQuerySession(fn, options) {
|
|
426
180
|
const d = getOrCreateDriver();
|
|
427
|
-
const pool = getOrCreateSessionPool();
|
|
428
181
|
try {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
await d.ready(signal);
|
|
182
|
+
return await d.queryClient.do({
|
|
183
|
+
fn,
|
|
184
|
+
timeout: options?.timeoutMs ?? TABLE_SESSION_TIMEOUT_MS,
|
|
185
|
+
idempotent: options?.idempotent,
|
|
434
186
|
});
|
|
435
|
-
// Ensure we have a few sessions ready to reduce cold-start session churn.
|
|
436
|
-
// Best-effort: ignore failures/timeouts.
|
|
437
|
-
void withTimeoutSignal(WITH_SESSION_TIMEOUT_MS, async (signal) => {
|
|
438
|
-
await pool.warmup(signal);
|
|
439
|
-
}).catch(() => undefined);
|
|
440
|
-
// @ydbjs/query QueryClient.do() is not implemented in the currently published builds.
|
|
441
|
-
// Cancellation is cooperative: we provide an AbortSignal to `fn()` and also store it in
|
|
442
|
-
// the @ydbjs/query async-local context so query operations can observe it via `.signal(...)`.
|
|
443
|
-
// (Note: this signal is not auto-aborted by `withSession()`; query-level `.timeout(...)`
|
|
444
|
-
// is the primary guard for long-running YQL statements.)
|
|
445
|
-
const operationSignal = new AbortController().signal;
|
|
446
|
-
const ctxMod = await getQueryCtx();
|
|
447
|
-
let leased = null;
|
|
448
|
-
const MAX_ATTEMPTS = 3;
|
|
449
|
-
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
|
|
450
|
-
leased = await withTimeoutSignal(WITH_SESSION_TIMEOUT_MS, async (signal) => {
|
|
451
|
-
return await pool.acquire(signal);
|
|
452
|
-
});
|
|
453
|
-
try {
|
|
454
|
-
const store = {
|
|
455
|
-
nodeId: leased.nodeId,
|
|
456
|
-
sessionId: leased.sessionId,
|
|
457
|
-
signal: operationSignal,
|
|
458
|
-
};
|
|
459
|
-
const result = await ctxMod.ctx.run(store, async () => {
|
|
460
|
-
return await fn(sql, operationSignal);
|
|
461
|
-
});
|
|
462
|
-
pool.release(leased);
|
|
463
|
-
return result;
|
|
464
|
-
}
|
|
465
|
-
catch (err) {
|
|
466
|
-
const shouldRetryWithFreshSession = err instanceof Error &&
|
|
467
|
-
/BAD_SESSION|SESSION_EXPIRED|SessionExpired|No session became available/i.test(err.message ?? "");
|
|
468
|
-
if (shouldRetryWithFreshSession) {
|
|
469
|
-
await pool.discard(leased);
|
|
470
|
-
leased = null;
|
|
471
|
-
if (attempt === MAX_ATTEMPTS - 1) {
|
|
472
|
-
throw err;
|
|
473
|
-
}
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
pool.release(leased);
|
|
477
|
-
throw err;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
throw new Error("withSession: exhausted attempts to acquire a healthy session");
|
|
481
187
|
}
|
|
482
188
|
catch (err) {
|
|
189
|
+
// Query sessions can also get stuck/busy; reuse the same driver refresh path.
|
|
483
190
|
void maybeRefreshDriverOnSessionError(err);
|
|
484
191
|
throw err;
|
|
485
192
|
}
|
|
486
193
|
}
|
|
487
194
|
export async function withStartupProbeSession(fn) {
|
|
488
|
-
const sql = getOrCreateQueryClient();
|
|
489
195
|
const d = getOrCreateDriver();
|
|
490
|
-
const pool = getOrCreateSessionPool();
|
|
491
|
-
const ac = new AbortController();
|
|
492
|
-
const t = setTimeout(() => ac.abort(), STARTUP_PROBE_SESSION_TIMEOUT_MS);
|
|
493
196
|
try {
|
|
494
|
-
|
|
495
|
-
await d.ready(ac.signal);
|
|
496
|
-
void pool.warmup(ac.signal);
|
|
497
|
-
const ctxMod = await getQueryCtx();
|
|
498
|
-
const leased = await pool.acquire(ac.signal);
|
|
499
|
-
try {
|
|
500
|
-
const store = {
|
|
501
|
-
nodeId: leased.nodeId,
|
|
502
|
-
sessionId: leased.sessionId,
|
|
503
|
-
signal: ac.signal,
|
|
504
|
-
};
|
|
505
|
-
const result = await ctxMod.ctx.run(store, async () => {
|
|
506
|
-
return await fn(sql, ac.signal);
|
|
507
|
-
});
|
|
508
|
-
pool.release(leased);
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
catch (err) {
|
|
512
|
-
pool.release(leased);
|
|
513
|
-
throw err;
|
|
514
|
-
}
|
|
197
|
+
return await d.tableClient.withSession(fn, STARTUP_PROBE_SESSION_TIMEOUT_MS);
|
|
515
198
|
}
|
|
516
199
|
catch (err) {
|
|
517
200
|
void maybeRefreshDriverOnSessionError(err);
|
|
518
201
|
throw err;
|
|
519
202
|
}
|
|
520
|
-
finally {
|
|
521
|
-
clearTimeout(t);
|
|
522
|
-
}
|
|
523
203
|
}
|
|
524
204
|
export async function isYdbAvailable(timeoutMs = YDB_HEALTHCHECK_READY_TIMEOUT_MS) {
|
|
525
205
|
const d = getOrCreateDriver();
|
|
526
206
|
try {
|
|
527
|
-
await
|
|
528
|
-
await d.ready(signal);
|
|
529
|
-
});
|
|
530
|
-
return true;
|
|
207
|
+
return await d.ready(timeoutMs);
|
|
531
208
|
}
|
|
532
209
|
catch {
|
|
533
210
|
return false;
|
|
@@ -541,27 +218,9 @@ export async function destroyDriver() {
|
|
|
541
218
|
if (!driver) {
|
|
542
219
|
return;
|
|
543
220
|
}
|
|
544
|
-
logger.info("Destroying YDB driver");
|
|
221
|
+
logger.info("Destroying YDB driver and session pool");
|
|
545
222
|
try {
|
|
546
|
-
|
|
547
|
-
try {
|
|
548
|
-
await sessionPool.close();
|
|
549
|
-
}
|
|
550
|
-
catch (err) {
|
|
551
|
-
logger.warn({ err }, "Error during session pool close (ignored)");
|
|
552
|
-
}
|
|
553
|
-
sessionPool = undefined;
|
|
554
|
-
}
|
|
555
|
-
if (sqlClient) {
|
|
556
|
-
try {
|
|
557
|
-
await sqlClient[Symbol.asyncDispose]();
|
|
558
|
-
}
|
|
559
|
-
catch (err) {
|
|
560
|
-
logger.warn({ err }, "Error during query client disposal (ignored)");
|
|
561
|
-
}
|
|
562
|
-
sqlClient = undefined;
|
|
563
|
-
}
|
|
564
|
-
driver.close();
|
|
223
|
+
await driver.destroy();
|
|
565
224
|
}
|
|
566
225
|
catch (err) {
|
|
567
226
|
logger.warn({ err }, "Error during driver destruction (ignored)");
|