From 7134584f211d8d6da259da39a93ad0398300010e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 20 Nov 2023 21:13:09 -0500 Subject: [PATCH 01/31] proof of concept for using http2 instead of axios --- src/client/httpClient.ts | 43 ++++++++++++++++++++++++-------- tests/collections/client.test.ts | 3 +++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index dbee6a8..3d8c218 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -19,10 +19,11 @@ import { inspect } from 'util'; import { LIB_NAME, LIB_VERSION } from '../version'; import { getStargateAccessToken } from '../collections/utils'; import { EJSON } from 'bson'; +import http2 from 'http2'; const REQUESTED_WITH = LIB_NAME + '/' + LIB_VERSION; const DEFAULT_AUTH_HEADER = 'X-Cassandra-Token'; -const DEFAULT_METHOD = 'get'; +// const DEFAULT_METHOD = 'get'; const DEFAULT_TIMEOUT = 30000; export const AUTH_API_PATH = '/v1/auth'; const HTTP_METHODS = { @@ -88,6 +89,7 @@ axiosAgent.interceptors.request.use(requestInterceptor); axiosAgent.interceptors.response.use(responseInterceptor); export class HTTPClient { + origin: string; baseUrl: string; applicationToken: string; authHeaderName: string; @@ -96,6 +98,7 @@ export class HTTPClient { authUrl: string; isAstra: boolean; logSkippedOptions: boolean; + session: http2.ClientHttp2Session; constructor(options: APIClientOptions) { // do not support usage in browsers @@ -120,6 +123,9 @@ export class HTTPClient { this.applicationToken = '';//We will set this by accessing the auth url when the first request is received } + this.origin = new URL(this.baseUrl).origin; + this.session = http2.connect(this.origin); + if (options.logLevel) { setLevel(options.logLevel); } @@ -156,16 +162,31 @@ export class HTTPClient { ] }; } - const response = await axiosAgent({ - url: requestInfo.url, - data: requestInfo.data, - params: requestInfo.params, - method: requestInfo.method || DEFAULT_METHOD, - timeout: requestInfo.timeout || DEFAULT_TIMEOUT, - headers: { - [this.authHeaderName]: this.applicationToken - } - }); + + const response: { status?: number, data?: Record } = await new Promise((resolve, reject) => { + const path = requestInfo.url?.replace(this.origin, ''); + const req = this.session.request({ + ':path': path, + ':method': 'POST', + token: this.applicationToken + }); + req.write(serializeCommand(requestInfo.data), 'utf8'); + req.end(); + + const response: { status?: number, data?: Record } = {}; + req.on('response', data => { + response.status = data[':status']; + }); + + req.setEncoding('utf8'); + let data = ''; + req.on('data', (chunk) => { data += chunk; }); + req.on('end', () => { + response.data = JSON.parse(data); + resolve(response); + }); + }); + if (response.status === 401 || (response.data?.errors?.length > 0 && response.data.errors[0]?.message === 'UNAUTHENTICATED: Invalid token')) { logger.debug('@stargate-mongoose/rest: reconnecting'); try { diff --git a/tests/collections/client.test.ts b/tests/collections/client.test.ts index 1f56a5e..6500473 100644 --- a/tests/collections/client.test.ts +++ b/tests/collections/client.test.ts @@ -25,6 +25,9 @@ describe('StargateMongoose clients test', () => { let appClient: Client | null; let clientURI: string; before(async function () { + if (process.env.TEST_DOC_DB === 'jsonapi') { + return this.skip(); + } if (testClient == null) { return this.skip(); } From f59b1c214fe5d0a764671c05aa158636c4bbb254 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 20 Nov 2023 21:14:32 -0500 Subject: [PATCH 02/31] fix TS build issues --- src/client/httpClient.ts | 8 ++++---- src/version.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index 3d8c218..f709991 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -187,7 +187,7 @@ export class HTTPClient { }); }); - if (response.status === 401 || (response.data?.errors?.length > 0 && response.data.errors[0]?.message === 'UNAUTHENTICATED: Invalid token')) { + if (response.status === 401 || (response.data?.errors?.length > 0 && response.data?.errors?.[0]?.message === 'UNAUTHENTICATED: Invalid token')) { logger.debug('@stargate-mongoose/rest: reconnecting'); try { this.applicationToken = await getStargateAccessToken(this.authUrl, this.username, this.password); @@ -204,9 +204,9 @@ export class HTTPClient { } if (response.status === 200) { return { - status: response.data.status, - data: deserialize(response.data.data), - errors: response.data.errors + status: response.data?.status, + data: deserialize(response.data?.data), + errors: response.data?.errors }; } else { logger.error(requestInfo.url + ': ' + response.status); diff --git a/src/version.ts b/src/version.ts index 8e5cb17..1cf208a 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const LIB_NAME = 'stargate-mongoose'; -export const LIB_VERSION = '0.3.0'; +export const LIB_NAME = "stargate-mongoose"; +export const LIB_VERSION = "0.3.0"; From e30f026de5c3cf6cc1c66e4528cf7743f482782f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Nov 2023 13:17:34 -0500 Subject: [PATCH 03/31] error handling improvements --- src/client/httpClient.ts | 17 ++++++++++++++++- src/version.ts | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index f709991..fc98b70 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -126,6 +126,12 @@ export class HTTPClient { this.origin = new URL(this.baseUrl).origin; this.session = http2.connect(this.origin); + // Without these handlers, any errors will end up as uncaught exceptions, + // even if they are handled in `_request()`. + // More info: https://github.com/nodejs/node/issues/16345 + this.session.on('error', () => {}); + this.session.on('socketError', () => {}); + if (options.logLevel) { setLevel(options.logLevel); } @@ -178,11 +184,20 @@ export class HTTPClient { response.status = data[':status']; }); + req.on('error', error => { + reject(error); + }); + req.setEncoding('utf8'); let data = ''; req.on('data', (chunk) => { data += chunk; }); req.on('end', () => { - response.data = JSON.parse(data); + try { + response.data = JSON.parse(data); + } catch (error) { + reject(new Error('Unable to parse response as JSON, got: "' + data + '"')); + return; + } resolve(response); }); }); diff --git a/src/version.ts b/src/version.ts index 1cf208a..8e5cb17 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const LIB_NAME = "stargate-mongoose"; -export const LIB_VERSION = "0.3.0"; +export const LIB_NAME = 'stargate-mongoose'; +export const LIB_VERSION = '0.3.0'; From 211060f6409f0c35d765656d62c544c8049b6555 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Nov 2023 14:37:53 -0500 Subject: [PATCH 04/31] clean up a bunch of dangling references --- src/client/httpClient.ts | 4 ++++ src/collections/client.ts | 20 ++++++++++++++++++-- src/collections/db.ts | 17 ++++++++++++++++- src/driver/connection.ts | 3 +++ tests/driver/api.test.ts | 4 ++++ tests/driver/index.test.ts | 5 +++++ tests/fixtures.ts | 5 ++++- 7 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index fc98b70..7e3f8d3 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -143,6 +143,10 @@ export class HTTPClient { this.logSkippedOptions = options.logSkippedOptions || false; } + close() { + this.session.close(); + } + async _request(requestInfo: AxiosRequestConfig): Promise { try { if (this.applicationToken === '') { diff --git a/src/collections/client.ts b/src/collections/client.ts index c2a903c..eb47116 100644 --- a/src/collections/client.ts +++ b/src/collections/client.ts @@ -35,6 +35,7 @@ export class Client { httpClient: HTTPClient; keyspaceName?: string; createNamespaceOnConnect?: boolean; + dbs: Map; constructor(baseUrl: string, keyspaceName: string, options: ClientOptions) { this.keyspaceName = keyspaceName; @@ -55,6 +56,7 @@ export class Client { isAstra: options.isAstra, logSkippedOptions: options.logSkippedOptions }); + this.dbs = new Map(); } /** @@ -101,10 +103,20 @@ export class Client { */ db(dbName?: string) { if (dbName) { - return new Db(this.httpClient, dbName); + if (this.dbs.has(dbName)) { + return this.dbs.get(dbName); + } + const db = new Db(this.httpClient, dbName); + this.dbs.set(dbName, db); + return db; } if (this.keyspaceName) { - return new Db(this.httpClient, this.keyspaceName); + if (this.dbs.has(this.keyspaceName)) { + return this.dbs.get(this.keyspaceName); + } + const db = new Db(this.httpClient, this.keyspaceName); + this.dbs.set(this.keyspaceName, db); + return db; } throw new Error('Database name must be provided'); } @@ -123,6 +135,10 @@ export class Client { * @returns Client */ close() { + this.httpClient.close(); + for (const db of this.dbs.values()) { + db.close(); + } return this; } diff --git a/src/collections/db.ts b/src/collections/db.ts index c0a2c25..ba75bd1 100644 --- a/src/collections/db.ts +++ b/src/collections/db.ts @@ -21,6 +21,7 @@ export class Db { rootHttpClient: HTTPClient; httpClient: HTTPClient; name: string; + collections: Map; constructor(httpClient: HTTPClient, name: string) { if (!name) { @@ -39,6 +40,7 @@ export class Db { logSkippedOptions: httpClient.logSkippedOptions, }); this.name = name; + this.collections = new Map(); } /** @@ -50,7 +52,13 @@ export class Db { if (!collectionName) { throw new Error('Db: collection name is required'); } - return new Collection(this.httpClient, collectionName); + const collection = this.collections.get(collectionName); + if (collection != null) { + return collection; + } + const newCollection = new Collection(this.httpClient, collectionName); + this.collections.set(collectionName, newCollection); + return newCollection; } /** @@ -111,6 +119,13 @@ export class Db { async createDatabase() { return await createNamespace(this.rootHttpClient, this.name); } + + close() { + for (const collection of this.collections.values()) { + collection.httpClient.close(); + } + this.httpClient.close(); + } } export class StargateAstraError extends Error { diff --git a/src/driver/connection.ts b/src/driver/connection.ts index 3346b89..111d0ae 100644 --- a/src/driver/connection.ts +++ b/src/driver/connection.ts @@ -141,6 +141,9 @@ export class Connection extends MongooseConnection { * @returns Client */ doClose(_force?: boolean) { + if (this.client != null) { + this.client.close(); + } return this; } } \ No newline at end of file diff --git a/tests/driver/api.test.ts b/tests/driver/api.test.ts index 9239ef7..0391f28 100644 --- a/tests/driver/api.test.ts +++ b/tests/driver/api.test.ts @@ -53,6 +53,10 @@ describe('Mongoose Model API level tests', async () => { dbUri = testClient.uri; isAstra = testClient.isAstra; }); + after(function() { + jsonAPIMongoose?.connection?.getClient()?.close(); + astraMongoose?.connection?.getClient()?.close(); + }); let mongooseInstance: Mongoose | null = null; let Product: Model, Cart: Model, astraMongoose: Mongoose | null, jsonAPIMongoose: Mongoose | null; beforeEach(async () => { diff --git a/tests/driver/index.test.ts b/tests/driver/index.test.ts index 641b575..e038a56 100644 --- a/tests/driver/index.test.ts +++ b/tests/driver/index.test.ts @@ -126,9 +126,11 @@ describe('Driver based tests', async () => { if (isAstra) { astraMongoose?.connection.dropCollection('carts'); astraMongoose?.connection.dropCollection('products'); + astraMongoose?.connection?.getClient()?.close(); } else { jsonAPIMongoose?.connection.dropCollection('carts'); jsonAPIMongoose?.connection.dropCollection('products'); + jsonAPIMongoose?.connection?.getClient()?.close(); } } }); @@ -146,6 +148,7 @@ describe('Driver based tests', async () => { await mongooseInstance?.connection.dropCollection('grandchildren'); await mongooseInstance?.connection.dropCollection('carts'); await mongooseInstance?.connection.dropCollection('products'); + mongooseInstance?.connection?.getClient()?.close(); }); it('handles find cursors', async () => { // @ts-ignore @@ -390,6 +393,7 @@ describe('Driver based tests', async () => { const resp = await connection.dropDatabase(); assert.strictEqual(resp.status?.ok, 1); } + mongooseInstance.connection.getClient().close(); }); it('should createDatabase if not exists in createCollection call for non-AstraDB', async () => { const mongooseInstance = new mongoose.Mongoose(); @@ -420,6 +424,7 @@ describe('Driver based tests', async () => { const resp = await connection.createCollection('new_collection'); assert.strictEqual(resp.status?.ok, 1); } + mongooseInstance.connection.getClient().close(); }); }); }); diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 3a8bf42..e0e01ea 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -144,4 +144,7 @@ export const testClient = process.env.TEST_DOC_DB === 'astra' ? } : null ) : null); - +after(async function() { + const client = await testClient?.client; + client?.close(); +}); From a9d58f897e4cee436689303652a1aa66cda6c115 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 26 Nov 2023 13:20:55 -0500 Subject: [PATCH 05/31] clean up all dangling http2 sessions in tests so test process exits after tests pass --- tests/collections/collection.test.ts | 5 ++++- tests/collections/db.test.ts | 11 ++++++++++- tests/collections/options.test.ts | 5 +++++ tests/driver/api.test.ts | 11 +++++++---- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/tests/collections/collection.test.ts b/tests/collections/collection.test.ts index 4e90c29..056539b 100644 --- a/tests/collections/collection.test.ts +++ b/tests/collections/collection.test.ts @@ -49,17 +49,20 @@ describe(`StargateMongoose - ${testClientName} Connection - collections.collecti it('should initialize a Collection', () => { const collection = new Collection(db.httpClient, 'new_collection'); assert.ok(collection); + collection.httpClient.close(); }); it('should not initialize a Collection without a name', () => { let error: any; + let collection: Collection | null = null; try { // @ts-ignore: Testing invalid input - const collection = new Collection(db.httpClient); + collection = new Collection(db.httpClient); assert.ok(collection); } catch (e) { error = e; } assert.ok(error); + collection?.httpClient?.close(); }); }); diff --git a/tests/collections/db.test.ts b/tests/collections/db.test.ts index 86fb56b..68daa51 100644 --- a/tests/collections/db.test.ts +++ b/tests/collections/db.test.ts @@ -47,6 +47,7 @@ describe('StargateMongoose - collections.Db', async () => { it('should initialize a Db', () => { const db = new Db(httpClient, 'test-db'); assert.ok(db); + db.close(); }); it('should not initialize a Db without a name', () => { let error: any; @@ -66,11 +67,13 @@ describe('StargateMongoose - collections.Db', async () => { const db = new Db(httpClient, 'test-db'); const collection = db.collection('test-collection'); assert.ok(collection); + db.close(); }); it('should not initialize a Collection without a name', () => { let error: any; + let db: Db | null = null; try { - const db = new Db(httpClient, 'test-db'); + db = new Db(httpClient, 'test-db'); // @ts-ignore - intentionally passing undefined for testing purposes const collection = db.collection(); assert.ok(collection); @@ -78,6 +81,7 @@ describe('StargateMongoose - collections.Db', async () => { error = e; } assert.ok(error); + db?.close(); }); it('should create a Collection', async () => { const collectionName = TEST_COLLECTION_NAME; @@ -88,6 +92,7 @@ describe('StargateMongoose - collections.Db', async () => { const res2 = await db.createCollection(collectionName); assert.ok(res2); assert.strictEqual(res2.status.ok, 1); + db.close(); }); it('should drop a Collection', async () => { @@ -97,6 +102,7 @@ describe('StargateMongoose - collections.Db', async () => { const res = await db.dropCollection(`test_db_collection_${suffix}`); assert.strictEqual(res.status?.ok, 1); assert.strictEqual(res.errors, undefined); + db.close(); }); }); @@ -130,6 +136,8 @@ describe('StargateMongoose - collections.Db', async () => { 'INVALID_ARGUMENT: Unknown namespace \'' + keyspaceName + '\', you must create it first.' ); } + + db.close(); }); }); @@ -174,6 +182,7 @@ describe('StargateMongoose - collections.Db', async () => { assert.strictEqual(res.status?.ok, 1); await db.createCollection(`test_db_collection_${suffix}`); + db.close(); }); }); }); \ No newline at end of file diff --git a/tests/collections/options.test.ts b/tests/collections/options.test.ts index 5f52939..e55f736 100644 --- a/tests/collections/options.test.ts +++ b/tests/collections/options.test.ts @@ -48,6 +48,11 @@ describe('Options tests', async () => { await dropCollections(isAstra, astraMongoose, jsonAPIMongoose, 'products'); }); + afterEach(function() { + jsonAPIMongoose?.connection?.getClient()?.close(); + astraMongoose?.connection?.getClient()?.close(); + }); + async function createClientsAndModels(isAstra: boolean) { let Product, astraMongoose, jsonAPIMongoose; const productSchema = new mongoose.Schema({ diff --git a/tests/driver/api.test.ts b/tests/driver/api.test.ts index 0391f28..32e0a52 100644 --- a/tests/driver/api.test.ts +++ b/tests/driver/api.test.ts @@ -53,10 +53,6 @@ describe('Mongoose Model API level tests', async () => { dbUri = testClient.uri; isAstra = testClient.isAstra; }); - after(function() { - jsonAPIMongoose?.connection?.getClient()?.close(); - astraMongoose?.connection?.getClient()?.close(); - }); let mongooseInstance: Mongoose | null = null; let Product: Model, Cart: Model, astraMongoose: Mongoose | null, jsonAPIMongoose: Mongoose | null; beforeEach(async () => { @@ -66,6 +62,10 @@ describe('Mongoose Model API level tests', async () => { await dropCollections(isAstra, astraMongoose, jsonAPIMongoose, 'products'); await dropCollections(isAstra, astraMongoose, jsonAPIMongoose, 'carts'); }); + afterEach(function() { + jsonAPIMongoose?.connection?.getClient()?.close(); + astraMongoose?.connection?.getClient()?.close(); + }); function getInstance() { const mongooseInstance = new mongoose.Mongoose(); @@ -801,6 +801,9 @@ describe('Mongoose Model API level tests', async () => { } }); + after(function() { + mongooseInstance.connection.getClient().close(); + }); beforeEach(async function() { await mongooseInstance!.connection.dropCollection('vector'); From edb27ffda96afe531664361971816ce2ac02d077 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 1 Dec 2023 16:59:59 -0500 Subject: [PATCH 06/31] make collections reuse provided http client --- src/client/httpClient.ts | 12 +++++ src/collections/collection.ts | 79 ++++++++++++++++++++-------- src/collections/cursor.ts | 6 ++- src/version.ts | 4 +- tests/collections/collection.test.ts | 1 - 5 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index 7e3f8d3..0269e01 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -265,6 +265,18 @@ export class HTTPClient { handleIfErrorResponse(response, data); return response; } + + async executeCommandWithUrl(url: string, data: Record, optionsToRetain: Set | null) { + const commandName = Object.keys(data)[0]; + cleanupOptions(commandName, data[commandName], optionsToRetain, this.logSkippedOptions); + const response = await this._request({ + url: this.baseUrl + url, + method: HTTP_METHODS.post, + data + }); + handleIfErrorResponse(response, data); + return response; + } } export class StargateServerError extends Error { diff --git a/src/collections/collection.ts b/src/collections/collection.ts index a977d6a..973ef93 100644 --- a/src/collections/collection.ts +++ b/src/collections/collection.ts @@ -102,6 +102,7 @@ export type UpdateOneCommand = { export class Collection { httpClient: any; name: string; + httpBasePath: string; collectionName: string; constructor(httpClient: HTTPClient, name: string) { @@ -109,18 +110,10 @@ export class Collection { throw new Error('Collection name is required'); } // use a clone of the underlying http client to support multiple collections from a single db - this.httpClient = new HTTPClient({ - baseUrl: httpClient.baseUrl + `/${name}`, - username: httpClient.username, - password: httpClient.password, - authUrl: httpClient.authUrl, - applicationToken: httpClient.applicationToken, - authHeaderName: httpClient.authHeaderName, - isAstra: httpClient.isAstra, - logSkippedOptions: httpClient.logSkippedOptions - }); + this.httpClient = httpClient; this.name = name; this.collectionName = name; + this.httpBasePath = `/${name}`; } async insertOne(document: Record) { @@ -130,7 +123,11 @@ export class Collection { document } }; - const resp = await this.httpClient.executeCommand(command, null); + const resp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); return { acknowledged: true, insertedId: resp.status.insertedIds[0] @@ -146,7 +143,11 @@ export class Collection { options } }; - const resp = await this.httpClient.executeCommand(command, insertManyInternalOptionsKeys); + const resp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + insertManyInternalOptionsKeys + ); return { acknowledged: true, insertedCount: resp.status.insertedIds?.length || 0, @@ -168,7 +169,11 @@ export class Collection { command.updateOne.sort = options?.sort; } setDefaultIdForUpsert(command.updateOne); - const updateOneResp = await this.httpClient.executeCommand(command, updateOneInternalOptionsKeys); + const updateOneResp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + updateOneInternalOptionsKeys + ); const resp = { modifiedCount: updateOneResp.status.modifiedCount, matchedCount: updateOneResp.status.matchedCount, @@ -192,7 +197,11 @@ export class Collection { } }; setDefaultIdForUpsert(command.updateMany); - const updateManyResp = await this.httpClient.executeCommand(command, updateManyInternalOptionsKeys); + const updateManyResp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + updateManyInternalOptionsKeys + ); if (updateManyResp.status.moreData) { throw new StargateMongooseError(`More than ${updateManyResp.status.modifiedCount} records found for update by the server`, command); } @@ -219,7 +228,11 @@ export class Collection { if (options?.sort) { command.deleteOne.sort = options.sort; } - const deleteOneResp = await this.httpClient.executeCommand(command, null); + const deleteOneResp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); return { acknowledged: true, deletedCount: deleteOneResp.status.deletedCount @@ -234,7 +247,11 @@ export class Collection { filter } }; - const deleteManyResp = await this.httpClient.executeCommand(command, null); + const deleteManyResp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); if (deleteManyResp.status.moreData) { throw new StargateMongooseError(`More records found to be deleted even after deleting ${deleteManyResp.status.deletedCount} records`, command); } @@ -267,7 +284,11 @@ export class Collection { command.findOne.projection = options.projection; } - const resp = await this.httpClient.executeCommand(command, findOneInternalOptionsKeys); + const resp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + findOneInternalOptionsKeys + ); return resp.data.document; }); } @@ -296,7 +317,11 @@ export class Collection { delete options.sort; } } - const resp = await this.httpClient.executeCommand(command, findOneAndReplaceInternalOptionsKeys); + const resp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + findOneAndReplaceInternalOptionsKeys + ); return { value : resp.data?.document, ok : 1 @@ -315,7 +340,11 @@ export class Collection { filter } }; - const resp = await this.httpClient.executeCommand(command, null); + const resp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); return resp.status.count; }); } @@ -330,7 +359,11 @@ export class Collection { command.findOneAndDelete.sort = options.sort; } - const resp = await this.httpClient.executeCommand(command, null); + const resp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); return { value : resp.data?.document, ok : 1 @@ -358,7 +391,11 @@ export class Collection { command.findOneAndUpdate.sort = options.sort; delete options.sort; } - const resp = await this.httpClient.executeCommand(command, findOneAndUpdateInternalOptionsKeys); + const resp = await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + findOneAndUpdateInternalOptionsKeys + ); return { value : resp.data?.document, ok : 1 diff --git a/src/collections/cursor.ts b/src/collections/cursor.ts index 032435f..3b2e3ac 100644 --- a/src/collections/cursor.ts +++ b/src/collections/cursor.ts @@ -134,7 +134,11 @@ export class FindCursor { if (Object.keys(options).length > 0) { command.find.options = options; } - const resp = await this.collection.httpClient.executeCommand(command, findInternalOptionsKeys); + const resp = await this.collection.httpClient.executeCommandWithUrl( + this.collection.httpBasePath, + command, + findInternalOptionsKeys + ); this.nextPageState = resp.data.nextPageState; if (this.nextPageState == null) { this.exhausted = true; diff --git a/src/version.ts b/src/version.ts index 8e5cb17..1cf208a 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const LIB_NAME = 'stargate-mongoose'; -export const LIB_VERSION = '0.3.0'; +export const LIB_NAME = "stargate-mongoose"; +export const LIB_VERSION = "0.3.0"; diff --git a/tests/collections/collection.test.ts b/tests/collections/collection.test.ts index 056539b..abf06ff 100644 --- a/tests/collections/collection.test.ts +++ b/tests/collections/collection.test.ts @@ -49,7 +49,6 @@ describe(`StargateMongoose - ${testClientName} Connection - collections.collecti it('should initialize a Collection', () => { const collection = new Collection(db.httpClient, 'new_collection'); assert.ok(collection); - collection.httpClient.close(); }); it('should not initialize a Collection without a name', () => { let error: any; From df4ae56dfc31f4ff17e3d2f9109a8a2bb99973bd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 1 Dec 2023 18:12:38 -0500 Subject: [PATCH 07/31] reuse same HTTP client for all databases as well as collections --- src/client/httpClient.ts | 22 ++++----- src/collections/client.ts | 7 +-- src/collections/collection.ts | 74 ++++++++++++++-------------- src/collections/cursor.ts | 6 +-- src/collections/db.ts | 62 +++++++++++------------ src/driver/connection.ts | 2 +- src/version.ts | 4 +- tests/collections/collection.test.ts | 4 +- tests/collections/db.test.ts | 8 --- tests/driver/api.test.ts | 8 +-- 10 files changed, 90 insertions(+), 107 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index 0269e01..5f22d9c 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -101,7 +101,7 @@ export class HTTPClient { session: http2.ClientHttp2Session; constructor(options: APIClientOptions) { - // do not support usage in browsers + // do not support usage in browsers if (typeof window !== 'undefined') { throw new Error('not for use in a web browser'); } @@ -267,16 +267,16 @@ export class HTTPClient { } async executeCommandWithUrl(url: string, data: Record, optionsToRetain: Set | null) { - const commandName = Object.keys(data)[0]; - cleanupOptions(commandName, data[commandName], optionsToRetain, this.logSkippedOptions); - const response = await this._request({ - url: this.baseUrl + url, - method: HTTP_METHODS.post, - data - }); - handleIfErrorResponse(response, data); - return response; - } + const commandName = Object.keys(data)[0]; + cleanupOptions(commandName, data[commandName], optionsToRetain, this.logSkippedOptions); + const response = await this._request({ + url: this.baseUrl + url, + method: HTTP_METHODS.post, + data + }); + handleIfErrorResponse(response, data); + return response; + } } export class StargateServerError extends Error { diff --git a/src/collections/client.ts b/src/collections/client.ts index eb47116..e9323bd 100644 --- a/src/collections/client.ts +++ b/src/collections/client.ts @@ -104,7 +104,7 @@ export class Client { db(dbName?: string) { if (dbName) { if (this.dbs.has(dbName)) { - return this.dbs.get(dbName); + return this.dbs.get(dbName); } const db = new Db(this.httpClient, dbName); this.dbs.set(dbName, db); @@ -112,7 +112,7 @@ export class Client { } if (this.keyspaceName) { if (this.dbs.has(this.keyspaceName)) { - return this.dbs.get(this.keyspaceName); + return this.dbs.get(this.keyspaceName); } const db = new Db(this.httpClient, this.keyspaceName); this.dbs.set(this.keyspaceName, db); @@ -136,9 +136,6 @@ export class Client { */ close() { this.httpClient.close(); - for (const db of this.dbs.values()) { - db.close(); - } return this; } diff --git a/src/collections/collection.ts b/src/collections/collection.ts index 973ef93..66dddbe 100644 --- a/src/collections/collection.ts +++ b/src/collections/collection.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import {Db} from './db'; import {FindCursor} from './cursor'; -import {HTTPClient} from '@/src/client'; import {executeOperation, setDefaultIdForUpsert} from './utils'; import {InsertManyResult} from 'mongoose'; import { @@ -105,15 +105,15 @@ export class Collection { httpBasePath: string; collectionName: string; - constructor(httpClient: HTTPClient, name: string) { + constructor(db: Db, name: string) { if (!name) { throw new Error('Collection name is required'); } // use a clone of the underlying http client to support multiple collections from a single db - this.httpClient = httpClient; + this.httpClient = db.httpClient; this.name = name; this.collectionName = name; - this.httpBasePath = `/${name}`; + this.httpBasePath = `/${db.name}/${name}`; } async insertOne(document: Record) { @@ -124,9 +124,9 @@ export class Collection { } }; const resp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - null + this.httpBasePath, + command, + null ); return { acknowledged: true, @@ -144,9 +144,9 @@ export class Collection { } }; const resp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - insertManyInternalOptionsKeys + this.httpBasePath, + command, + insertManyInternalOptionsKeys ); return { acknowledged: true, @@ -170,9 +170,9 @@ export class Collection { } setDefaultIdForUpsert(command.updateOne); const updateOneResp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - updateOneInternalOptionsKeys + this.httpBasePath, + command, + updateOneInternalOptionsKeys ); const resp = { modifiedCount: updateOneResp.status.modifiedCount, @@ -198,9 +198,9 @@ export class Collection { }; setDefaultIdForUpsert(command.updateMany); const updateManyResp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - updateManyInternalOptionsKeys + this.httpBasePath, + command, + updateManyInternalOptionsKeys ); if (updateManyResp.status.moreData) { throw new StargateMongooseError(`More than ${updateManyResp.status.modifiedCount} records found for update by the server`, command); @@ -229,9 +229,9 @@ export class Collection { command.deleteOne.sort = options.sort; } const deleteOneResp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - null + this.httpBasePath, + command, + null ); return { acknowledged: true, @@ -248,9 +248,9 @@ export class Collection { } }; const deleteManyResp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - null + this.httpBasePath, + command, + null ); if (deleteManyResp.status.moreData) { throw new StargateMongooseError(`More records found to be deleted even after deleting ${deleteManyResp.status.deletedCount} records`, command); @@ -285,9 +285,9 @@ export class Collection { } const resp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - findOneInternalOptionsKeys + this.httpBasePath, + command, + findOneInternalOptionsKeys ); return resp.data.document; }); @@ -318,9 +318,9 @@ export class Collection { } } const resp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - findOneAndReplaceInternalOptionsKeys + this.httpBasePath, + command, + findOneAndReplaceInternalOptionsKeys ); return { value : resp.data?.document, @@ -341,9 +341,9 @@ export class Collection { } }; const resp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - null + this.httpBasePath, + command, + null ); return resp.status.count; }); @@ -360,9 +360,9 @@ export class Collection { } const resp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - null + this.httpBasePath, + command, + null ); return { value : resp.data?.document, @@ -392,9 +392,9 @@ export class Collection { delete options.sort; } const resp = await this.httpClient.executeCommandWithUrl( - this.httpBasePath, - command, - findOneAndUpdateInternalOptionsKeys + this.httpBasePath, + command, + findOneAndUpdateInternalOptionsKeys ); return { value : resp.data?.document, diff --git a/src/collections/cursor.ts b/src/collections/cursor.ts index 3b2e3ac..894a07e 100644 --- a/src/collections/cursor.ts +++ b/src/collections/cursor.ts @@ -135,9 +135,9 @@ export class FindCursor { command.find.options = options; } const resp = await this.collection.httpClient.executeCommandWithUrl( - this.collection.httpBasePath, - command, - findInternalOptionsKeys + this.collection.httpBasePath, + command, + findInternalOptionsKeys ); this.nextPageState = resp.data.nextPageState; if (this.nextPageState == null) { diff --git a/src/collections/db.ts b/src/collections/db.ts index ba75bd1..d316fe1 100644 --- a/src/collections/db.ts +++ b/src/collections/db.ts @@ -22,6 +22,7 @@ export class Db { httpClient: HTTPClient; name: string; collections: Map; + httpBasePath: string; constructor(httpClient: HTTPClient, name: string) { if (!name) { @@ -29,18 +30,10 @@ export class Db { } this.rootHttpClient = httpClient; // use a clone of the underlying http client to support multiple db's from a single connection - this.httpClient = new HTTPClient({ - baseUrl: httpClient.baseUrl + `/${name}`, - username: httpClient.username, - password: httpClient.password, - authUrl: httpClient.authUrl, - applicationToken: httpClient.applicationToken, - authHeaderName: httpClient.authHeaderName, - isAstra: httpClient.isAstra, - logSkippedOptions: httpClient.logSkippedOptions, - }); + this.httpClient = httpClient; this.name = name; this.collections = new Map(); + this.httpBasePath = `/${name}`; } /** @@ -56,7 +49,7 @@ export class Db { if (collection != null) { return collection; } - const newCollection = new Collection(this.httpClient, collectionName); + const newCollection = new Collection(this, collectionName); this.collections.set(collectionName, newCollection); return newCollection; } @@ -69,21 +62,25 @@ export class Db { */ async createCollection(collectionName: string, options?: CreateCollectionOptions) { return executeOperation(async () => { - type CreateCollectionCommand = { - createCollection: { - name: string, - options?: CreateCollectionOptions - } - }; - const command: CreateCollectionCommand = { - createCollection: { - name: collectionName - } - }; - if (options != null) { - command.createCollection.options = options; - } - return await this.httpClient.executeCommand(command, createCollectionOptionsKeys); + type CreateCollectionCommand = { + createCollection: { + name: string, + options?: CreateCollectionOptions + } + }; + const command: CreateCollectionCommand = { + createCollection: { + name: collectionName + } + }; + if (options != null) { + command.createCollection.options = options; + } + return await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + createCollectionOptionsKeys + ); }); } @@ -98,7 +95,11 @@ export class Db { name: collectionName } }; - return await this.httpClient.executeCommand(command, null); + return await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); } /** @@ -119,13 +120,6 @@ export class Db { async createDatabase() { return await createNamespace(this.rootHttpClient, this.name); } - - close() { - for (const collection of this.collections.values()) { - collection.httpClient.close(); - } - this.httpClient.close(); - } } export class StargateAstraError extends Error { diff --git a/src/driver/connection.ts b/src/driver/connection.ts index 111d0ae..edb5736 100644 --- a/src/driver/connection.ts +++ b/src/driver/connection.ts @@ -142,7 +142,7 @@ export class Connection extends MongooseConnection { */ doClose(_force?: boolean) { if (this.client != null) { - this.client.close(); + this.client.close(); } return this; } diff --git a/src/version.ts b/src/version.ts index 1cf208a..8e5cb17 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const LIB_NAME = "stargate-mongoose"; -export const LIB_VERSION = "0.3.0"; +export const LIB_NAME = 'stargate-mongoose'; +export const LIB_VERSION = '0.3.0'; diff --git a/tests/collections/collection.test.ts b/tests/collections/collection.test.ts index abf06ff..5049ca1 100644 --- a/tests/collections/collection.test.ts +++ b/tests/collections/collection.test.ts @@ -47,7 +47,7 @@ describe(`StargateMongoose - ${testClientName} Connection - collections.collecti describe('Collection initialization', () => { it('should initialize a Collection', () => { - const collection = new Collection(db.httpClient, 'new_collection'); + const collection = new Collection(db, 'new_collection'); assert.ok(collection); }); it('should not initialize a Collection without a name', () => { @@ -55,7 +55,7 @@ describe(`StargateMongoose - ${testClientName} Connection - collections.collecti let collection: Collection | null = null; try { // @ts-ignore: Testing invalid input - collection = new Collection(db.httpClient); + collection = new Collection(db); assert.ok(collection); } catch (e) { error = e; diff --git a/tests/collections/db.test.ts b/tests/collections/db.test.ts index 68daa51..c93d2b0 100644 --- a/tests/collections/db.test.ts +++ b/tests/collections/db.test.ts @@ -47,7 +47,6 @@ describe('StargateMongoose - collections.Db', async () => { it('should initialize a Db', () => { const db = new Db(httpClient, 'test-db'); assert.ok(db); - db.close(); }); it('should not initialize a Db without a name', () => { let error: any; @@ -67,7 +66,6 @@ describe('StargateMongoose - collections.Db', async () => { const db = new Db(httpClient, 'test-db'); const collection = db.collection('test-collection'); assert.ok(collection); - db.close(); }); it('should not initialize a Collection without a name', () => { let error: any; @@ -81,7 +79,6 @@ describe('StargateMongoose - collections.Db', async () => { error = e; } assert.ok(error); - db?.close(); }); it('should create a Collection', async () => { const collectionName = TEST_COLLECTION_NAME; @@ -92,7 +89,6 @@ describe('StargateMongoose - collections.Db', async () => { const res2 = await db.createCollection(collectionName); assert.ok(res2); assert.strictEqual(res2.status.ok, 1); - db.close(); }); it('should drop a Collection', async () => { @@ -102,7 +98,6 @@ describe('StargateMongoose - collections.Db', async () => { const res = await db.dropCollection(`test_db_collection_${suffix}`); assert.strictEqual(res.status?.ok, 1); assert.strictEqual(res.errors, undefined); - db.close(); }); }); @@ -136,8 +131,6 @@ describe('StargateMongoose - collections.Db', async () => { 'INVALID_ARGUMENT: Unknown namespace \'' + keyspaceName + '\', you must create it first.' ); } - - db.close(); }); }); @@ -182,7 +175,6 @@ describe('StargateMongoose - collections.Db', async () => { assert.strictEqual(res.status?.ok, 1); await db.createCollection(`test_db_collection_${suffix}`); - db.close(); }); }); }); \ No newline at end of file diff --git a/tests/driver/api.test.ts b/tests/driver/api.test.ts index 32e0a52..1408d4d 100644 --- a/tests/driver/api.test.ts +++ b/tests/driver/api.test.ts @@ -63,9 +63,9 @@ describe('Mongoose Model API level tests', async () => { await dropCollections(isAstra, astraMongoose, jsonAPIMongoose, 'carts'); }); afterEach(function() { - jsonAPIMongoose?.connection?.getClient()?.close(); - astraMongoose?.connection?.getClient()?.close(); - }); + jsonAPIMongoose?.connection?.getClient()?.close(); + astraMongoose?.connection?.getClient()?.close(); + }); function getInstance() { const mongooseInstance = new mongoose.Mongoose(); @@ -802,7 +802,7 @@ describe('Mongoose Model API level tests', async () => { }); after(function() { - mongooseInstance.connection.getClient().close(); + mongooseInstance.connection.getClient().close(); }); beforeEach(async function() { From 0999889121ed3161a57d97e87bd613a3e82d5821 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 2 Dec 2023 16:29:47 -0500 Subject: [PATCH 08/31] refactor out HTTP2 request logic into a separate function --- src/client/httpClient.ts | 91 ++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index 5f22d9c..f910858 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -172,39 +172,21 @@ export class HTTPClient { ] }; } + if (!requestInfo.url) { + return { + errors: [ + { + message: 'URL not specified' + } + ] + }; + } - const response: { status?: number, data?: Record } = await new Promise((resolve, reject) => { - const path = requestInfo.url?.replace(this.origin, ''); - const req = this.session.request({ - ':path': path, - ':method': 'POST', - token: this.applicationToken - }); - req.write(serializeCommand(requestInfo.data), 'utf8'); - req.end(); - - const response: { status?: number, data?: Record } = {}; - req.on('response', data => { - response.status = data[':status']; - }); - - req.on('error', error => { - reject(error); - }); - - req.setEncoding('utf8'); - let data = ''; - req.on('data', (chunk) => { data += chunk; }); - req.on('end', () => { - try { - response.data = JSON.parse(data); - } catch (error) { - reject(new Error('Unable to parse response as JSON, got: "' + data + '"')); - return; - } - resolve(response); - }); - }); + const response = await this.makeHTTP2Request( + requestInfo.url.replace(this.origin, ''), + this.applicationToken, + requestInfo.data + ); if (response.status === 401 || (response.data?.errors?.length > 0 && response.data?.errors?.[0]?.message === 'UNAUTHENTICATED: Invalid token')) { logger.debug('@stargate-mongoose/rest: reconnecting'); @@ -254,16 +236,43 @@ export class HTTPClient { } } - async executeCommand(data: Record, optionsToRetain: Set | null) { - const commandName = Object.keys(data)[0]; - cleanupOptions(commandName, data[commandName], optionsToRetain, this.logSkippedOptions); - const response = await this._request({ - url: this.baseUrl, - method: HTTP_METHODS.post, - data + makeHTTP2Request( + path: string, + token: string, + body: Record + ): Promise<{ status: number, data: Record }> { + return new Promise((resolve, reject) => { + const req = this.session.request({ + ':path': path, + ':method': 'POST', + token + }); + req.write(serializeCommand(body), 'utf8'); + req.end(); + + let status = 0; + req.on('response', data => { + status = data[':status'] ?? 0; + }); + + req.on('error', error => { + reject(error); + }); + + req.setEncoding('utf8'); + let responseBody = ''; + req.on('data', (chunk) => { responseBody += chunk; }); + req.on('end', () => { + let data = {}; + try { + data = JSON.parse(responseBody); + resolve({ status, data }); + } catch (error) { + reject(new Error('Unable to parse response as JSON, got: "' + data + '"')); + return; + } + }); }); - handleIfErrorResponse(response, data); - return response; } async executeCommandWithUrl(url: string, data: Record, optionsToRetain: Set | null) { From 1fefe9c8167b7e5140a1bbd12732739c477b8075 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 3 Dec 2023 13:08:35 -0500 Subject: [PATCH 09/31] confirm that Mongoose close() and disconnect() correctly disconnect http2 client --- src/client/httpClient.ts | 3 +++ tests/collections/client.test.ts | 33 +++++++++++++++++++++++++------- tests/driver/index.test.ts | 22 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index f910858..71a876b 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -99,6 +99,7 @@ export class HTTPClient { isAstra: boolean; logSkippedOptions: boolean; session: http2.ClientHttp2Session; + closed: boolean; constructor(options: APIClientOptions) { // do not support usage in browsers @@ -125,6 +126,7 @@ export class HTTPClient { this.origin = new URL(this.baseUrl).origin; this.session = http2.connect(this.origin); + this.closed = false; // Without these handlers, any errors will end up as uncaught exceptions, // even if they are handled in `_request()`. @@ -145,6 +147,7 @@ export class HTTPClient { close() { this.session.close(); + this.closed = true; } async _request(requestInfo: AxiosRequestConfig): Promise { diff --git a/tests/collections/client.test.ts b/tests/collections/client.test.ts index 6500473..d7fc011 100644 --- a/tests/collections/client.test.ts +++ b/tests/collections/client.test.ts @@ -25,9 +25,6 @@ describe('StargateMongoose clients test', () => { let appClient: Client | null; let clientURI: string; before(async function () { - if (process.env.TEST_DOC_DB === 'jsonapi') { - return this.skip(); - } if (testClient == null) { return this.skip(); } @@ -78,6 +75,7 @@ describe('StargateMongoose clients test', () => { assert.strictEqual(client.httpClient.authHeaderName, AUTH_HEADER_NAME_TO_CHECK); const db = client.db(); assert.ok(db); + await client.close(); }); it('should parse baseApiPath from URL when possible', async () => { const AUTH_TOKEN_TO_CHECK = '123'; @@ -97,6 +95,7 @@ describe('StargateMongoose clients test', () => { assert.strictEqual(client.httpClient.authHeaderName, AUTH_HEADER_NAME_TO_CHECK); const db = client.db(); assert.ok(db); + await client.close(); }); it('should parse baseApiPath from URL when possible (multiple path elements)', async () => { const AUTH_TOKEN_TO_CHECK = '123'; @@ -116,6 +115,7 @@ describe('StargateMongoose clients test', () => { assert.strictEqual(client.httpClient.authHeaderName, AUTH_HEADER_NAME_TO_CHECK); const db = client.db(); assert.ok(db); + await client.close(); }); it('should handle when the keyspace name is present in the baseApiPath also', async () => { //only the last occurrence of the keyspace name in the url path must be treated as keyspace @@ -138,6 +138,7 @@ describe('StargateMongoose clients test', () => { assert.strictEqual(client.httpClient.authHeaderName, AUTH_HEADER_NAME_TO_CHECK); const db = client.db(); assert.ok(db); + await client.close(); }); it('should honor the baseApiPath from options when provided', async () => { const AUTH_TOKEN_TO_CHECK = '123'; @@ -159,6 +160,7 @@ describe('StargateMongoose clients test', () => { assert.strictEqual(client.httpClient.authHeaderName, AUTH_HEADER_NAME_TO_CHECK); const db = client.db(); assert.ok(db); + await client.close(); }); it('should handle empty baseApiPath', async () => { const AUTH_TOKEN_TO_CHECK = '123'; @@ -178,6 +180,7 @@ describe('StargateMongoose clients test', () => { assert.strictEqual(client.httpClient.authHeaderName, AUTH_HEADER_NAME_TO_CHECK); const db = client.db(); assert.ok(db); + await client.close(); }); it('should initialize a Client connection with a uri using the constructor', () => { const client = new Client(baseUrl, 'keyspace1', { @@ -216,6 +219,7 @@ describe('StargateMongoose clients test', () => { await client.connect(); assert.ok(client); assert.ok(client.httpClient); + await client.close(); }); it('should set the auth header name as set in the options', async () => { const TEST_HEADER_NAME = 'test-header'; @@ -227,6 +231,7 @@ describe('StargateMongoose clients test', () => { const connectedClient = await client.connect(); assert.ok(connectedClient); assert.strictEqual(connectedClient.httpClient.authHeaderName, TEST_HEADER_NAME); + await client.close(); }); it('should create client when token is not present, but auth details are present', async () => { const client = new Client(baseUrl, 'keyspace1', { @@ -235,6 +240,7 @@ describe('StargateMongoose clients test', () => { }); const connectedClient = client.connect(); assert.ok(connectedClient); + await client.close(); }); it('should not create client when token is not present & one/more of auth details are missing', async () => { let error: any; @@ -260,6 +266,8 @@ describe('StargateMongoose clients test', () => { const connectedClient = client.connect(); assert.ok(connectedClient); assert.strictEqual((await connectedClient).httpClient.authUrl, TEST_AUTH_URL); + + await client.close(); }); it('should construct the auth url with baseUrl when not provided', async () => { const client = new Client(baseUrl, 'keyspace1', { @@ -270,6 +278,8 @@ describe('StargateMongoose clients test', () => { const connectedClient = client.connect(); assert.ok(connectedClient); assert.strictEqual((await connectedClient).httpClient.authUrl, baseUrl + AUTH_API_PATH); + + await client.close(); }); }); describe('Client Db operations', () => { @@ -281,6 +291,8 @@ describe('StargateMongoose clients test', () => { await client.connect(); const db = client.db('keyspace1'); assert.ok(db); + + await client.close(); }); it('should not return a db if no name is provided', async () => { const client = new Client(baseUrl, 'keyspace1', { @@ -296,6 +308,17 @@ describe('StargateMongoose clients test', () => { error = e; } assert.ok(error); + await client.close(); + }); + it('close() should close HTTP client', async () => { + const client = new Client(baseUrl, 'keyspace1', { + applicationToken: '123', + createNamespaceOnConnect: false + }); + await client.connect(); + assert.ok(!client.httpClient.closed); + await client.close(); + assert.ok(client.httpClient.closed); }); }); describe('Client noops', () => { @@ -303,9 +326,5 @@ describe('StargateMongoose clients test', () => { const maxListeners = appClient?.setMaxListeners(1); assert.strictEqual(maxListeners, 1); }); - it('should handle noop: close', async () => { - const closedClient = appClient?.close(); - assert.ok(closedClient); - }); }); }); diff --git a/tests/driver/index.test.ts b/tests/driver/index.test.ts index e038a56..ce2d3f3 100644 --- a/tests/driver/index.test.ts +++ b/tests/driver/index.test.ts @@ -18,6 +18,8 @@ import * as StargateMongooseDriver from '@/src/driver'; import { testClient } from '@/tests/fixtures'; import { logger } from '@/src/logger'; import { parseUri } from '@/src/collections/utils'; +import { HTTPClient } from '@/src/client'; +import { Client } from '@/src/collections'; describe('Driver based tests', async () => { let dbUri: string; @@ -358,6 +360,26 @@ describe('Driver based tests', async () => { ); }); + it('disconnect() closes all httpClients', async () => { + const mongooseInstance = await createMongooseInstance(); + const client: Client = mongooseInstance.connection.getClient() as any as Client; + const httpClient: HTTPClient = client.httpClient; + assert.ok(!httpClient.closed); + await mongooseInstance.disconnect(); + + assert.ok(httpClient.closed); + }); + + it('close() close underlying httpClient', async () => { + const mongooseInstance = await createMongooseInstance(); + const client: Client = mongooseInstance.connection.getClient() as any as Client; + const httpClient: HTTPClient = client.httpClient; + assert.ok(!httpClient.closed); + await client.close(); + + assert.ok(httpClient.closed); + }); + async function createMongooseInstance() { const mongooseInstance = new mongoose.Mongoose(); mongooseInstance.setDriver(StargateMongooseDriver); From e263737b3089de135aad01c440bb15f301e18a38 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 3 Dec 2023 15:53:52 -0500 Subject: [PATCH 10/31] better typings for http2 internals --- src/client/httpClient.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index 71a876b..2af6736 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -245,7 +245,7 @@ export class HTTPClient { body: Record ): Promise<{ status: number, data: Record }> { return new Promise((resolve, reject) => { - const req = this.session.request({ + const req: http2.ClientHttp2Stream = this.session.request({ ':path': path, ':method': 'POST', token @@ -254,17 +254,19 @@ export class HTTPClient { req.end(); let status = 0; - req.on('response', data => { + req.on('response', (data: http2.IncomingHttpStatusHeader) => { status = data[':status'] ?? 0; }); - req.on('error', error => { + req.on('error', (error: Error) => { reject(error); }); req.setEncoding('utf8'); let responseBody = ''; - req.on('data', (chunk) => { responseBody += chunk; }); + req.on('data', (chunk: string) => { + responseBody += chunk; + }); req.on('end', () => { let data = {}; try { From 17a66a81a5ee29899ff87470e569ace0215c550e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 3 Dec 2023 16:04:12 -0500 Subject: [PATCH 11/31] test double closing httpClient --- tests/collections/client.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/collections/client.test.ts b/tests/collections/client.test.ts index d7fc011..e09627c 100644 --- a/tests/collections/client.test.ts +++ b/tests/collections/client.test.ts @@ -319,6 +319,9 @@ describe('StargateMongoose clients test', () => { assert.ok(!client.httpClient.closed); await client.close(); assert.ok(client.httpClient.closed); + + await client.close(); + assert.ok(client.httpClient.closed); }); }); describe('Client noops', () => { From 6d31cac1653260c899ceb373cf9b40fb760ad29e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 23 Dec 2023 17:24:31 -0500 Subject: [PATCH 12/31] first benchmark --- .npmignore | 4 +- benchmarks/.env.benchmark | 4 ++ benchmarks/benchmark-insert-mongoose.ts | 56 +++++++++++++++++++++++++ benchmarks/setup.ts | 6 +++ package.json | 1 + 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 benchmarks/.env.benchmark create mode 100644 benchmarks/benchmark-insert-mongoose.ts create mode 100644 benchmarks/setup.ts diff --git a/.npmignore b/.npmignore index bc70c5d..ce2e800 100644 --- a/.npmignore +++ b/.npmignore @@ -7,4 +7,6 @@ tests .env.example .github -*.tgz \ No newline at end of file +*.tgz + +benchmarks \ No newline at end of file diff --git a/benchmarks/.env.benchmark b/benchmarks/.env.benchmark new file mode 100644 index 0000000..55fbd5d --- /dev/null +++ b/benchmarks/.env.benchmark @@ -0,0 +1,4 @@ +JSON_API_URI=http://127.0.0.1:8181/v1/benchmark +JSON_API_USERNAME=cassandra +JSON_API_PASSWORD=cassandra +JSON_API_AUTH_URL=http://127.0.0.1:8081/v1/auth \ No newline at end of file diff --git a/benchmarks/benchmark-insert-mongoose.ts b/benchmarks/benchmark-insert-mongoose.ts new file mode 100644 index 0000000..47a05a7 --- /dev/null +++ b/benchmarks/benchmark-insert-mongoose.ts @@ -0,0 +1,56 @@ +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => console.log('Done'), + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + console.log() + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); + + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); + + console.log('Inserting 10k docs'); + + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await Test.create({ + name: `John Smith ${i}`, + email: `john${i}@gmail.com`, + age: 30 + }); + } + const results = { + 'Total time MS': Date.now() - start + }; + console.log(results); + process.exit(0); +} diff --git a/benchmarks/setup.ts b/benchmarks/setup.ts new file mode 100644 index 0000000..a72bd8c --- /dev/null +++ b/benchmarks/setup.ts @@ -0,0 +1,6 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ + path: path.join('benchmarks', '.env.benchmark') +}); \ No newline at end of file diff --git a/package.json b/package.json index 8d114cb..8b7de5c 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "url": "git+https://github.com/stargate/stargate-mongoose.git" }, "scripts": { + "benchmark": "ts-node -r ./benchmarks/setup ./benchmarks/benchmark-insert-mongoose", "lint": "eslint .", "test": "env TEST_DOC_DB=jsonapi ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", "test-astra": "env TEST_DOC_DB=astra nyc ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", From c2ba9926eed0be2514d5b315ba49fcedc282ad8b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 23 Dec 2023 17:48:34 -0500 Subject: [PATCH 13/31] add 2nd benchmark script --- benchmarks/benchmark-insert-axios.ts | 67 +++++++++++++++++++++++++ benchmarks/benchmark-insert-mongoose.ts | 5 +- package.json | 2 +- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 benchmarks/benchmark-insert-axios.ts diff --git a/benchmarks/benchmark-insert-axios.ts b/benchmarks/benchmark-insert-axios.ts new file mode 100644 index 0000000..32199cb --- /dev/null +++ b/benchmarks/benchmark-insert-axios.ts @@ -0,0 +1,67 @@ +import axios from 'axios'; +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); + + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); + // @ts-ignore + const token = mongoose.connection.getClient().httpClient.applicationToken; + + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await axios.post( + `${process.env.JSON_API_URI}/tests`, + { + insertOne: { + document: { + name: `John Smith ${i}`, + email: `john${i}@gmail.com`, + age: 30 + } + } + }, + { + headers: { + Token: token + } + } + ); + } + const results = { + 'Total time MS': Date.now() - start + }; + console.log(results); +} diff --git a/benchmarks/benchmark-insert-mongoose.ts b/benchmarks/benchmark-insert-mongoose.ts index 47a05a7..55f8de7 100644 --- a/benchmarks/benchmark-insert-mongoose.ts +++ b/benchmarks/benchmark-insert-mongoose.ts @@ -7,7 +7,7 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => console.log('Done'), + () => process.exit(0), err => { console.error(err); process.exit(-1); @@ -38,8 +38,6 @@ async function main() { await Test.db.dropCollection('tests').catch(() => {}); await Test.createCollection(); - console.log('Inserting 10k docs'); - const start = Date.now(); for (let i = 0; i < 10000; ++i) { await Test.create({ @@ -52,5 +50,4 @@ async function main() { 'Total time MS': Date.now() - start }; console.log(results); - process.exit(0); } diff --git a/package.json b/package.json index 8b7de5c..bb7484d 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "url": "git+https://github.com/stargate/stargate-mongoose.git" }, "scripts": { - "benchmark": "ts-node -r ./benchmarks/setup ./benchmarks/benchmark-insert-mongoose", + "benchmark": "ts-node -r ./benchmarks/setup ./benchmarks/benchmark-insert-axios", "lint": "eslint .", "test": "env TEST_DOC_DB=jsonapi ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", "test-astra": "env TEST_DOC_DB=astra nyc ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", From 2ab00cc5b46f0a921752ff8f7005339aa6685e9e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 24 Dec 2023 18:53:48 -0500 Subject: [PATCH 14/31] add 2 more benchmarks, run all benchmarks with npm run benchmark --- .gitignore | 4 +- benchmarks/benchmark-findone-axios.ts | 73 ++++++++++++++++++++++++ benchmarks/benchmark-findone-mongoose.ts | 55 ++++++++++++++++++ benchmarks/benchmark-insert-axios.ts | 5 +- benchmarks/benchmark-insert-mongoose.ts | 6 +- package.json | 2 +- 6 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 benchmarks/benchmark-findone-axios.ts create mode 100644 benchmarks/benchmark-findone-mongoose.ts diff --git a/.gitignore b/.gitignore index 0b1bd25..b13d5ba 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ node_modules .vscode dist package-lock.json -.idea \ No newline at end of file +.idea +*.out +*.tgz \ No newline at end of file diff --git a/benchmarks/benchmark-findone-axios.ts b/benchmarks/benchmark-findone-axios.ts new file mode 100644 index 0000000..ee8fe38 --- /dev/null +++ b/benchmarks/benchmark-findone-axios.ts @@ -0,0 +1,73 @@ +import axios from 'axios'; +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => console.log('Done'), + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); + + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); + + await Test.create({ + name: 'John Smith', + email: 'john@gmail.com', + age: 30 + }); + + // @ts-ignore + const token = mongoose.connection.getClient().httpClient.applicationToken; + + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await axios.post( + `${process.env.JSON_API_URI ?? ''}/tests`, + { + findOne: { + filter: { + name: 'John Smith' + } + } + }, + { + headers: { + Token: token + } + } + ); + } + const results = { + name: 'benchmark-findone-axios', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} diff --git a/benchmarks/benchmark-findone-mongoose.ts b/benchmarks/benchmark-findone-mongoose.ts new file mode 100644 index 0000000..ba30808 --- /dev/null +++ b/benchmarks/benchmark-findone-mongoose.ts @@ -0,0 +1,55 @@ +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => console.log('Done'), + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); + + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); + + await Test.create({ + name: 'John Smith', + email: 'john@gmail.com', + age: 30 + }); + + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await Test.findOne({ name: 'John Smith' }); + } + const results = { + name: 'benchmark-findone-mongoose', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} diff --git a/benchmarks/benchmark-insert-axios.ts b/benchmarks/benchmark-insert-axios.ts index 32199cb..d9baea3 100644 --- a/benchmarks/benchmark-insert-axios.ts +++ b/benchmarks/benchmark-insert-axios.ts @@ -61,7 +61,8 @@ async function main() { ); } const results = { - 'Total time MS': Date.now() - start + name: 'benchmark-insert-axios', + totalTimeMS: Date.now() - start }; - console.log(results); + console.log(JSON.stringify(results, null, ' ')); } diff --git a/benchmarks/benchmark-insert-mongoose.ts b/benchmarks/benchmark-insert-mongoose.ts index 55f8de7..34c24b0 100644 --- a/benchmarks/benchmark-insert-mongoose.ts +++ b/benchmarks/benchmark-insert-mongoose.ts @@ -15,7 +15,6 @@ main().then( ); async function main() { - console.log() await mongoose.connect(process.env.JSON_API_URI ?? '', { username: process.env.JSON_API_USERNAME, password: process.env.JSON_API_PASSWORD, @@ -47,7 +46,8 @@ async function main() { }); } const results = { - 'Total time MS': Date.now() - start + name: 'benchmark-insert-axios', + totalTimeMS: Date.now() - start }; - console.log(results); + console.log(JSON.stringify(results, null, ' ')); } diff --git a/package.json b/package.json index bb7484d..4119a56 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "url": "git+https://github.com/stargate/stargate-mongoose.git" }, "scripts": { - "benchmark": "ts-node -r ./benchmarks/setup ./benchmarks/benchmark-insert-axios", + "benchmark": "for file in ./benchmarks/benchmark-*.ts; do echo \"$file\"; ts-node -r ./benchmarks/setup \"$file\" > \"$file\".out; done", "lint": "eslint .", "test": "env TEST_DOC_DB=jsonapi ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", "test-astra": "env TEST_DOC_DB=astra nyc ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", From bdc272653f8cf1cc3674d5a114601241b1a968b7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 24 Dec 2023 22:27:40 -0500 Subject: [PATCH 15/31] add vector benchmarks --- benchmarks/benchmark-findone-axios-vector.ts | 81 +++++++++++++++++++ .../benchmark-findone-mongoose-vector.ts | 62 ++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 benchmarks/benchmark-findone-axios-vector.ts create mode 100644 benchmarks/benchmark-findone-mongoose-vector.ts diff --git a/benchmarks/benchmark-findone-axios-vector.ts b/benchmarks/benchmark-findone-axios-vector.ts new file mode 100644 index 0000000..87ea07d --- /dev/null +++ b/benchmarks/benchmark-findone-axios-vector.ts @@ -0,0 +1,81 @@ +import axios from 'axios'; +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => console.log('Done'), + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + collectionOptions: { vector: { size: 1536, function: 'cosine' } }, + autoCreate: false + }), + 'vectors' + ); + + // @ts-ignore + const dbName = Vector.db.db.name; + + await Vector.db.dropCollection('vectors').catch(() => {}); + await Vector.createCollection(); + + const numVectors = 1000; + for (let i = 0; i < numVectors; ++i) { + const $vector = Array(1536).fill(0); + $vector[i] = 1; + await Vector.create({ $vector, prompt: `Test ${i}` }); + } + + // @ts-ignore + const baseUrl = mongoose.connection.getClient().httpClient.baseUrl; + // @ts-ignore + const token = mongoose.connection.getClient().httpClient.applicationToken; + const $meta = [1, ...Array(1535).fill(0)]; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await axios.post( + `${baseUrl}/${dbName}/vectors`, + { + findOne: { + filter: {}, + sort: { $vector: $meta } + } + }, + { + headers: { + Token: token + } + } + ); + } + const results = { + name: 'benchmark-findone-axios-vector', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} diff --git a/benchmarks/benchmark-findone-mongoose-vector.ts b/benchmarks/benchmark-findone-mongoose-vector.ts new file mode 100644 index 0000000..b553dcb --- /dev/null +++ b/benchmarks/benchmark-findone-mongoose-vector.ts @@ -0,0 +1,62 @@ +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => console.log('Done'), + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + collectionOptions: { vector: { size: 1536, function: 'cosine' } }, + autoCreate: false + }), + 'vectors' + ); + + await Vector.db.dropCollection('vectors').catch(() => {}); + await Vector.createCollection(); + + const numVectors = 1000; + for (let i = 0; i < numVectors; ++i) { + const $vector = Array(1536).fill(0); + $vector[i] = 1; + await Vector.create({ $vector, prompt: `Test ${i}` }); + } + + const $meta = [1, ...Array(1535).fill(0)]; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await Vector + .findOne({}) + .sort({ $vector: { $meta } }); + } + const results = { + name: 'benchmark-findone-mongoose-vector', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} From 8b3e87dcb47fa3269989c6648d63746356518d6e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 24 Dec 2023 22:44:09 -0500 Subject: [PATCH 16/31] quick fix to make benchmark script run --- benchmarks/benchmark-findone-axios-vector.ts | 2 +- benchmarks/benchmark-findone-axios.ts | 2 +- benchmarks/benchmark-findone-mongoose-vector.ts | 2 +- benchmarks/benchmark-findone-mongoose.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/benchmark-findone-axios-vector.ts b/benchmarks/benchmark-findone-axios-vector.ts index 87ea07d..ee9a876 100644 --- a/benchmarks/benchmark-findone-axios-vector.ts +++ b/benchmarks/benchmark-findone-axios-vector.ts @@ -8,7 +8,7 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => console.log('Done'), + () => { process.exit(0); }, err => { console.error(err); process.exit(-1); diff --git a/benchmarks/benchmark-findone-axios.ts b/benchmarks/benchmark-findone-axios.ts index ee8fe38..48dd994 100644 --- a/benchmarks/benchmark-findone-axios.ts +++ b/benchmarks/benchmark-findone-axios.ts @@ -8,7 +8,7 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => console.log('Done'), + () => { process.exit(0); }, err => { console.error(err); process.exit(-1); diff --git a/benchmarks/benchmark-findone-mongoose-vector.ts b/benchmarks/benchmark-findone-mongoose-vector.ts index b553dcb..a1e8b32 100644 --- a/benchmarks/benchmark-findone-mongoose-vector.ts +++ b/benchmarks/benchmark-findone-mongoose-vector.ts @@ -7,7 +7,7 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => console.log('Done'), + () => { process.exit(0); }, err => { console.error(err); process.exit(-1); diff --git a/benchmarks/benchmark-findone-mongoose.ts b/benchmarks/benchmark-findone-mongoose.ts index ba30808..19019db 100644 --- a/benchmarks/benchmark-findone-mongoose.ts +++ b/benchmarks/benchmark-findone-mongoose.ts @@ -7,7 +7,7 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => console.log('Done'), + () => { process.exit(0); }, err => { console.error(err); process.exit(-1); From 1a001a0d1fe384130d2b11a05323ca146ca419b1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 17:11:52 -0500 Subject: [PATCH 17/31] add benchmarks that run against astra --- .../benchmark-findone-axios-vector-astra.ts | 76 +++++++++++ benchmarks/benchmark-findone-axios-vector.ts | 126 +++++++++--------- benchmarks/benchmark-findone-axios.ts | 110 +++++++-------- ...benchmark-findone-mongoose-vector-astra.ts | 53 ++++++++ .../benchmark-findone-mongoose-vector.ts | 92 ++++++------- benchmarks/benchmark-findone-mongoose.ts | 78 +++++------ benchmarks/benchmark-insert-axios.ts | 104 +++++++-------- benchmarks/benchmark-insert-mongoose.ts | 76 +++++------ benchmarks/setup.ts | 2 +- package.json | 2 +- 10 files changed, 424 insertions(+), 295 deletions(-) create mode 100644 benchmarks/benchmark-findone-axios-vector-astra.ts create mode 100644 benchmarks/benchmark-findone-mongoose-vector-astra.ts diff --git a/benchmarks/benchmark-findone-axios-vector-astra.ts b/benchmarks/benchmark-findone-axios-vector-astra.ts new file mode 100644 index 0000000..4193c6c --- /dev/null +++ b/benchmarks/benchmark-findone-axios-vector-astra.ts @@ -0,0 +1,76 @@ +import axios from 'axios'; +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + if (!process.env.ASTRA_CONNECTION_STRING) { + console.log('{}'); + return; + } + await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { + isAstra: true + } as mongoose.ConnectOptions); + + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + autoCreate: false + }), + process.env.ASTRA_COLLECTION_NAME + ); + + // @ts-ignore + const dbName = Vector.db.db.name; + + const collectionName = process.env.ASTRA_COLLECTION_NAME ?? ''; + + // @ts-ignore + const baseUrl = mongoose.connection.getClient().httpClient.baseUrl; + + // @ts-ignore + const token = mongoose.connection.getClient().httpClient.applicationToken; + const $meta = [1, ...Array(1535).fill(0)]; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await axios.post( + `${baseUrl}/${dbName}/${collectionName}`, + { + findOne: { + filter: {}, + sort: { $vector: $meta } + } + }, + { + headers: { + Token: token + } + } + ); + } + const results = { + name: 'benchmark-findone-axios-vector-astra', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} diff --git a/benchmarks/benchmark-findone-axios-vector.ts b/benchmarks/benchmark-findone-axios-vector.ts index ee9a876..577f4a1 100644 --- a/benchmarks/benchmark-findone-axios-vector.ts +++ b/benchmarks/benchmark-findone-axios-vector.ts @@ -8,74 +8,74 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => { process.exit(0); }, - err => { - console.error(err); - process.exit(-1); - } + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } ); async function main() { - await mongoose.connect(process.env.JSON_API_URI ?? '', { - username: process.env.JSON_API_USERNAME, - password: process.env.JSON_API_PASSWORD, - authUrl: process.env.JSON_API_AUTH_URL - } as mongoose.ConnectOptions); - const Vector = mongoose.model( - 'Vector', - new mongoose.Schema({ - $vector: { - type: [Number] - }, - prompt: { - type: String, - required: true - } - }, { - collectionOptions: { vector: { size: 1536, function: 'cosine' } }, - autoCreate: false - }), - 'vectors' - ); + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + collectionOptions: { vector: { size: 1536, function: 'cosine' } }, + autoCreate: false + }), + 'vectors' + ); - // @ts-ignore - const dbName = Vector.db.db.name; + // @ts-ignore + const dbName = Vector.db.db.name; - await Vector.db.dropCollection('vectors').catch(() => {}); - await Vector.createCollection(); + await Vector.db.dropCollection('vectors').catch(() => {}); + await Vector.createCollection(); - const numVectors = 1000; - for (let i = 0; i < numVectors; ++i) { - const $vector = Array(1536).fill(0); - $vector[i] = 1; - await Vector.create({ $vector, prompt: `Test ${i}` }); - } + const numVectors = 1000; + for (let i = 0; i < numVectors; ++i) { + const $vector = Array(1536).fill(0); + $vector[i] = 1; + await Vector.create({ $vector, prompt: `Test ${i}` }); + } - // @ts-ignore - const baseUrl = mongoose.connection.getClient().httpClient.baseUrl; - // @ts-ignore - const token = mongoose.connection.getClient().httpClient.applicationToken; - const $meta = [1, ...Array(1535).fill(0)]; - const start = Date.now(); - for (let i = 0; i < 100; ++i) { - await axios.post( - `${baseUrl}/${dbName}/vectors`, - { - findOne: { - filter: {}, - sort: { $vector: $meta } - } - }, - { - headers: { - Token: token - } - } - ); - } - const results = { - name: 'benchmark-findone-axios-vector', - totalTimeMS: Date.now() - start - }; - console.log(JSON.stringify(results, null, ' ')); + // @ts-ignore + const baseUrl = mongoose.connection.getClient().httpClient.baseUrl; + // @ts-ignore + const token = mongoose.connection.getClient().httpClient.applicationToken; + const $meta = [1, ...Array(1535).fill(0)]; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await axios.post( + `${baseUrl}/${dbName}/vectors`, + { + findOne: { + filter: {}, + sort: { $vector: $meta } + } + }, + { + headers: { + Token: token + } + } + ); + } + const results = { + name: 'benchmark-findone-axios-vector', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); } diff --git a/benchmarks/benchmark-findone-axios.ts b/benchmarks/benchmark-findone-axios.ts index 48dd994..34aad78 100644 --- a/benchmarks/benchmark-findone-axios.ts +++ b/benchmarks/benchmark-findone-axios.ts @@ -8,66 +8,66 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => { process.exit(0); }, - err => { - console.error(err); - process.exit(-1); - } + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } ); async function main() { - await mongoose.connect(process.env.JSON_API_URI ?? '', { - username: process.env.JSON_API_USERNAME, - password: process.env.JSON_API_PASSWORD, - authUrl: process.env.JSON_API_AUTH_URL - } as mongoose.ConnectOptions); - const Test = mongoose.model('Test', new mongoose.Schema({ - name: { - type: String, - required: true - }, - email: { - type: String, - required: true - }, - age: { - type: Number - } - })); + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); - await Test.db.dropCollection('tests').catch(() => {}); - await Test.createCollection(); + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); - await Test.create({ - name: 'John Smith', - email: 'john@gmail.com', - age: 30 - }); + await Test.create({ + name: 'John Smith', + email: 'john@gmail.com', + age: 30 + }); - // @ts-ignore - const token = mongoose.connection.getClient().httpClient.applicationToken; + // @ts-ignore + const token = mongoose.connection.getClient().httpClient.applicationToken; - const start = Date.now(); - for (let i = 0; i < 10000; ++i) { - await axios.post( - `${process.env.JSON_API_URI ?? ''}/tests`, - { - findOne: { - filter: { - name: 'John Smith' - } - } - }, - { - headers: { - Token: token - } - } - ); - } - const results = { - name: 'benchmark-findone-axios', - totalTimeMS: Date.now() - start - }; - console.log(JSON.stringify(results, null, ' ')); + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await axios.post( + `${process.env.JSON_API_URI ?? ''}/tests`, + { + findOne: { + filter: { + name: 'John Smith' + } + } + }, + { + headers: { + Token: token + } + } + ); + } + const results = { + name: 'benchmark-findone-axios', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); } diff --git a/benchmarks/benchmark-findone-mongoose-vector-astra.ts b/benchmarks/benchmark-findone-mongoose-vector-astra.ts new file mode 100644 index 0000000..a98704f --- /dev/null +++ b/benchmarks/benchmark-findone-mongoose-vector-astra.ts @@ -0,0 +1,53 @@ +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + if (!process.env.ASTRA_CONNECTION_STRING) { + console.log('{}'); + return; + } + await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { + isAstra: true + } as mongoose.ConnectOptions); + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + autoCreate: false + }), + process.env.ASTRA_COLLECTION_NAME + ); + + const $meta = [1, ...Array(1535).fill(0)]; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await Vector + .findOne({}) + .sort({ $vector: { $meta } }); + } + const results = { + name: 'benchmark-findone-mongoose-vector-astra', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} diff --git a/benchmarks/benchmark-findone-mongoose-vector.ts b/benchmarks/benchmark-findone-mongoose-vector.ts index a1e8b32..8dd95d7 100644 --- a/benchmarks/benchmark-findone-mongoose-vector.ts +++ b/benchmarks/benchmark-findone-mongoose-vector.ts @@ -7,56 +7,56 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => { process.exit(0); }, - err => { - console.error(err); - process.exit(-1); - } + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } ); async function main() { - await mongoose.connect(process.env.JSON_API_URI ?? '', { - username: process.env.JSON_API_USERNAME, - password: process.env.JSON_API_PASSWORD, - authUrl: process.env.JSON_API_AUTH_URL - } as mongoose.ConnectOptions); - const Vector = mongoose.model( - 'Vector', - new mongoose.Schema({ - $vector: { - type: [Number] - }, - prompt: { - type: String, - required: true - } - }, { - collectionOptions: { vector: { size: 1536, function: 'cosine' } }, - autoCreate: false - }), - 'vectors' - ); + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + collectionOptions: { vector: { size: 1536, function: 'cosine' } }, + autoCreate: false + }), + 'vectors' + ); - await Vector.db.dropCollection('vectors').catch(() => {}); - await Vector.createCollection(); + await Vector.db.dropCollection('vectors').catch(() => {}); + await Vector.createCollection(); - const numVectors = 1000; - for (let i = 0; i < numVectors; ++i) { - const $vector = Array(1536).fill(0); - $vector[i] = 1; - await Vector.create({ $vector, prompt: `Test ${i}` }); - } + const numVectors = 1000; + for (let i = 0; i < numVectors; ++i) { + const $vector = Array(1536).fill(0); + $vector[i] = 1; + await Vector.create({ $vector, prompt: `Test ${i}` }); + } - const $meta = [1, ...Array(1535).fill(0)]; - const start = Date.now(); - for (let i = 0; i < 100; ++i) { - await Vector - .findOne({}) - .sort({ $vector: { $meta } }); - } - const results = { - name: 'benchmark-findone-mongoose-vector', - totalTimeMS: Date.now() - start - }; - console.log(JSON.stringify(results, null, ' ')); + const $meta = [1, ...Array(1535).fill(0)]; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await Vector + .findOne({}) + .sort({ $vector: { $meta } }); + } + const results = { + name: 'benchmark-findone-mongoose-vector', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); } diff --git a/benchmarks/benchmark-findone-mongoose.ts b/benchmarks/benchmark-findone-mongoose.ts index 19019db..91b5352 100644 --- a/benchmarks/benchmark-findone-mongoose.ts +++ b/benchmarks/benchmark-findone-mongoose.ts @@ -7,49 +7,49 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => { process.exit(0); }, - err => { - console.error(err); - process.exit(-1); - } + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } ); async function main() { - await mongoose.connect(process.env.JSON_API_URI ?? '', { - username: process.env.JSON_API_USERNAME, - password: process.env.JSON_API_PASSWORD, - authUrl: process.env.JSON_API_AUTH_URL - } as mongoose.ConnectOptions); - const Test = mongoose.model('Test', new mongoose.Schema({ - name: { - type: String, - required: true - }, - email: { - type: String, - required: true - }, - age: { - type: Number - } - })); + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); - await Test.db.dropCollection('tests').catch(() => {}); - await Test.createCollection(); + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); - await Test.create({ - name: 'John Smith', - email: 'john@gmail.com', - age: 30 - }); + await Test.create({ + name: 'John Smith', + email: 'john@gmail.com', + age: 30 + }); - const start = Date.now(); - for (let i = 0; i < 10000; ++i) { - await Test.findOne({ name: 'John Smith' }); - } - const results = { - name: 'benchmark-findone-mongoose', - totalTimeMS: Date.now() - start - }; - console.log(JSON.stringify(results, null, ' ')); + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await Test.findOne({ name: 'John Smith' }); + } + const results = { + name: 'benchmark-findone-mongoose', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); } diff --git a/benchmarks/benchmark-insert-axios.ts b/benchmarks/benchmark-insert-axios.ts index d9baea3..961179c 100644 --- a/benchmarks/benchmark-insert-axios.ts +++ b/benchmarks/benchmark-insert-axios.ts @@ -8,61 +8,61 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => { process.exit(0); }, - err => { - console.error(err); - process.exit(-1); - } + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } ); async function main() { - await mongoose.connect(process.env.JSON_API_URI ?? '', { - username: process.env.JSON_API_USERNAME, - password: process.env.JSON_API_PASSWORD, - authUrl: process.env.JSON_API_AUTH_URL - } as mongoose.ConnectOptions); - const Test = mongoose.model('Test', new mongoose.Schema({ - name: { - type: String, - required: true - }, - email: { - type: String, - required: true - }, - age: { - type: Number - } - })); + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); - await Test.db.dropCollection('tests').catch(() => {}); - await Test.createCollection(); - // @ts-ignore - const token = mongoose.connection.getClient().httpClient.applicationToken; + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); + // @ts-ignore + const token = mongoose.connection.getClient().httpClient.applicationToken; - const start = Date.now(); - for (let i = 0; i < 10000; ++i) { - await axios.post( - `${process.env.JSON_API_URI}/tests`, - { - insertOne: { - document: { - name: `John Smith ${i}`, - email: `john${i}@gmail.com`, - age: 30 - } - } - }, - { - headers: { - Token: token - } - } - ); - } - const results = { - name: 'benchmark-insert-axios', - totalTimeMS: Date.now() - start - }; - console.log(JSON.stringify(results, null, ' ')); + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await axios.post( + `${process.env.JSON_API_URI}/tests`, + { + insertOne: { + document: { + name: `John Smith ${i}`, + email: `john${i}@gmail.com`, + age: 30 + } + } + }, + { + headers: { + Token: token + } + } + ); + } + const results = { + name: 'benchmark-insert-axios', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); } diff --git a/benchmarks/benchmark-insert-mongoose.ts b/benchmarks/benchmark-insert-mongoose.ts index 34c24b0..0369c19 100644 --- a/benchmarks/benchmark-insert-mongoose.ts +++ b/benchmarks/benchmark-insert-mongoose.ts @@ -7,47 +7,47 @@ mongoose.set('autoIndex', false); mongoose.setDriver(driver); main().then( - () => process.exit(0), - err => { - console.error(err); - process.exit(-1); - } + () => process.exit(0), + err => { + console.error(err); + process.exit(-1); + } ); async function main() { - await mongoose.connect(process.env.JSON_API_URI ?? '', { - username: process.env.JSON_API_USERNAME, - password: process.env.JSON_API_PASSWORD, - authUrl: process.env.JSON_API_AUTH_URL - } as mongoose.ConnectOptions); - const Test = mongoose.model('Test', new mongoose.Schema({ - name: { - type: String, - required: true - }, - email: { - type: String, - required: true - }, - age: { - type: Number - } - })); + await mongoose.connect(process.env.JSON_API_URI ?? '', { + username: process.env.JSON_API_USERNAME, + password: process.env.JSON_API_PASSWORD, + authUrl: process.env.JSON_API_AUTH_URL + } as mongoose.ConnectOptions); + const Test = mongoose.model('Test', new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + age: { + type: Number + } + })); - await Test.db.dropCollection('tests').catch(() => {}); - await Test.createCollection(); + await Test.db.dropCollection('tests').catch(() => {}); + await Test.createCollection(); - const start = Date.now(); - for (let i = 0; i < 10000; ++i) { - await Test.create({ - name: `John Smith ${i}`, - email: `john${i}@gmail.com`, - age: 30 - }); - } - const results = { - name: 'benchmark-insert-axios', - totalTimeMS: Date.now() - start - }; - console.log(JSON.stringify(results, null, ' ')); + const start = Date.now(); + for (let i = 0; i < 10000; ++i) { + await Test.create({ + name: `John Smith ${i}`, + email: `john${i}@gmail.com`, + age: 30 + }); + } + const results = { + name: 'benchmark-insert-axios', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); } diff --git a/benchmarks/setup.ts b/benchmarks/setup.ts index a72bd8c..f569006 100644 --- a/benchmarks/setup.ts +++ b/benchmarks/setup.ts @@ -2,5 +2,5 @@ import dotenv from 'dotenv'; import path from 'path'; dotenv.config({ - path: path.join('benchmarks', '.env.benchmark') + path: path.join('benchmarks', '.env.benchmark') }); \ No newline at end of file diff --git a/package.json b/package.json index 4119a56..31d7fa5 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "url": "git+https://github.com/stargate/stargate-mongoose.git" }, "scripts": { - "benchmark": "for file in ./benchmarks/benchmark-*.ts; do echo \"$file\"; ts-node -r ./benchmarks/setup \"$file\" > \"$file\".out; done", + "benchmark": "npm run build; for file in ./benchmarks/benchmark-*.ts; do echo \"$file\"; ts-node -r ./benchmarks/setup \"$file\" > \"$file\".out; done", "lint": "eslint .", "test": "env TEST_DOC_DB=jsonapi ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", "test-astra": "env TEST_DOC_DB=astra nyc ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", From f6c78e3c21610ebd09d4058d083c44478c850971 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 17:25:46 -0500 Subject: [PATCH 18/31] add benchmarks workflow --- .github/workflows/benchmarks.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/benchmarks.yml diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 0000000..d56d5c3 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,21 @@ +name: Tests - stargate-mongoose +on: + pull_request: + push: +jobs: + e2e: + runs-on: ubuntu-latest + needs: tests + steps: + - name: disable and stop mono-xsp4.service + run: | + sudo kill -9 $(sudo lsof -t -i:8084) + - uses: actions/checkout@v3 + - name: Setting up the node version + uses: actions/setup-node@v3 + with: + node-version: 16.15.0 + - name: setup project + run: npm i + - name: benchmarks + run: npm run benchmark \ No newline at end of file From 5959071a9df25a8b70afe49fc9d43e7db157b504 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 17:28:18 -0500 Subject: [PATCH 19/31] fix workflows config --- .github/workflows/benchmarks.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index d56d5c3..6258949 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -5,12 +5,19 @@ on: jobs: e2e: runs-on: ubuntu-latest - needs: tests steps: - name: disable and stop mono-xsp4.service run: | sudo kill -9 $(sudo lsof -t -i:8084) - uses: actions/checkout@v3 + - name: Set up JSON API + run: | + chmod +x bin/start_json_api.sh + bin/start_json_api.sh + - name: Wait for services + run: | + while ! nc -z localhost 8181; do sleep 1; done + while ! nc -z localhost 8081; do sleep 1; done - name: Setting up the node version uses: actions/setup-node@v3 with: From 490b7b212e386e291b14d9d4ac7383865a15fa87 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 17:29:25 -0500 Subject: [PATCH 20/31] fix workflow name --- .github/workflows/benchmarks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 6258949..cd13b99 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -1,9 +1,9 @@ -name: Tests - stargate-mongoose +name: Benchmarks - stargate-mongoose on: pull_request: push: jobs: - e2e: + benchmarks: runs-on: ubuntu-latest steps: - name: disable and stop mono-xsp4.service From 483d1c63c2ca1c5760c149c36860be595180962d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 17:57:01 -0500 Subject: [PATCH 21/31] rename benchmark script, print file output instead of uploading artifacts --- .github/workflows/benchmarks.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index cd13b99..f142c41 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -25,4 +25,4 @@ jobs: - name: setup project run: npm i - name: benchmarks - run: npm run benchmark \ No newline at end of file + run: npm run benchmarks \ No newline at end of file diff --git a/package.json b/package.json index 31d7fa5..3ec0688 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "url": "git+https://github.com/stargate/stargate-mongoose.git" }, "scripts": { - "benchmark": "npm run build; for file in ./benchmarks/benchmark-*.ts; do echo \"$file\"; ts-node -r ./benchmarks/setup \"$file\" > \"$file\".out; done", + "benchmarks": "npm run build; for file in ./benchmarks/benchmark-*.ts; do echo \"$file\"; ts-node -r ./benchmarks/setup \"$file\" > \"$file\".out; cat \"$file\".out; done", "lint": "eslint .", "test": "env TEST_DOC_DB=jsonapi ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", "test-astra": "env TEST_DOC_DB=astra nyc ts-mocha --paths -p tsconfig.json tests/**/*.test.ts", From bc210ce98f4cff8ee010c272a98a598e41567eb6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 18:34:13 -0500 Subject: [PATCH 22/31] add script to upload benchmark results --- .../benchmark-findone-axios-vector-astra.ts | 2 +- ...benchmark-findone-mongoose-vector-astra.ts | 2 +- benchmarks/upload-results.ts | 65 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 benchmarks/upload-results.ts diff --git a/benchmarks/benchmark-findone-axios-vector-astra.ts b/benchmarks/benchmark-findone-axios-vector-astra.ts index 4193c6c..aaf17b7 100644 --- a/benchmarks/benchmark-findone-axios-vector-astra.ts +++ b/benchmarks/benchmark-findone-axios-vector-astra.ts @@ -17,7 +17,7 @@ main().then( async function main() { if (!process.env.ASTRA_CONNECTION_STRING) { - console.log('{}'); + console.log('{"name":"benchmark-findone-axios-vector-astra"}'); return; } await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { diff --git a/benchmarks/benchmark-findone-mongoose-vector-astra.ts b/benchmarks/benchmark-findone-mongoose-vector-astra.ts index a98704f..10229fe 100644 --- a/benchmarks/benchmark-findone-mongoose-vector-astra.ts +++ b/benchmarks/benchmark-findone-mongoose-vector-astra.ts @@ -16,7 +16,7 @@ main().then( async function main() { if (!process.env.ASTRA_CONNECTION_STRING) { - console.log('{}'); + console.log('{"name":"benchmark-findone-mongoose-vector-astra"}'); return; } await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { diff --git a/benchmarks/upload-results.ts b/benchmarks/upload-results.ts new file mode 100644 index 0000000..22cf03c --- /dev/null +++ b/benchmarks/upload-results.ts @@ -0,0 +1,65 @@ +import { driver } from '../'; +import { execSync } from 'child_process'; +import fs from 'fs'; +import mongoose from 'mongoose'; +import path from 'path'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + if (!process.env.ASTRA_UPLOAD_CONNECTION_STRING) { + return; + } + + const githash = execSync('git rev-parse HEAD').toString().trim(); + + await mongoose.connect(process.env.ASTRA_UPLOAD_CONNECTION_STRING, { + isAstra: true + } as mongoose.ConnectOptions); + const BenchmarkResult = mongoose.model( + 'BenchmarkResult', + new mongoose.Schema({ + githash: { + type: String, + required: true + }, + name: { + type: String, + required: true + }, + totalTimeMS: { + type: Number + } + }, { autoCreate: true, timestamps: true }) + ); + await BenchmarkResult.init(); + + const files = fs + .readdirSync(path.join('.', 'benchmarks')) + .filter(file => file.endsWith('.out')); + for (const file of files) { + const { name, totalTimeMS } = JSON.parse( + fs.readFileSync(path.join('.', 'benchmarks', file), 'utf8') + ); + if (totalTimeMS == null) { + continue; + } + const doc = await BenchmarkResult.findOneAndUpdate( + { githash, name }, + { totalTimeMS }, + { upsert: true, returnDocument: 'after' } + ); + console.log(doc); + } +} \ No newline at end of file From c1269039558abf875d0664ec432afe051335155c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Dec 2023 10:34:52 -0500 Subject: [PATCH 23/31] add switch to enable/disable http2 --- src/client/httpClient.ts | 53 +++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/client/httpClient.ts b/src/client/httpClient.ts index 2af6736..ce16fef 100644 --- a/src/client/httpClient.ts +++ b/src/client/httpClient.ts @@ -23,7 +23,7 @@ import http2 from 'http2'; const REQUESTED_WITH = LIB_NAME + '/' + LIB_VERSION; const DEFAULT_AUTH_HEADER = 'X-Cassandra-Token'; -// const DEFAULT_METHOD = 'get'; +const DEFAULT_METHOD = 'get'; const DEFAULT_TIMEOUT = 30000; export const AUTH_API_PATH = '/v1/auth'; const HTTP_METHODS = { @@ -46,6 +46,7 @@ interface APIClientOptions { authUrl?: string; isAstra?: boolean; logSkippedOptions?: boolean; + useHTTP2?: boolean; } export interface APIResponse { @@ -98,7 +99,7 @@ export class HTTPClient { authUrl: string; isAstra: boolean; logSkippedOptions: boolean; - session: http2.ClientHttp2Session; + http2Session?: http2.ClientHttp2Session; closed: boolean; constructor(options: APIClientOptions) { @@ -124,15 +125,17 @@ export class HTTPClient { this.applicationToken = '';//We will set this by accessing the auth url when the first request is received } - this.origin = new URL(this.baseUrl).origin; - this.session = http2.connect(this.origin); this.closed = false; - - // Without these handlers, any errors will end up as uncaught exceptions, - // even if they are handled in `_request()`. - // More info: https://github.com/nodejs/node/issues/16345 - this.session.on('error', () => {}); - this.session.on('socketError', () => {}); + this.origin = new URL(this.baseUrl).origin; + if (options.useHTTP2) { + this.http2Session = http2.connect(this.origin); + + // Without these handlers, any errors will end up as uncaught exceptions, + // even if they are handled in `_request()`. + // More info: https://github.com/nodejs/node/issues/16345 + this.http2Session.on('error', () => {}); + this.http2Session.on('socketError', () => {}); + } if (options.logLevel) { setLevel(options.logLevel); @@ -146,7 +149,9 @@ export class HTTPClient { } close() { - this.session.close(); + if (this.http2Session != null) { + this.http2Session.close(); + } this.closed = true; } @@ -185,11 +190,22 @@ export class HTTPClient { }; } - const response = await this.makeHTTP2Request( - requestInfo.url.replace(this.origin, ''), - this.applicationToken, - requestInfo.data - ); + const response = this.http2Session != null + ? await this.makeHTTP2Request( + requestInfo.url.replace(this.origin, ''), + this.applicationToken, + requestInfo.data + ) + : await axiosAgent({ + url: requestInfo.url, + data: requestInfo.data, + params: requestInfo.params, + method: requestInfo.method || DEFAULT_METHOD, + timeout: requestInfo.timeout || DEFAULT_TIMEOUT, + headers: { + [this.authHeaderName]: this.applicationToken + } + }); if (response.status === 401 || (response.data?.errors?.length > 0 && response.data?.errors?.[0]?.message === 'UNAUTHENTICATED: Invalid token')) { logger.debug('@stargate-mongoose/rest: reconnecting'); @@ -245,7 +261,10 @@ export class HTTPClient { body: Record ): Promise<{ status: number, data: Record }> { return new Promise((resolve, reject) => { - const req: http2.ClientHttp2Stream = this.session.request({ + if (this.http2Session == null) { + throw new Error('Cannot make http2 request without session'); + } + const req: http2.ClientHttp2Stream = this.http2Session.request({ ':path': path, ':method': 'POST', token From ae82eb13acd98af1bbd660c56c0b42e8af22d1c8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Dec 2023 10:55:49 -0500 Subject: [PATCH 24/31] a couple of quick benchmark fixes --- benchmarks/benchmark-findone-axios.ts | 8 +++++--- benchmarks/benchmark-insert-mongoose.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/benchmarks/benchmark-findone-axios.ts b/benchmarks/benchmark-findone-axios.ts index 34aad78..044de1f 100644 --- a/benchmarks/benchmark-findone-axios.ts +++ b/benchmarks/benchmark-findone-axios.ts @@ -16,7 +16,8 @@ main().then( ); async function main() { - await mongoose.connect(process.env.JSON_API_URI ?? '', { + const uri = process.env.JSON_API_URI ?? ''; + await mongoose.connect(uri, { username: process.env.JSON_API_USERNAME, password: process.env.JSON_API_PASSWORD, authUrl: process.env.JSON_API_AUTH_URL @@ -50,12 +51,13 @@ async function main() { const start = Date.now(); for (let i = 0; i < 10000; ++i) { await axios.post( - `${process.env.JSON_API_URI ?? ''}/tests`, + `${uri}/tests`, { findOne: { filter: { name: 'John Smith' - } + }, + options: {} } }, { diff --git a/benchmarks/benchmark-insert-mongoose.ts b/benchmarks/benchmark-insert-mongoose.ts index 0369c19..538a314 100644 --- a/benchmarks/benchmark-insert-mongoose.ts +++ b/benchmarks/benchmark-insert-mongoose.ts @@ -46,7 +46,7 @@ async function main() { }); } const results = { - name: 'benchmark-insert-axios', + name: 'benchmark-insert-mongoose', totalTimeMS: Date.now() - start }; console.log(JSON.stringify(results, null, ' ')); From e1b7adca8022af4442df07a13162e6983929d294 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Dec 2023 11:13:50 -0500 Subject: [PATCH 25/31] properly pass useHTTP2 down to httpClient --- src/collections/client.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/collections/client.ts b/src/collections/client.ts index e9323bd..90855a8 100644 --- a/src/collections/client.ts +++ b/src/collections/client.ts @@ -29,6 +29,7 @@ export interface ClientOptions { authUrl?: string; isAstra?: boolean; logSkippedOptions?: boolean; + useHTTP2?: boolean; } export class Client { @@ -54,7 +55,8 @@ export class Client { password: options.password, authUrl: options.authUrl, isAstra: options.isAstra, - logSkippedOptions: options.logSkippedOptions + logSkippedOptions: options.logSkippedOptions, + useHTTP2: options.useHTTP2 }); this.dbs = new Map(); } @@ -76,7 +78,8 @@ export class Client { password: options?.password, authUrl: options?.authUrl, isAstra: options?.isAstra, - logSkippedOptions: options?.logSkippedOptions + logSkippedOptions: options?.logSkippedOptions, + useHTTP2: options?.useHTTP2 }); await client.connect(); return client; From 878ae37b06416691c8453b5a29efdbdebd3ea287 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Dec 2023 11:46:39 -0500 Subject: [PATCH 26/31] add working http2 benchmark that shows http2 is 3x faster than http1 --- ...ark-findone-mongoose-vector-astra-http2.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts diff --git a/benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts b/benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts new file mode 100644 index 0000000..c2cf9cc --- /dev/null +++ b/benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts @@ -0,0 +1,54 @@ +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + if (!process.env.ASTRA_CONNECTION_STRING) { + console.log('{"name":"benchmark-findone-mongoose-vector-astra"}'); + return; + } + await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { + isAstra: true, + useHTTP2: true + } as mongoose.ConnectOptions); + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + autoCreate: false + }), + process.env.ASTRA_COLLECTION_NAME + ); + + const $meta = [1, ...Array(1535).fill(0)]; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await Vector + .findOne({}) + .sort({ $vector: { $meta } }); + } + const results = { + name: 'benchmark-findone-mongoose-vector-astra-http2', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} From 4df1941cbe3fff5a6d67921416b3423611599804 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Dec 2023 13:11:29 -0500 Subject: [PATCH 27/31] add basic README for benchmarks --- benchmarks/README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 benchmarks/README.md diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..88d5a35 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,73 @@ +# Benchmarks + +Each `benchmark-*.ts` file is a separate benchmark that tests different functionality. +Benchmarks use either stargate-mongoose or the Axios HTTP client directly; this is to check whether stargate-mongoose is adding significant overhead or not. + +## Running Benchmarks + +The easiest way to run all benchmarks is using the following npm command: + +``` +npm run benchmarks +``` + +This will run all benchmarks, and output results to both stdout and `.out` files in the `benchmarks` directory. + +You can also run benchmarks individually using `ts-node` as follows: + +``` +$ ./node_modules/.bin/ts-node -r ./benchmarks/setup.ts ./benchmarks/benchmark-findone-mongoose.ts +{ + "name": "benchmark-findone-mongoose", + "totalTimeMS": 14944 +} +``` + +The `benchmarks/.env.benchmark` contains environment variables that configure the connection details for the benchmarks. +The `benchmarks/setup.ts` file sets up the environment using [dotenv](https://npmjs.com/package/dotenv). + +## Astra Benchmarks + +There are 3 benchmarks that connect to Astra, rather than local JSON API: + +* `benchmark-findone-axios-vector-astra.ts` +* `benchmark-findone-mongoose-vector-astra.ts` +* `benchmark-findone-mongoose-vector-astra-http2.ts` + +By default, these benchmarks are skipped: + +``` +$ ./node_modules/.bin/ts-node -r ./benchmarks/setup.ts ./benchmarks/benchmark-findone-mongoose-vector-astra +{"name":"benchmark-findone-mongoose-vector-astra"} +``` + +To run these benchmarks, you need to add the following environment variables to `.env.benchmark`: + +``` +ASTRA_CONNECTION_STRING= +ASTRA_COLLECTION_NAME= +``` + +`ASTRA_CONNECTION_STRING` contains the connection string that will be passed to `mongoose.connect()`, like `https://abc-us-east-2.apps.astra.datastax.com/api/json/v1/mykeyspace?applicationToken=AstraCS:...`, and `ASTRA_COLLECTION_NAME` is the name of the collection that the Mongoose model will connect to. +The Astra benchmarks assume that there is already a collection with data set up in Astra. + +With these environment variables set up, you can run the Astra benchmarks: + +``` +$ ./node_modules/.bin/ts-node -r ./benchmarks/setup.ts ./benchmarks/benchmark-findone-mongoose-vector-astra +{ + "name": "benchmark-findone-mongoose-vector-astra", + "totalTimeMS": 20966 +} +``` + +If you add `ASTRA_CONNECTION_STRING` to `.env.benchmark`, be careful not to accidentally commit your changes to git. +An alternative approach is to set the environment variables in your CLI command as follows: + +``` +$ env ASTRA_CONNECTION_STRING="https://abc-us-east-2.apps.astra.datastax.com/..." env ASTRA_COLLECTION_NAME=mycollection ./node_modules/.bin/ts-node -r ./benchmarks/setup.ts ./benchmarks/benchmark-findone-mongoose-vector-astra +{ + "name": "benchmark-findone-mongoose-vector-astra", + "totalTimeMS": 20436 +} +``` \ No newline at end of file From 15e5c03b7ceca876f4f13fcbc067c1682a0ff3bd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 7 Jan 2024 14:42:49 -0500 Subject: [PATCH 28/31] add script to setup benchmark data for comparing http1 vs http2 --- benchmarks/setup-astra-vector-example.ts | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 benchmarks/setup-astra-vector-example.ts diff --git a/benchmarks/setup-astra-vector-example.ts b/benchmarks/setup-astra-vector-example.ts new file mode 100644 index 0000000..17bffbd --- /dev/null +++ b/benchmarks/setup-astra-vector-example.ts @@ -0,0 +1,71 @@ +import { driver } from '../'; +import mongoose from 'mongoose'; + +mongoose.set('autoCreate', false); +mongoose.set('autoIndex', false); + +mongoose.setDriver(driver); + +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + if (!process.env.ASTRA_CONNECTION_STRING) { + throw new Error('Must set ASTRA_CONNECTION_STRING'); + } + if (!process.env.ASTRA_COLLECTION_NAME) { + throw new Error('Must set ASTRA_COLLECTION_NAME'); +} + await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { + isAstra: true + } as mongoose.ConnectOptions); + + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + collectionOptions: { vector: { size: 1536, function: 'cosine' } }, + autoCreate: false + }), + process.env.ASTRA_COLLECTION_NAME + ); + + console.log('Recreating collection...'); + await Vector.db.dropCollection(process.env.ASTRA_COLLECTION_NAME); + await Vector.createCollection(); + + console.log('Creating vectors...'); + const numVectors = 1000; + for (let i = 0; i < numVectors; ++i) { + console.log(`${i} / ${numVectors}`); + const $vector = Array(1536).fill(0); + $vector[i] = 1; + for (let j = 0; j < 3; ++j) { + try { + await Vector.create({ $vector, prompt: `Test ${i}` }); + break; + } catch (err) { + if (j >= 2) { + throw err; + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + } + + console.log('Done'); + process.exit(0); +} \ No newline at end of file From b8ac5abfc5b3d88ebe8818a3fe9790b79889d517 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Feb 2024 14:05:37 -0500 Subject: [PATCH 29/31] create collection if not found for astra vector benchmark --- ...ark-findone-mongoose-vector-astra-http2.ts | 54 ------------------- ...benchmark-findone-mongoose-vector-astra.ts | 8 +++ 2 files changed, 8 insertions(+), 54 deletions(-) delete mode 100644 benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts diff --git a/benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts b/benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts deleted file mode 100644 index c2cf9cc..0000000 --- a/benchmarks/benchmark-findone-mongoose-vector-astra-http2.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { driver } from '../'; -import mongoose from 'mongoose'; - -mongoose.set('autoCreate', false); -mongoose.set('autoIndex', false); - -mongoose.setDriver(driver); - -main().then( - () => { process.exit(0); }, - err => { - console.error(err); - process.exit(-1); - } -); - -async function main() { - if (!process.env.ASTRA_CONNECTION_STRING) { - console.log('{"name":"benchmark-findone-mongoose-vector-astra"}'); - return; - } - await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { - isAstra: true, - useHTTP2: true - } as mongoose.ConnectOptions); - const Vector = mongoose.model( - 'Vector', - new mongoose.Schema({ - $vector: { - type: [Number] - }, - prompt: { - type: String, - required: true - } - }, { - autoCreate: false - }), - process.env.ASTRA_COLLECTION_NAME - ); - - const $meta = [1, ...Array(1535).fill(0)]; - const start = Date.now(); - for (let i = 0; i < 100; ++i) { - await Vector - .findOne({}) - .sort({ $vector: { $meta } }); - } - const results = { - name: 'benchmark-findone-mongoose-vector-astra-http2', - totalTimeMS: Date.now() - start - }; - console.log(JSON.stringify(results, null, ' ')); -} diff --git a/benchmarks/benchmark-findone-mongoose-vector-astra.ts b/benchmarks/benchmark-findone-mongoose-vector-astra.ts index 10229fe..fbaa40a 100644 --- a/benchmarks/benchmark-findone-mongoose-vector-astra.ts +++ b/benchmarks/benchmark-findone-mongoose-vector-astra.ts @@ -38,6 +38,14 @@ async function main() { process.env.ASTRA_COLLECTION_NAME ); + const collections = await mongoose.connection.listCollections(); + if (!collections.find(({ name }) => name === process.env.ASTRA_COLLECTION_NAME)) { + console.log(`Collection ${process.env.ASTRA_COLLECTION_NAME} not found, creating...`); + await Vector.createCollection({ + vector: { dimension: 1536, metric: 'cosine' } + }); + } + const $meta = [1, ...Array(1535).fill(0)]; const start = Date.now(); for (let i = 0; i < 100; ++i) { From 13588e1ef07ef4c09378a63c05b908220e46f8aa Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Feb 2024 14:14:42 -0500 Subject: [PATCH 30/31] use correct script for astra vector setup --- benchmarks/benchmark-findone-mongoose-vector-astra.ts | 8 -------- benchmarks/setup-astra-vector-example.ts | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/benchmarks/benchmark-findone-mongoose-vector-astra.ts b/benchmarks/benchmark-findone-mongoose-vector-astra.ts index fbaa40a..10229fe 100644 --- a/benchmarks/benchmark-findone-mongoose-vector-astra.ts +++ b/benchmarks/benchmark-findone-mongoose-vector-astra.ts @@ -38,14 +38,6 @@ async function main() { process.env.ASTRA_COLLECTION_NAME ); - const collections = await mongoose.connection.listCollections(); - if (!collections.find(({ name }) => name === process.env.ASTRA_COLLECTION_NAME)) { - console.log(`Collection ${process.env.ASTRA_COLLECTION_NAME} not found, creating...`); - await Vector.createCollection({ - vector: { dimension: 1536, metric: 'cosine' } - }); - } - const $meta = [1, ...Array(1535).fill(0)]; const start = Date.now(); for (let i = 0; i < 100; ++i) { diff --git a/benchmarks/setup-astra-vector-example.ts b/benchmarks/setup-astra-vector-example.ts index 17bffbd..c511f16 100644 --- a/benchmarks/setup-astra-vector-example.ts +++ b/benchmarks/setup-astra-vector-example.ts @@ -36,7 +36,7 @@ async function main() { required: true } }, { - collectionOptions: { vector: { size: 1536, function: 'cosine' } }, + collectionOptions: { vector: { dimension: 1536, metric: 'cosine' } }, autoCreate: false }), process.env.ASTRA_COLLECTION_NAME From 5ea1ce6c7bbe02d81194165a459e16a973b4e414 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Feb 2024 15:09:57 -0500 Subject: [PATCH 31/31] add pinecone benchmarks and quick fixes for Astra benchmarks --- benchmarks/README.md | 22 +++++ benchmarks/benchmark-findone-pinecone.ts | 36 ++++++++ benchmarks/setup-astra-vector-example.ts | 96 +++++++++++---------- benchmarks/setup-pinecone-vector-example.ts | 38 ++++++++ benchmarks/upload-results.ts | 18 ++-- 5 files changed, 154 insertions(+), 56 deletions(-) create mode 100644 benchmarks/benchmark-findone-pinecone.ts create mode 100644 benchmarks/setup-pinecone-vector-example.ts diff --git a/benchmarks/README.md b/benchmarks/README.md index 88d5a35..edaf405 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -70,4 +70,26 @@ $ env ASTRA_CONNECTION_STRING="https://abc-us-east-2.apps.astra.datastax.com/... "name": "benchmark-findone-mongoose-vector-astra", "totalTimeMS": 20436 } +``` + +## Pinecone Benchmarks + +First, install the Pinecone client: + +``` +npm install @pinecone-database/pinecone --no-save +``` + +Then set up sample data: + +``` +``` +env PINECONE_INDEX=your-index-here env PINECONE_API_KEY=your-api-key-here ./node_modules/.bin/ts-node benchmarks/setup-pinecone-vector-example.ts +``` +``` + +Then run Pinecone benchmarks: + +``` +env PINECONE_INDEX=your-index-here env PINECONE_API_KEY=your-api-key-here ./node_modules/.bin/ts-node benchmarks/benchmark-findone-pinecone.ts ``` \ No newline at end of file diff --git a/benchmarks/benchmark-findone-pinecone.ts b/benchmarks/benchmark-findone-pinecone.ts new file mode 100644 index 0000000..6347d71 --- /dev/null +++ b/benchmarks/benchmark-findone-pinecone.ts @@ -0,0 +1,36 @@ +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + let Pinecone; + try { + Pinecone = require('@pinecone-database/pinecone').Pinecone; + } catch (err) { + console.log('{"name":"benchmark-findone-pinecone"}'); + return; + } + + if (!process.env.PINECONE_API_KEY || !process.env.PINECONE_INDEX) { + console.log('{"name":"benchmark-findone-pinecone"}'); + return; + } + + const pinecone = new Pinecone(); + + const vector = [1, ...Array(1535).fill(0)]; + const index = process.env.PINECONE_INDEX; + const start = Date.now(); + for (let i = 0; i < 100; ++i) { + await pinecone.index(index).query({ topK: 1, vector, includeValues: true }); + } + const results = { + name: 'benchmark-findone-pinecone', + totalTimeMS: Date.now() - start + }; + console.log(JSON.stringify(results, null, ' ')); +} diff --git a/benchmarks/setup-astra-vector-example.ts b/benchmarks/setup-astra-vector-example.ts index c511f16..915517d 100644 --- a/benchmarks/setup-astra-vector-example.ts +++ b/benchmarks/setup-astra-vector-example.ts @@ -15,57 +15,59 @@ main().then( ); async function main() { - if (!process.env.ASTRA_CONNECTION_STRING) { - throw new Error('Must set ASTRA_CONNECTION_STRING'); - } - if (!process.env.ASTRA_COLLECTION_NAME) { - throw new Error('Must set ASTRA_COLLECTION_NAME'); -} - await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { - isAstra: true - } as mongoose.ConnectOptions); + if (!process.env.ASTRA_CONNECTION_STRING) { + throw new Error('Must set ASTRA_CONNECTION_STRING'); + } + if (!process.env.ASTRA_COLLECTION_NAME) { + throw new Error('Must set ASTRA_COLLECTION_NAME'); + } + await mongoose.connect(process.env.ASTRA_CONNECTION_STRING, { + isAstra: true, + useHTTP2: false + } as mongoose.ConnectOptions); - const Vector = mongoose.model( - 'Vector', - new mongoose.Schema({ - $vector: { - type: [Number] - }, - prompt: { - type: String, - required: true - } - }, { - collectionOptions: { vector: { dimension: 1536, metric: 'cosine' } }, - autoCreate: false - }), - process.env.ASTRA_COLLECTION_NAME - ); + const Vector = mongoose.model( + 'Vector', + new mongoose.Schema({ + $vector: { + type: [Number] + }, + prompt: { + type: String, + required: true + } + }, { + collectionOptions: { vector: { dimension: 1536, metric: 'cosine' } }, + autoCreate: false + }), + process.env.ASTRA_COLLECTION_NAME + ); - console.log('Recreating collection...'); - await Vector.db.dropCollection(process.env.ASTRA_COLLECTION_NAME); - await Vector.createCollection(); + console.log('Recreating collection...'); + await Vector.db.dropCollection(process.env.ASTRA_COLLECTION_NAME); + await Vector.createCollection(); - console.log('Creating vectors...'); - const numVectors = 1000; - for (let i = 0; i < numVectors; ++i) { - console.log(`${i} / ${numVectors}`); - const $vector = Array(1536).fill(0); - $vector[i] = 1; - for (let j = 0; j < 3; ++j) { - try { - await Vector.create({ $vector, prompt: `Test ${i}` }); - break; - } catch (err) { - if (j >= 2) { - throw err; - } - await new Promise(resolve => setTimeout(resolve, 100)); + console.log('Creating vectors...'); + const numVectors = 1000; + const start = Date.now(); + for (let i = 0; i < numVectors; ++i) { + console.log(`${i} / ${numVectors}`); + const $vector = Array(1536).fill(0); + $vector[i] = 1; + for (let j = 0; j < 3; ++j) { + try { + await Vector.create({ $vector, prompt: `Test ${i}` }); + break; + } catch (err) { + if (j >= 2) { + throw err; + } + await new Promise(resolve => setTimeout(resolve, 100)); + } } - } - } + } - console.log('Done'); - process.exit(0); + console.log('Done', Date.now() - start); + process.exit(0); } \ No newline at end of file diff --git a/benchmarks/setup-pinecone-vector-example.ts b/benchmarks/setup-pinecone-vector-example.ts new file mode 100644 index 0000000..205df89 --- /dev/null +++ b/benchmarks/setup-pinecone-vector-example.ts @@ -0,0 +1,38 @@ +import { Pinecone } from '@pinecone-database/pinecone'; + +main().then( + () => { process.exit(0); }, + err => { + console.error(err); + process.exit(-1); + } +); + +async function main() { + if (!process.env.PINECONE_API_KEY || !process.env.PINECONE_INDEX) { + throw new Error('Cannot run pinecone setup without an API key'); + } + + const pinecone = new Pinecone(); + + console.log('Creating vectors...'); + + const numVectors = 1000; + const index = process.env.PINECONE_INDEX; + const start = Date.now(); + for (let i = 0; i < numVectors; ++i) { + console.log(`${i} / ${numVectors}`); + const values: number[] = Array(1536).fill(0); + values[i] = 1; + await pinecone.index(index).upsert([{ + id: '' + i, + values, + metadata: { + prompt: `Test ${i}` + } + }]); + } + + console.log('Done', Date.now() - start); + process.exit(0); +} \ No newline at end of file diff --git a/benchmarks/upload-results.ts b/benchmarks/upload-results.ts index 22cf03c..282bb32 100644 --- a/benchmarks/upload-results.ts +++ b/benchmarks/upload-results.ts @@ -31,15 +31,15 @@ async function main() { 'BenchmarkResult', new mongoose.Schema({ githash: { - type: String, - required: true + type: String, + required: true }, name: { - type: String, - required: true + type: String, + required: true }, totalTimeMS: { - type: Number + type: Number } }, { autoCreate: true, timestamps: true }) ); @@ -53,12 +53,12 @@ async function main() { fs.readFileSync(path.join('.', 'benchmarks', file), 'utf8') ); if (totalTimeMS == null) { - continue; + continue; } const doc = await BenchmarkResult.findOneAndUpdate( - { githash, name }, - { totalTimeMS }, - { upsert: true, returnDocument: 'after' } + { githash, name }, + { totalTimeMS }, + { upsert: true, returnDocument: 'after' } ); console.log(doc); }