Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
383
vendor/ruvector/npm/packages/cloud-run/vector-client.js
vendored
Normal file
383
vendor/ruvector/npm/packages/cloud-run/vector-client.js
vendored
Normal file
@@ -0,0 +1,383 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Vector Client - Optimized ruvector connection layer
|
||||
*
|
||||
* High-performance client with connection pooling, caching, and streaming support.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.VectorClient = void 0;
|
||||
const events_1 = require("events");
|
||||
const lru_cache_1 = require("lru-cache");
|
||||
const api_1 = require("@opentelemetry/api");
|
||||
const prom_client_1 = require("prom-client");
|
||||
// Metrics
|
||||
const metrics = {
|
||||
queryDuration: new prom_client_1.Histogram({
|
||||
name: 'vector_query_duration_seconds',
|
||||
help: 'Vector query duration in seconds',
|
||||
labelNames: ['collection', 'operation', 'cached'],
|
||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2],
|
||||
}),
|
||||
cacheHits: new prom_client_1.Counter({
|
||||
name: 'vector_cache_hits_total',
|
||||
help: 'Total number of cache hits',
|
||||
labelNames: ['collection'],
|
||||
}),
|
||||
cacheMisses: new prom_client_1.Counter({
|
||||
name: 'vector_cache_misses_total',
|
||||
help: 'Total number of cache misses',
|
||||
labelNames: ['collection'],
|
||||
}),
|
||||
poolConnections: new prom_client_1.Gauge({
|
||||
name: 'vector_pool_connections',
|
||||
help: 'Number of connections in the pool',
|
||||
labelNames: ['state'],
|
||||
}),
|
||||
retries: new prom_client_1.Counter({
|
||||
name: 'vector_retries_total',
|
||||
help: 'Total number of retry attempts',
|
||||
labelNames: ['collection', 'reason'],
|
||||
}),
|
||||
};
|
||||
const tracer = api_1.trace.getTracer('vector-client', '1.0.0');
|
||||
// Cache key generation
|
||||
function getCacheKey(collection, query) {
|
||||
const queryStr = JSON.stringify({
|
||||
collection,
|
||||
vector: query.vector?.slice(0, 5), // Use first 5 dimensions for caching
|
||||
filter: query.filter,
|
||||
limit: query.limit,
|
||||
type: query.type,
|
||||
});
|
||||
return Buffer.from(queryStr).toString('base64');
|
||||
}
|
||||
/**
|
||||
* Connection Pool Manager
|
||||
*/
|
||||
class ConnectionPool extends events_1.EventEmitter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.connections = [];
|
||||
this.waitQueue = [];
|
||||
this.cleanupInterval = null;
|
||||
this.initializePool();
|
||||
this.startCleanup();
|
||||
}
|
||||
async initializePool() {
|
||||
for (let i = 0; i < this.config.minConnections; i++) {
|
||||
await this.createConnection();
|
||||
}
|
||||
}
|
||||
async createConnection() {
|
||||
const span = tracer.startSpan('create-connection');
|
||||
try {
|
||||
// TODO: Replace with actual ruvector Node.js binding
|
||||
// const client = await ruvector.connect(this.config.host);
|
||||
const client = {
|
||||
// Mock client for now
|
||||
query: async (collection, params) => {
|
||||
return { results: [] };
|
||||
},
|
||||
close: async () => { },
|
||||
};
|
||||
const connection = {
|
||||
id: `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
client,
|
||||
inUse: false,
|
||||
lastUsed: Date.now(),
|
||||
queryCount: 0,
|
||||
};
|
||||
this.connections.push(connection);
|
||||
metrics.poolConnections.inc({ state: 'idle' });
|
||||
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
||||
return connection;
|
||||
}
|
||||
catch (error) {
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
async acquire() {
|
||||
// Find available connection
|
||||
const available = this.connections.find(conn => !conn.inUse);
|
||||
if (available) {
|
||||
available.inUse = true;
|
||||
available.lastUsed = Date.now();
|
||||
metrics.poolConnections.dec({ state: 'idle' });
|
||||
metrics.poolConnections.inc({ state: 'active' });
|
||||
return available;
|
||||
}
|
||||
// Create new connection if under max
|
||||
if (this.connections.length < this.config.maxConnections) {
|
||||
const newConn = await this.createConnection();
|
||||
newConn.inUse = true;
|
||||
metrics.poolConnections.dec({ state: 'idle' });
|
||||
metrics.poolConnections.inc({ state: 'active' });
|
||||
return newConn;
|
||||
}
|
||||
// Wait for available connection
|
||||
return new Promise((resolve) => {
|
||||
this.waitQueue.push(resolve);
|
||||
});
|
||||
}
|
||||
release(connection) {
|
||||
connection.inUse = false;
|
||||
connection.lastUsed = Date.now();
|
||||
metrics.poolConnections.dec({ state: 'active' });
|
||||
metrics.poolConnections.inc({ state: 'idle' });
|
||||
// Process wait queue
|
||||
const waiter = this.waitQueue.shift();
|
||||
if (waiter) {
|
||||
connection.inUse = true;
|
||||
metrics.poolConnections.dec({ state: 'idle' });
|
||||
metrics.poolConnections.inc({ state: 'active' });
|
||||
waiter(connection);
|
||||
}
|
||||
}
|
||||
startCleanup() {
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const toRemove = [];
|
||||
// Find idle connections to remove
|
||||
for (const conn of this.connections) {
|
||||
if (!conn.inUse &&
|
||||
now - conn.lastUsed > this.config.idleTimeout &&
|
||||
this.connections.length > this.config.minConnections) {
|
||||
toRemove.push(conn);
|
||||
}
|
||||
}
|
||||
// Remove idle connections
|
||||
for (const conn of toRemove) {
|
||||
const index = this.connections.indexOf(conn);
|
||||
if (index > -1) {
|
||||
this.connections.splice(index, 1);
|
||||
conn.client.close();
|
||||
metrics.poolConnections.dec({ state: 'idle' });
|
||||
}
|
||||
}
|
||||
}, 30000); // Run every 30 seconds
|
||||
}
|
||||
async close() {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
}
|
||||
await Promise.all(this.connections.map(async (conn) => {
|
||||
try {
|
||||
await conn.client.close();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error closing connection:', error);
|
||||
}
|
||||
}));
|
||||
this.connections = [];
|
||||
metrics.poolConnections.set({ state: 'idle' }, 0);
|
||||
metrics.poolConnections.set({ state: 'active' }, 0);
|
||||
}
|
||||
getStats() {
|
||||
return {
|
||||
total: this.connections.length,
|
||||
active: this.connections.filter(c => c.inUse).length,
|
||||
idle: this.connections.filter(c => !c.inUse).length,
|
||||
waiting: this.waitQueue.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Vector Client with connection pooling and caching
|
||||
*/
|
||||
class VectorClient {
|
||||
constructor(config) {
|
||||
this.initialized = false;
|
||||
this.config = {
|
||||
host: config.host,
|
||||
maxConnections: config.maxConnections || 100,
|
||||
minConnections: config.minConnections || 10,
|
||||
idleTimeout: config.idleTimeout || 60000,
|
||||
connectionTimeout: config.connectionTimeout || 5000,
|
||||
queryTimeout: config.queryTimeout || 30000,
|
||||
retryAttempts: config.retryAttempts || 3,
|
||||
retryDelay: config.retryDelay || 1000,
|
||||
cacheSize: config.cacheSize || 10000,
|
||||
cacheTTL: config.cacheTTL || 300000, // 5 minutes
|
||||
enableMetrics: config.enableMetrics !== false,
|
||||
};
|
||||
this.pool = new ConnectionPool(this.config);
|
||||
this.cache = new lru_cache_1.LRUCache({
|
||||
max: this.config.cacheSize,
|
||||
ttl: this.config.cacheTTL,
|
||||
updateAgeOnGet: true,
|
||||
updateAgeOnHas: false,
|
||||
});
|
||||
}
|
||||
async initialize() {
|
||||
if (this.initialized)
|
||||
return;
|
||||
const span = tracer.startSpan('initialize-client');
|
||||
try {
|
||||
// Initialize connection pool
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); // Wait for initial connections
|
||||
this.initialized = true;
|
||||
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
||||
console.log('Vector client initialized', { config: this.config });
|
||||
}
|
||||
catch (error) {
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
async query(collection, query) {
|
||||
if (!this.initialized) {
|
||||
throw new Error('Client not initialized');
|
||||
}
|
||||
const cacheKey = getCacheKey(collection, query);
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) {
|
||||
metrics.cacheHits.inc({ collection });
|
||||
return cached;
|
||||
}
|
||||
metrics.cacheMisses.inc({ collection });
|
||||
const span = tracer.startSpan('vector-query', {
|
||||
attributes: { collection, cached: false },
|
||||
});
|
||||
const startTime = Date.now();
|
||||
let connection = null;
|
||||
try {
|
||||
connection = await this.pool.acquire();
|
||||
const result = await this.executeWithRetry(() => connection.client.query(collection, query), collection, 'query');
|
||||
connection.queryCount++;
|
||||
// Cache the result
|
||||
this.cache.set(cacheKey, result);
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
metrics.queryDuration.observe({ collection, operation: 'query', cached: 'false' }, duration);
|
||||
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
if (connection) {
|
||||
this.pool.release(connection);
|
||||
}
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
async streamQuery(collection, query, onChunk) {
|
||||
if (!this.initialized) {
|
||||
throw new Error('Client not initialized');
|
||||
}
|
||||
const span = tracer.startSpan('vector-stream-query', {
|
||||
attributes: { collection },
|
||||
});
|
||||
const startTime = Date.now();
|
||||
let connection = null;
|
||||
try {
|
||||
connection = await this.pool.acquire();
|
||||
// TODO: Replace with actual streaming from ruvector binding
|
||||
// For now, simulate streaming by chunking results
|
||||
const results = await this.executeWithRetry(() => connection.client.query(collection, query), collection, 'stream');
|
||||
// Stream results in chunks
|
||||
const chunkSize = 10;
|
||||
for (let i = 0; i < results.results.length; i += chunkSize) {
|
||||
const chunk = results.results.slice(i, i + chunkSize);
|
||||
for (const item of chunk) {
|
||||
onChunk(item);
|
||||
}
|
||||
// Small delay to simulate streaming
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
connection.queryCount++;
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
metrics.queryDuration.observe({ collection, operation: 'stream', cached: 'false' }, duration);
|
||||
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
||||
}
|
||||
catch (error) {
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
if (connection) {
|
||||
this.pool.release(connection);
|
||||
}
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
async batchQuery(queries) {
|
||||
if (!this.initialized) {
|
||||
throw new Error('Client not initialized');
|
||||
}
|
||||
const span = tracer.startSpan('vector-batch-query', {
|
||||
attributes: { queryCount: queries.length },
|
||||
});
|
||||
try {
|
||||
// Execute queries in parallel with connection pooling
|
||||
const results = await Promise.all(queries.map(q => this.query(q.collection, q)));
|
||||
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
||||
return results;
|
||||
}
|
||||
catch (error) {
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
async executeWithRetry(fn, collection, operation) {
|
||||
let lastError = null;
|
||||
for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
|
||||
try {
|
||||
return await Promise.race([
|
||||
fn(),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Query timeout')), this.config.queryTimeout)),
|
||||
]);
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < this.config.retryAttempts) {
|
||||
metrics.retries.inc({ collection, reason: lastError.message });
|
||||
const delay = this.config.retryDelay * Math.pow(2, attempt); // Exponential backoff
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError || new Error('Unknown error during retry');
|
||||
}
|
||||
async healthCheck() {
|
||||
try {
|
||||
const stats = this.pool.getStats();
|
||||
return stats.total > 0;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async close() {
|
||||
await this.pool.close();
|
||||
this.cache.clear();
|
||||
this.initialized = false;
|
||||
console.log('Vector client closed');
|
||||
}
|
||||
getStats() {
|
||||
return {
|
||||
pool: this.pool.getStats(),
|
||||
cache: {
|
||||
size: this.cache.size,
|
||||
max: this.cache.max,
|
||||
},
|
||||
};
|
||||
}
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
exports.VectorClient = VectorClient;
|
||||
//# sourceMappingURL=vector-client.js.map
|
||||
Reference in New Issue
Block a user