UNPKG

@zilliz/milvus2-sdk-node

Version:

Official Node.js SDK for [Milvus](https://github.com/milvus-io/milvus) vector database. Provides gRPC and HTTP clients for vector similarity search, metadata filtering, and full collection/index/user management.

634 lines • 31.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GRPCClient = exports.LOADER_OPTIONS = void 0; const grpc_js_1 = require("@grpc/grpc-js"); const dayjs_1 = __importDefault(require("dayjs")); const generic_pool_1 = require("generic-pool"); const __1 = require("../"); const User_1 = require("./User"); // default loader options exports.LOADER_OPTIONS = { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, // populate oneof fields }; /** * A client for interacting with the Milvus server via gRPC. */ class GRPCClient extends User_1.User { /** * Creates a new instance of MilvusClient. * @param configOrAddress The Milvus server's address or client configuration object. * @param ssl Whether to use SSL or not. * @param username The username for authentication. * @param password The password for authentication. * @param channelOptions Additional channel options for gRPC. */ constructor(configOrAddress, ssl, username, password, channelOptions) { // setup the configuration super(configOrAddress, ssl, username, password, channelOptions); // Incremented whenever the live telemetry endpoint changes. The telemetry // manager uses this epoch to discard a response that arrives from an old // endpoint after a successful global-cluster failover. this.telemetryEndpointEpoch = 0; // Async topology discovery and candidate validation can complete after close(). Fence // their publication so a released candidate can never resurrect a closed client. this.lifecycleGeneration = 0; this.closed = false; // Store sdkVersion for reconnection this._sdkVersion = ''; // alias this.useDatabase = this.use; // Get the gRPC service for Milvus this._MilvusService = (0, __1.getGRPCService)({ serviceName: this.protoInternalPath.serviceName, // the name of the Milvus service }, Object.assign(Object.assign({}, exports.LOADER_OPTIONS), this.config.loaderOptions)); this._ClientTelemetryService = (0, __1.getGRPCService)({ serviceName: 'milvus.proto.milvus.ClientTelemetryService', }, Object.assign(Object.assign({}, exports.LOADER_OPTIONS), this.config.loaderOptions)); this.telemetryDatabaseExplicit = Boolean(this.config.database); this.telemetry = new __1.ClientTelemetryManager({ sender: request => this.sendTelemetryHeartbeat(request), config: this.config.telemetry, userProvider: () => this.config.username || '', databaseProvider: () => this.telemetryDatabaseExplicit ? this.metadata.get(__1.METADATA.DATABASE) || __1.DEFAULT_DB : '', configProvider: () => ({ address: this.config.address, username: this.config.username, database: this.metadata.get(__1.METADATA.DATABASE) || __1.DEFAULT_DB, ssl: this.config.ssl, timeout: this.config.timeout, }), senderEpochProvider: () => this.telemetryEndpointEpoch, }); // setup auth if necessary const auth = (0, __1.getAuthString)(this.config); if (auth.length > 0) { this.metadata.set(__1.METADATA.AUTH, auth); } // setup database this.metadata.set(__1.METADATA.DATABASE, this.config.database || __1.DEFAULT_DB); // meta interceptor, add the injector const metaInterceptor = (0, __1.getMetaInterceptor)(this.metadataListener.bind(this)); // retry interceptor const retryInterceptor = (0, __1.getRetryInterceptor)({ maxRetries: typeof this.config.maxRetries === 'undefined' ? __1.DEFAULT_MAX_RETRIES : this.config.maxRetries, retryDelay: typeof this.config.retryDelay === 'undefined' ? __1.DEFAULT_RETRY_DELAY : this.config.retryDelay, clientId: this.clientId, }); // interceptors const interceptors = [metaInterceptor]; // add request metadata interceptor (adds client-request-unixmsec) interceptors.push((0, __1.getRequestMetadataInterceptor)()); // add trace if necessary if (this.config.trace) { // add trace interceptor interceptors.push((0, __1.getTraceInterceptor)()); } // Heartbeats are best-effort: keep auth/request metadata/trace propagation, but do not // let the ordinary RPC retry interceptor turn one heartbeat into a retry burst. The // telemetry manager owns its own next-heartbeat/backoff policy. this.telemetryChannelOptions = Object.assign(Object.assign({}, this.channelOptions), { interceptors: [...interceptors] }); // Ordinary Milvus RPCs retain the configured retry behavior. this.channelOptions.interceptors = [...interceptors, retryInterceptor]; // For global cluster, skip pool creation here — pool will be created // in connect() after topology is fetched and primary endpoint is resolved. if (!this.isGlobal) { this.channelPool = this.createChannelPool(); } } // create a grpc service client(connect) connect(sdkVersion) { if (this.closed) { return; } this.lifecycleGeneration += 1; const lifecycleGeneration = this.lifecycleGeneration; this._sdkVersion = sdkVersion; this.telemetry.setSdkVersion(sdkVersion); if (this.isGlobal) { // For global cluster: fetch topology → create pool → connect this.connectPromise = this._initGlobalConnection(sdkVersion, lifecycleGeneration); } else { // Normal connection this.replaceTelemetryClient(); this.connectPromise = this._getServerInfo(sdkVersion, this.channelPool, true, lifecycleGeneration); } } /** * Initializes a global cluster connection. * Fetches topology, resolves primary endpoint, creates pool, starts refresher. */ _initGlobalConnection(sdkVersion, lifecycleGeneration = this.lifecycleGeneration) { return __awaiter(this, void 0, void 0, function* () { const token = this.config.token || ''; __1.logger.debug(`\x1b[36m[Global]\x1b[0m Initializing global cluster connection to ${this.globalEndpoint}`); // Fetch topology to discover primary cluster const topology = yield (0, __1.fetchTopology)(this.globalEndpoint, token); if (!this.isLifecycleCurrent(lifecycleGeneration)) { return; } this.globalTopology = topology; // Resolve primary endpoint and create pool const primary = (0, __1.getPrimaryCluster)(topology); this.config.address = primary.endpoint; __1.logger.debug(`\x1b[36m[Global]\x1b[0m Resolved primary: ${primary.endpoint} (cluster=${primary.clusterId}), creating channel pool`); this.channelPool = this.createChannelPool(); this.replaceTelemetryClient(); this._attachFailoverHandler(); // Start background topology refresher this.topologyRefresher = new __1.TopologyRefresher({ globalEndpoint: this.globalEndpoint, token, topology, onTopologyChange: newTopology => { this.globalTopology = newTopology; }, }); this.topologyRefresher.start(); // Now connect to the primary. Validate without side effects first because close may // run while the RPC is awaiting a response. const serverInfo = yield this._getServerInfo(sdkVersion, this.channelPool, false); if (!this.isLifecycleCurrent(lifecycleGeneration)) { return serverInfo; } this.applyServerInfo(serverInfo); return serverInfo; }); } /** * Reconnects to a new primary cluster after failover. * Creates a new pool for the new primary, then drains the old pool. * @returns true if primary changed and reconnection happened, false if primary unchanged */ reconnectToPrimary() { return __awaiter(this, void 0, void 0, function* () { if (this.closed) { return false; } // Serialize concurrent failover attempts if (this.isReconnecting) { __1.logger.debug(`\x1b[36m[Global]\x1b[0m Reconnect already in progress, waiting for completion`); if (this.reconnectingPromise) { return this.reconnectingPromise; } return false; } let primaryChanged = false; const lifecycleGeneration = this.lifecycleGeneration; this.isReconnecting = true; this.reconnectingPromise = (() => __awaiter(this, void 0, void 0, function* () { var _a; let candidatePool; let candidateTelemetryClient; let candidateRefresher; try { const token = this.config.token || ''; __1.logger.debug(`\x1b[36m[Global]\x1b[0m Attempting reconnect, fetching fresh topology`); // Fetch fresh topology const newTopology = yield (0, __1.fetchTopology)(this.globalEndpoint, token); if (!this.isLifecycleCurrent(lifecycleGeneration)) { return false; } const newPrimary = (0, __1.getPrimaryCluster)(newTopology); // Check if primary actually changed if (newPrimary.endpoint === this.config.address) { __1.logger.debug(`\x1b[36m[Global]\x1b[0m Primary unchanged (${this.config.address}), no reconnect needed`); this.globalTopology = newTopology; return false; // Primary hasn't changed, no reconnect needed } __1.logger.info(`Global cluster failover: ${this.config.address} -> ${newPrimary.endpoint}`); // Build and validate a complete candidate lifecycle without mutating the live // address, pool, telemetry transport, topology, or connection status. In // particular, pool factories must capture the candidate address rather than // reading this.config.address later when generic-pool creates a client. __1.logger.debug(`\x1b[36m[Global]\x1b[0m Creating new channel pool for ${newPrimary.endpoint}`); candidatePool = this.createChannelPool(newPrimary.endpoint); candidateTelemetryClient = this.createTelemetryClient(newPrimary.endpoint); const candidateServerInfo = yield this._getServerInfo(this._sdkVersion, candidatePool, false); if (!this.isLifecycleCurrent(lifecycleGeneration)) { try { candidateTelemetryClient.close(); } catch (_b) { // ignore cleanup errors on an unpublished stale candidate } candidateTelemetryClient = undefined; yield this.disposeChannelPool(candidatePool); candidatePool = undefined; return false; } candidateRefresher = new __1.TopologyRefresher({ globalEndpoint: this.globalEndpoint, token, topology: newTopology, onTopologyChange: t => { this.globalTopology = t; }, }); // Attaching the handler only mutates the isolated candidate pool and makes it // ready for publication without exposing it to ordinary operations yet. this._attachFailoverHandler(candidatePool); const oldAddress = this.config.address; const oldPool = this.channelPool; const oldTelemetryClient = this.telemetryClient; const oldRefresher = this.topologyRefresher; // JavaScript runs this assignment block without interleaving another promise // continuation. Advance the epoch before publishing the candidate telemetry // client so any late response from the old endpoint is ignored as a whole. this.telemetryEndpointEpoch += 1; this.config.address = newPrimary.endpoint; this.channelPool = candidatePool; this.telemetryClient = candidateTelemetryClient; this.applyServerInfo(candidateServerInfo, false); this.globalTopology = newTopology; this.topologyRefresher = candidateRefresher; primaryChanged = true; // Candidate resources are now owned by the live lifecycle and must not be // cleaned up by the failure path. candidatePool = undefined; candidateTelemetryClient = undefined; candidateRefresher = undefined; // Everything below is post-commit cleanup/startup. None of it may enter the // candidate-validation catch path and pretend the already-published lifecycle // was rolled back. try { (_a = this.topologyRefresher) === null || _a === void 0 ? void 0 : _a.start(); } catch (error) { __1.logger.warn(`Failed to start topology refresher: ${error.message}`); } if (candidateServerInfo === null || candidateServerInfo === void 0 ? void 0 : candidateServerInfo.identifier) { try { this.telemetry.start(); } catch (error) { __1.logger.warn(`Failed to start client telemetry: ${error.message}`); } } try { oldRefresher === null || oldRefresher === void 0 ? void 0 : oldRefresher.stop(); } catch (error) { __1.logger.warn(`Failed to stop old topology refresher: ${error.message}`); } try { oldTelemetryClient === null || oldTelemetryClient === void 0 ? void 0 : oldTelemetryClient.close(); } catch (error) { __1.logger.warn(`Failed to close old telemetry client: ${error.message}`); } // Existing operations may still hold a client from the old pool. drain() waits // for those borrowers before clear() closes the channels, so late operations can // finish while all new operations use the newly published pool. if (oldPool) { __1.logger.debug(`\x1b[36m[Global]\x1b[0m Draining old channel pool for ${oldAddress}`); yield this.disposeChannelPool(oldPool); } } catch (e) { if (primaryChanged) { // Publication is the commit point. An unexpected post-commit cleanup error // must not close the new live resources or report a rollback that did not // happen. __1.logger.warn(`Global cluster failover committed with a cleanup error: ${e.message}`); return true; } __1.logger.warn(`Global cluster failover failed: ${e.message}`); // The live lifecycle was not touched unless the synchronous publication block // completed. Candidate validation failures therefore leave the old address, // pool, telemetry manager/client/state, topology refresher, and status usable. try { candidateRefresher === null || candidateRefresher === void 0 ? void 0 : candidateRefresher.stop(); } catch (_c) { // ignore cleanup errors on an unpublished candidate } try { candidateTelemetryClient === null || candidateTelemetryClient === void 0 ? void 0 : candidateTelemetryClient.close(); } catch (_d) { // ignore cleanup errors on an unpublished candidate } if (candidatePool) { yield this.disposeChannelPool(candidatePool); } throw e; } return primaryChanged; }))(); try { return yield this.reconnectingPromise; } finally { this.isReconnecting = false; this.reconnectingPromise = null; } }); } /** * Attaches a failover handler to the channel pool for global cluster support. * When promisify encounters a gRPC UNAVAILABLE error after all retries, * this handler triggers topology refresh and pool rebuild. */ _attachFailoverHandler(pool = this.channelPool) { (0, __1.setPoolFailoverHandler)(pool, () => __awaiter(this, void 0, void 0, function* () { // Trigger topology refresh if (this.topologyRefresher) { this.topologyRefresher.triggerRefresh(); } yield this.reconnectToPrimary(); return this.channelPool; })); } /** * Creates a pool of gRPC service clients. * @returns {Pool} - A pool of gRPC service clients. */ createChannelPool(address = this.config.address) { var _a; const ServiceClientConstructor = this._MilvusService; const formattedAddress = (0, __1.formatAddress)(address); const pool = (0, generic_pool_1.createPool)({ create: () => __awaiter(this, void 0, void 0, function* () { // Create a new gRPC service client return new ServiceClientConstructor(formattedAddress, this.creds, this.channelOptions); }), destroy: (client) => __awaiter(this, void 0, void 0, function* () { // Close the gRPC service client return new Promise((resolve, reject) => { client.close(); resolve(client.getChannel().getConnectivityState(true)); }); }), }, (_a = this.config.pool) !== null && _a !== void 0 ? _a : { min: __1.DEFAULT_POOL_MIN, max: __1.DEFAULT_POOL_MAX, }); (0, __1.setPoolTelemetryManager)(pool, this.telemetry); return pool; } createTelemetryClient(address) { return new this._ClientTelemetryService((0, __1.formatAddress)(address), this.creds, this.telemetryChannelOptions); } replaceTelemetryClient(address = this.config.address) { const replacement = this.createTelemetryClient(address); const previous = this.telemetryClient; this.telemetryEndpointEpoch += 1; this.telemetryClient = replacement; previous === null || previous === void 0 ? void 0 : previous.close(); } disposeChannelPool(pool) { return __awaiter(this, void 0, void 0, function* () { try { yield pool.drain(); yield pool.clear(); } catch (_a) { // Pool cleanup is best-effort. A cleanup failure must neither roll back a // successful publication nor hide the original candidate validation error. } }); } sendTelemetryHeartbeat(request) { return new Promise((resolve, reject) => { if (!this.telemetryClient) { reject(new Error('telemetry client is not connected')); return; } this.telemetryClient.ClientHeartbeat(request, new grpc_js_1.Metadata(), { deadline: new Date(Date.now() + 10000) }, (error, response) => error ? reject(error) : resolve(response)); }); } /** Returns the telemetry manager for inspection and custom command handlers. */ getTelemetry() { return this.telemetry; } /** * Injects client metadata into the metadata of the gRPC client. * @param metadata The metadata object of the gRPC client. * @returns The updated metadata object. */ metadataListener(metadata) { // inject client metadata into the metadata of the grpc client for (var [key, value] of this.metadata) { metadata.add(key, value); } return metadata; } /** * Sets the active database for the gRPC client. * @param data An optional object containing the name of the database to use. * @returns A Promise that resolves with a `ResStatus` object. */ use(data) { return __awaiter(this, void 0, void 0, function* () { return new Promise(resolve => { if (!data || data.db_name === '') { __1.logger.info(`No database name provided, using default database: ${__1.DEFAULT_DB}`); } this.telemetryDatabaseExplicit = Boolean(data === null || data === void 0 ? void 0 : data.db_name); // update database this.metadata.set(__1.METADATA.DATABASE, (data && data.db_name) || __1.DEFAULT_DB); resolve({ error_code: __1.ErrorCode.SUCCESS, reason: '' }); }); }); } /** * Retrieves server information from the Milvus server. * @param {string} sdkVersion - The version of the SDK being used. * @returns {Promise<void>} - A Promise that resolves when the server information has been retrieved. */ _getServerInfo(sdkVersion, pool = this.channelPool, apply = true, lifecycleGeneration = this.lifecycleGeneration) { return __awaiter(this, void 0, void 0, function* () { // build user info const userInfo = { client_info: { sdk_type: 'nodejs', sdk_version: sdkVersion, local_time: (0, dayjs_1.default)().format(`YYYY-MM-DD HH:mm:ss.SSS`), user: this.config.username, reserved: this.config.option || {}, }, }; if (apply) { this.connectStatus = __1.CONNECT_STATUS.CONNECTING; } const response = yield (0, __1.promisify)(pool, 'Connect', userInfo, this.timeout); if (apply && this.isLifecycleCurrent(lifecycleGeneration)) { this.applyServerInfo(response); } return response; }); } applyServerInfo(response, startTelemetry = true) { if (response === null || response === void 0 ? void 0 : response.identifier) { this.metadata.set(__1.METADATA.CLIENT_ID, response.identifier); this.serverInfo = response.server_info; } this.connectStatus = (response === null || response === void 0 ? void 0 : response.identifier) ? __1.CONNECT_STATUS.CONNECTED : __1.CONNECT_STATUS.UNIMPLEMENTED; if ((response === null || response === void 0 ? void 0 : response.identifier) && startTelemetry) { this.telemetry.start(); } } /** * Closes the connection to the Milvus server. * This method drains and clears the connection pool, and updates the connection status to SHUTDOWN. * @returns {Promise<CONNECT_STATUS>} The updated connection status. */ closeConnection() { return __awaiter(this, void 0, void 0, function* () { if (this.closePromise) { return this.closePromise; } // Publish the close fence before touching resources. Any topology/candidate promise // released from this point on observes a different generation and may only clean up. this.closed = true; this.lifecycleGeneration += 1; this.telemetryEndpointEpoch += 1; this.connectStatus = __1.CONNECT_STATUS.SHUTDOWN; const telemetryClient = this.telemetryClient; this.telemetryClient = undefined; const topologyRefresher = this.topologyRefresher; this.topologyRefresher = null; const channelPool = this.channelPool; this.closePromise = (() => __awaiter(this, void 0, void 0, function* () { try { this.telemetry.stop(); } catch (_a) { // best-effort telemetry shutdown must not prevent channel cleanup } try { telemetryClient === null || telemetryClient === void 0 ? void 0 : telemetryClient.close(); } catch (_b) { // best-effort cleanup } if (topologyRefresher) { __1.logger.debug(`\x1b[36m[Global]\x1b[0m Stopping topology refresher on connection close`); try { topologyRefresher.stop(); } catch (_c) { // best-effort cleanup } } if (channelPool) { yield this.disposeChannelPool(channelPool); } return this.connectStatus; }))(); return this.closePromise; }); } isLifecycleCurrent(generation) { return !this.closed && generation === this.lifecycleGeneration; } /** * Returns version information for the Milvus server. * This method returns a Promise that resolves with a `GetVersionResponse` object. */ getVersion() { return __awaiter(this, void 0, void 0, function* () { // wait until connecting finished yield this.connectPromise; return yield (0, __1.promisify)(this.channelPool, 'GetVersion', {}, this.timeout); }); } /** * Checks the health of the Milvus server. * This method returns a Promise that resolves with a `CheckHealthResponse` object. */ checkHealth() { return __awaiter(this, void 0, void 0, function* () { // wait until connecting finished yield this.connectPromise; return yield (0, __1.promisify)(this.channelPool, 'CheckHealth', {}, this.timeout); }); } /** * Runs an analyzer on the provided text. * @param {RunAnalyzerRequest} data - The request object containing analyzer parameters and text. * @returns {Promise<RunAnalyzerResponse>} - A Promise that resolves with the analyzer response. */ runAnalyzer(data) { return __awaiter(this, void 0, void 0, function* () { return (0, __1.withTelemetryLogicalOperation)(this.channelPool, 'RunAnalyzer', data, () => __awaiter(this, void 0, void 0, function* () { return (0, __1.promisify)(this.channelPool, 'RunAnalyzer', { analyzer_params: data.analyzer_params ? JSON.stringify(data.analyzer_params) : '', placeholder: (Array.isArray(data.text) ? data.text : [data.text]).map(d => new TextEncoder().encode(String(d))), with_detail: data.with_detail, with_hash: data.with_hash, db_name: data.db_name, collection_name: data.collection_name, field_name: data.field_name, analyzer_names: data.analyzer_names, }, this.timeout); })); }); } /** * Adds a file resource to Milvus metadata. * @param {AddFileResourceReq} data - The file resource name and server-visible path. * @returns {Promise<ResStatus>} - A Promise that resolves with the operation status. */ addFileResource(data) { return __awaiter(this, void 0, void 0, function* () { return yield (0, __1.promisify)(this.channelPool, 'AddFileResource', { name: data.name, path: data.path, }, data.timeout || this.timeout); }); } /** * Removes a file resource from Milvus metadata. * @param {RemoveFileResourceReq} data - The file resource name. * @returns {Promise<ResStatus>} - A Promise that resolves with the operation status. */ removeFileResource(data) { return __awaiter(this, void 0, void 0, function* () { return yield (0, __1.promisify)(this.channelPool, 'RemoveFileResource', { name: data.name, }, data.timeout || this.timeout); }); } /** * Lists file resources registered in Milvus metadata. * @param {ListFileResourcesReq} [data] - Optional request parameters. * @returns {Promise<ListFileResourcesResponse>} - A Promise that resolves with file resources. */ listFileResources(data = {}) { return __awaiter(this, void 0, void 0, function* () { return yield (0, __1.promisify)(this.channelPool, 'ListFileResources', {}, data.timeout || this.timeout); }); } } exports.GRPCClient = GRPCClient; //# sourceMappingURL=GrpcClient.js.map