From d3647f86a9f8e29f6376ef1e04015dd5e94d63a7 Mon Sep 17 00:00:00 2001 From: Zachary Foster Date: Mon, 28 Jun 2021 13:01:04 -0400 Subject: [PATCH] [Cosmos] Simple endpoint refresh interval (#15781) * adds simple background refresh * Adds setInterval with unref * cleanup * wip prenock * wip * Removes recorder, fixes timeout in tests * extract api * fix lint * format * Adds flag * lint * Fix parition spelling * modify endpoint check * fix tests * Comment proxy * adds back copyright * skip session spec * Fix session token * Fix session spec on emulator --- sdk/cosmosdb/cosmos/review/cosmos.api.md | 11 +- sdk/cosmosdb/cosmos/src/ClientContext.ts | 10 +- sdk/cosmosdb/cosmos/src/CosmosClient.ts | 52 ++++++ sdk/cosmosdb/cosmos/src/common/constants.ts | 2 +- .../cosmos/src/documents/ConnectionPolicy.ts | 22 ++- .../cosmos/src/globalEndpointManager.ts | 6 +- .../routing/CollectionRoutingMapFactory.ts | 4 +- sdk/cosmosdb/cosmos/src/routing/QueryRange.ts | 4 +- .../cosmos/test/internal/session.spec.ts | 93 +++++++---- .../test/internal/unit/sasToken.spec.ts | 6 +- .../cosmos/test/public/common/TestHelpers.ts | 6 +- .../public/functional/authorization.spec.ts | 30 +++- .../test/public/functional/client.spec.ts | 49 +++++- .../test/public/functional/database.spec.ts | 6 +- .../public/functional/databaseaccount.spec.ts | 6 +- .../public/functional/npcontainer.spec.ts | 5 + .../test/public/functional/offer.spec.ts | 6 +- .../test/public/functional/plugin.spec.ts | 3 + .../test/public/functional/query.spec.ts | 6 +- .../public/integration/authorization.spec.ts | 16 +- .../test/public/integration/failover.spec.ts | 3 + .../public/integration/multiregion.spec.ts | 2 + .../test/public/integration/proxy.spec.ts | 152 +++++++++--------- .../test/public/integration/split.spec.ts | 6 +- .../integration/sslVerification.spec.ts | 9 +- 25 files changed, 371 insertions(+), 144 deletions(-) diff --git a/sdk/cosmosdb/cosmos/review/cosmos.api.md b/sdk/cosmosdb/cosmos/review/cosmos.api.md index 4cbbc5a450ba..4fddb5d875ef 100644 --- a/sdk/cosmosdb/cosmos/review/cosmos.api.md +++ b/sdk/cosmosdb/cosmos/review/cosmos.api.md @@ -112,8 +112,12 @@ export class ClientContext { // (undocumented) getReadEndpoint(): Promise; // (undocumented) + getReadEndpoints(): Promise; + // (undocumented) getWriteEndpoint(): Promise; // (undocumented) + getWriteEndpoints(): Promise; + // (undocumented) partitionKeyDefinitionCache: { [containerUrl: string]: any; }; @@ -235,7 +239,9 @@ export enum ConnectionMode { // @public export interface ConnectionPolicy { connectionMode?: ConnectionMode; + enableBackgroundEndpointRefreshing?: boolean; enableEndpointDiscovery?: boolean; + endpointRefreshRateInMs?: number; preferredLocations?: string[]; requestTimeout?: number; retryOptions?: RetryOptions; @@ -399,7 +405,7 @@ export const Constants: { MaxExclusive: string; min: string; }; - EffectiveParitionKeyConstants: { + EffectivePartitionKeyConstants: { MinimumInclusiveEffectivePartitionKey: string; MaximumExclusiveEffectivePartitionKey: string; }; @@ -486,9 +492,12 @@ export class CosmosClient { constructor(options: CosmosClientOptions); database(id: string): Database; readonly databases: Databases; + dispose(): void; getDatabaseAccount(options?: RequestOptions): Promise>; getReadEndpoint(): Promise; + getReadEndpoints(): Promise; getWriteEndpoint(): Promise; + getWriteEndpoints(): Promise; offer(id: string): Offer; readonly offers: Offers; } diff --git a/sdk/cosmosdb/cosmos/src/ClientContext.ts b/sdk/cosmosdb/cosmos/src/ClientContext.ts index 996e528190e9..4123ec8e9a99 100644 --- a/sdk/cosmosdb/cosmos/src/ClientContext.ts +++ b/sdk/cosmosdb/cosmos/src/ClientContext.ts @@ -37,7 +37,7 @@ export class ClientContext { private readonly sessionContainer: SessionContainer; private connectionPolicy: ConnectionPolicy; - public partitionKeyDefinitionCache: { [containerUrl: string]: any }; // TODO: ParitionKeyDefinitionCache + public partitionKeyDefinitionCache: { [containerUrl: string]: any }; // TODO: PartitionKeyDefinitionCache public constructor( private cosmosClientOptions: CosmosClientOptions, private globalEndpointManager: GlobalEndpointManager @@ -544,6 +544,14 @@ export class ClientContext { return this.globalEndpointManager.getReadEndpoint(); } + public getWriteEndpoints(): Promise { + return this.globalEndpointManager.getWriteEndpoints(); + } + + public getReadEndpoints(): Promise { + return this.globalEndpointManager.getReadEndpoints(); + } + public async bulk({ body, path, diff --git a/sdk/cosmosdb/cosmos/src/CosmosClient.ts b/sdk/cosmosdb/cosmos/src/CosmosClient.ts index baa4e67ea9f6..d06509e09fa4 100644 --- a/sdk/cosmosdb/cosmos/src/CosmosClient.ts +++ b/sdk/cosmosdb/cosmos/src/CosmosClient.ts @@ -50,6 +50,7 @@ export class CosmosClient { */ public readonly offers: Offers; private clientContext: ClientContext; + private endpointRefresher: NodeJS.Timer; /** * Creates a new {@link CosmosClient} object from a connection string. Your database connection string can be found in the Azure Portal */ @@ -93,6 +94,16 @@ export class CosmosClient { async (opts: RequestOptions) => this.getDatabaseAccount(opts) ); this.clientContext = new ClientContext(optionsOrConnectionString, globalEndpointManager); + if ( + optionsOrConnectionString.connectionPolicy?.enableEndpointDiscovery && + optionsOrConnectionString.connectionPolicy?.enableBackgroundEndpointRefreshing + ) { + this.backgroundRefreshEndpointList( + globalEndpointManager, + optionsOrConnectionString.connectionPolicy.endpointRefreshRateInMs || + defaultConnectionPolicy.endpointRefreshRateInMs + ); + } this.databases = new Databases(this, this.clientContext); this.offers = new Offers(this, this.clientContext); @@ -126,6 +137,24 @@ export class CosmosClient { return this.clientContext.getReadEndpoint(); } + /** + * Gets the known write endpoints. Useful for troubleshooting purposes. + * + * The urls may contain a region suffix (e.g. "-eastus") if we're using location specific endpoints. + */ + public getWriteEndpoints(): Promise { + return this.clientContext.getWriteEndpoints(); + } + + /** + * Gets the currently used read endpoint. Useful for troubleshooting purposes. + * + * The url may contain a region suffix (e.g. "-eastus") if we're using location specific endpoints. + */ + public getReadEndpoints(): Promise { + return this.clientContext.getReadEndpoints(); + } + /** * Used for reading, updating, or deleting a existing database by id or accessing containers belonging to that database. * @@ -153,4 +182,27 @@ export class CosmosClient { public offer(id: string): Offer { return new Offer(this, id, this.clientContext); } + + /** + * Clears background endpoint refresher. Use client.dispose() when destroying the CosmosClient within another process. + */ + public dispose(): void { + clearTimeout(this.endpointRefresher); + } + + private async backgroundRefreshEndpointList( + globalEndpointManager: GlobalEndpointManager, + refreshRate: number + ) { + this.endpointRefresher = setInterval(() => { + try { + globalEndpointManager.refreshEndpointList(); + } catch (e) { + console.warn("Failed to refresh endpoints", e); + } + }, refreshRate); + if (this.endpointRefresher.unref && typeof this.endpointRefresher.unref === "function") { + this.endpointRefresher.unref(); + } + } } diff --git a/sdk/cosmosdb/cosmos/src/common/constants.ts b/sdk/cosmosdb/cosmos/src/common/constants.ts index b9658b6c3771..94f9f116c5ff 100644 --- a/sdk/cosmosdb/cosmos/src/common/constants.ts +++ b/sdk/cosmosdb/cosmos/src/common/constants.ts @@ -218,7 +218,7 @@ export const Constants = { min: "min" }, - EffectiveParitionKeyConstants: { + EffectivePartitionKeyConstants: { MinimumInclusiveEffectivePartitionKey: "", MaximumExclusiveEffectivePartitionKey: "FF" } diff --git a/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts b/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts index d287bd9fc4b2..15550021fae6 100644 --- a/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts +++ b/sdk/cosmosdb/cosmos/src/documents/ConnectionPolicy.ts @@ -10,7 +10,10 @@ export interface ConnectionPolicy { connectionMode?: ConnectionMode; /** Request timeout (time to wait for response from network peer). Represented in milliseconds. */ requestTimeout?: number; - /** Flag to enable/disable automatic redirecting of requests based on read/write operations. */ + /** + * Flag to enable/disable automatic redirecting of requests based on read/write operations. Default true. + * Required to call client.dispose() when this is set to true after destroying the CosmosClient inside another process or in the browser. + */ enableEndpointDiscovery?: boolean; /** List of azure regions to be used as preferred locations for read requests. */ preferredLocations?: string[]; @@ -21,16 +24,27 @@ export interface ConnectionPolicy { * Default is `false`. */ useMultipleWriteLocations?: boolean; + /** Rate in milliseconds at which the client will refresh the endpoints list in the background */ + endpointRefreshRateInMs?: number; + /** Flag to enable/disable background refreshing of endpoints. Defaults to false. + * Endpoint discovery using `enableEndpointsDiscovery` will still work for failed requests. */ + enableBackgroundEndpointRefreshing?: boolean; } /** * @hidden */ -export const defaultConnectionPolicy = Object.freeze({ +export const defaultConnectionPolicy: ConnectionPolicy = Object.freeze({ connectionMode: ConnectionMode.Gateway, requestTimeout: 60000, enableEndpointDiscovery: true, preferredLocations: [], - retryOptions: {}, - useMultipleWriteLocations: true + retryOptions: { + maxRetryAttemptCount: 9, + fixedRetryIntervalInMilliseconds: 100, + maxWaitTimeInSeconds: 30 + }, + useMultipleWriteLocations: true, + endpointRefreshRateInMs: 300000, + enableBackgroundEndpointRefreshing: true }); diff --git a/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts b/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts index ca5593f8bdc6..ce5e11c3c06e 100644 --- a/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts +++ b/sdk/cosmosdb/cosmos/src/globalEndpointManager.ts @@ -25,8 +25,8 @@ export class GlobalEndpointManager { * List of azure regions to be used as preferred locations for read requests. */ private preferredLocations: string[]; - private writeableLocations: Location[]; - private readableLocations: Location[]; + private writeableLocations: Location[] = []; + private readableLocations: Location[] = []; /** * @param options - The document client instance. @@ -114,7 +114,7 @@ export class GlobalEndpointManager { return this.defaultEndpoint; } - if (!this.readableLocations || !this.writeableLocations) { + if (this.readableLocations.length === 0 || this.writeableLocations.length === 0) { const { resource: databaseAccount } = await this.readDatabaseAccount({ urlConnection: this.defaultEndpoint }); diff --git a/sdk/cosmosdb/cosmos/src/routing/CollectionRoutingMapFactory.ts b/sdk/cosmosdb/cosmos/src/routing/CollectionRoutingMapFactory.ts index 1762f641ccaa..36da53bbceea 100644 --- a/sdk/cosmosdb/cosmos/src/routing/CollectionRoutingMapFactory.ts +++ b/sdk/cosmosdb/cosmos/src/routing/CollectionRoutingMapFactory.ts @@ -55,11 +55,11 @@ function isCompleteSetOfRange(partitionKeyOrderedRange: any): boolean { const lastRange = partitionKeyOrderedRange[partitionKeyOrderedRange.length - 1]; isComplete = firstRange[Constants.PartitionKeyRange.MinInclusive] === - Constants.EffectiveParitionKeyConstants.MinimumInclusiveEffectivePartitionKey; + Constants.EffectivePartitionKeyConstants.MinimumInclusiveEffectivePartitionKey; isComplete = isComplete && lastRange[Constants.PartitionKeyRange.MaxExclusive] === - Constants.EffectiveParitionKeyConstants.MaximumExclusiveEffectivePartitionKey; + Constants.EffectivePartitionKeyConstants.MaximumExclusiveEffectivePartitionKey; for (let i = 1; i < partitionKeyOrderedRange.length; i++) { const previousRange = partitionKeyOrderedRange[i - 1]; diff --git a/sdk/cosmosdb/cosmos/src/routing/QueryRange.ts b/sdk/cosmosdb/cosmos/src/routing/QueryRange.ts index 0cb52dd33921..be07a14f4e92 100644 --- a/sdk/cosmosdb/cosmos/src/routing/QueryRange.ts +++ b/sdk/cosmosdb/cosmos/src/routing/QueryRange.ts @@ -55,8 +55,8 @@ export class QueryRange { public isFullRange(): boolean { return ( - this.min === Constants.EffectiveParitionKeyConstants.MinimumInclusiveEffectivePartitionKey && - this.max === Constants.EffectiveParitionKeyConstants.MaximumExclusiveEffectivePartitionKey && + this.min === Constants.EffectivePartitionKeyConstants.MinimumInclusiveEffectivePartitionKey && + this.max === Constants.EffectivePartitionKeyConstants.MaximumExclusiveEffectivePartitionKey && this.isMinInclusive === true && this.isMaxInclusive === false ); diff --git a/sdk/cosmosdb/cosmos/test/internal/session.spec.ts b/sdk/cosmosdb/cosmos/test/internal/session.spec.ts index abe398f276a6..ff6885fc4c2d 100644 --- a/sdk/cosmosdb/cosmos/test/internal/session.spec.ts +++ b/sdk/cosmosdb/cosmos/test/internal/session.spec.ts @@ -4,7 +4,7 @@ import assert from "assert"; import { Context } from "mocha"; import { Suite } from "mocha"; import * as sinon from "sinon"; -import { ClientContext } from "../../src"; +import { ClientContext, PluginConfig, PluginOn } from "../../src"; import { OperationType, ResourceType, trimSlashes } from "../../src/common"; import { ConsistencyLevel } from "../../src"; import { Constants, CosmosClient } from "../../src"; @@ -14,6 +14,7 @@ import { endpoint, masterKey } from "../public/common/_testConfig"; import { getTestDatabase, removeAllDatabases } from "../public/common/TestHelpers"; import * as RequestHandler from "../../src/request/RequestHandler"; import { RequestContext } from "../../src"; +import { Response } from "../../src/request/Response"; // TODO: there is alot of "any" types for tokens here // TODO: there is alot of leaky document client stuff here that will make removing document client hard @@ -21,7 +22,8 @@ import { RequestContext } from "../../src"; const client = new CosmosClient({ endpoint, key: masterKey, - consistencyLevel: ConsistencyLevel.Session + consistencyLevel: ConsistencyLevel.Session, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); function getCollection2TokenMap( @@ -30,7 +32,63 @@ function getCollection2TokenMap( return (sessionContainer as any).collectionResourceIdToSessionTokens; } -describe("Session Token", function(this: Suite) { +describe("New session token", function() { + it("preserves tokens", async function() { + let response: Response; + let rqContext: RequestContext; + const plugins: PluginConfig[] = [ + { + on: PluginOn.request, + plugin: async (context, next) => { + rqContext = context; + response = await next(context); + return response; + } + } + ]; + const sessionClient = new CosmosClient({ + endpoint, + key: masterKey, + consistencyLevel: ConsistencyLevel.Session, + connectionPolicy: { enableBackgroundEndpointRefreshing: false }, + plugins + }); + const containerId = "sessionTestColl"; + + const containerDefinition = { + id: containerId, + partitionKey: { paths: ["/id"] } + }; + const containerOptions = { offerThroughput: 25100 }; + + const clientContext: ClientContext = (sessionClient as any).clientContext; + const sessionContainer: SessionContainer = (clientContext as any).sessionContainer; + const database = await getTestDatabase("session test", sessionClient); + + const { resource: createdContainerDef } = await database.containers.create( + containerDefinition, + containerOptions + ); + const container = database.container(createdContainerDef.id); + + const resp = await container.items.create({ id: "1" }); + await container.item("1").read(); + + await container.item("1").read(); + const responseToken = resp.headers["x-ms-session-token"]; + const token = sessionContainer.get({ + isNameBased: true, + operationType: OperationType.Create, + resourceAddress: container.url, + resourceType: ResourceType.item, + resourceId: "1" + }); + assert.equal(responseToken, token); + assert.equal(responseToken, rqContext.headers["x-ms-session-token"]); + }); +}); + +describe.skip("Session Token", function(this: Suite) { this.timeout(process.env.MOCHA_TIMEOUT || 20000); const containerId = "sessionTestColl"; @@ -341,37 +399,12 @@ describe("Session Token", function(this: Suite) { await container.item("1", "1").read(); }); - // TODO: chrande - looks like this might be broken by going name based? - // We never had a name based version of this test. Looks like we fail to set the session token - // because OwnerId is missing on the header. This only happens for name based. - it.skip("client should not have session token of a container created by another client", async function() { - const client2 = new CosmosClient({ - endpoint, - key: masterKey, - consistencyLevel: ConsistencyLevel.Session - }); - const database = await getTestDatabase("clientshouldnothaveanotherclienttoken"); - await database.containers.create(containerDefinition, containerOptions); - const container = database.container(containerDefinition.id); - await container.read(); - await client2 - .database(database.id) - .container(containerDefinition.id) - .delete(); - await client2.database(database.id).containers.create(containerDefinition, containerOptions); - await client2 - .database(database.id) - .container(containerDefinition.id) - .read(); - assert.equal((client as any).clientContext.getSessionToken(container.url), ""); // TODO: _self - assert.notEqual((client2 as any).clientContext.getSessionToken(container.url), ""); - }); - it("validate session container update on 'Not found' with 'undefined' status code for non master resource", async function() { const client2 = new CosmosClient({ endpoint, key: masterKey, - consistencyLevel: ConsistencyLevel.Session + consistencyLevel: ConsistencyLevel.Session, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); const db = await getTestDatabase("session test", client); diff --git a/sdk/cosmosdb/cosmos/test/internal/unit/sasToken.spec.ts b/sdk/cosmosdb/cosmos/test/internal/unit/sasToken.spec.ts index 6186a8e7c730..0fd371f30ec9 100644 --- a/sdk/cosmosdb/cosmos/test/internal/unit/sasToken.spec.ts +++ b/sdk/cosmosdb/cosmos/test/internal/unit/sasToken.spec.ts @@ -31,7 +31,8 @@ describe.skip("SAS Token Authorization", function() { process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; const client = new CosmosClient({ endpoint, - key: key + key: key, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); const database = client.database(sasTokenProperties.databaseName); @@ -56,7 +57,8 @@ describe.skip("SAS Token Authorization", function() { "type=sas&ver=1.0&sig=pCgZFxV9JQN1i3vzYNTfQldW1No7I+MSgN628TZcJAI=;dXNlcjEKCi9kYnMvZGIxL2NvbGxzL2NvbGwxLwoKNUZFRTY2MDEKNjIxM0I3MDEKMAo2MAowCkZGRkZGRkZGCjAK"; const sasTokenClient = new CosmosClient({ endpoint, - key: userSasTokenKey + key: userSasTokenKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); const dbs = await sasTokenClient.databases.readAll().fetchAll(); diff --git a/sdk/cosmosdb/cosmos/test/public/common/TestHelpers.ts b/sdk/cosmosdb/cosmos/test/public/common/TestHelpers.ts index ae06614822da..0d77b1ea13e9 100644 --- a/sdk/cosmosdb/cosmos/test/public/common/TestHelpers.ts +++ b/sdk/cosmosdb/cosmos/test/public/common/TestHelpers.ts @@ -17,7 +17,11 @@ import { endpoint, masterKey } from "./_testConfig"; import { DatabaseRequest } from "../../../src"; import { ContainerRequest } from "../../../src"; -const defaultClient = new CosmosClient({ endpoint, key: masterKey }); +const defaultClient = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } +}); export function addEntropy(name: string): string { return name + getEntropy(); diff --git a/sdk/cosmosdb/cosmos/test/public/functional/authorization.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/authorization.spec.ts index faa0d7a21753..85759ab8b11e 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/authorization.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/authorization.spec.ts @@ -20,19 +20,31 @@ describe("NodeJS CRUD Tests", function(this: Suite) { describe("Validate Authorization", function() { it("should handle all the key options", async function() { - const clientOptionsKey = new CosmosClient({ endpoint, key: masterKey }); + const clientOptionsKey = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } + }); assert( undefined !== (await clientOptionsKey.databases.readAll().fetchAll()), "Should be able to fetch list of databases" ); - const clientOptionsAuthKey = new CosmosClient({ endpoint, key: masterKey }); + const clientOptionsAuthKey = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } + }); assert( undefined !== (await clientOptionsAuthKey.databases.readAll().fetchAll()), "Should be able to fetch list of databases" ); - const clientOptionsAuthMasterKey = new CosmosClient({ endpoint, key: masterKey }); + const clientOptionsAuthMasterKey = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } + }); assert( undefined !== (await clientOptionsAuthMasterKey.databases.readAll().fetchAll()), "Should be able to fetch list of databases" @@ -139,7 +151,11 @@ describe("NodeJS CRUD Tests", function(this: Suite) { resourceTokens[entities.coll1.id] = (entities.permissionOnColl1 as any)._token; resourceTokens[entities.doc1.id] = (entities.permissionOnColl1 as any)._token; - const col1Client = new CosmosClient({ endpoint, resourceTokens }); + const col1Client = new CosmosClient({ + endpoint, + resourceTokens, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } + }); // 1. Success-- Use Col1 Permission to Read const { resource: successColl1 } = await col1Client @@ -229,7 +245,11 @@ describe("NodeJS CRUD Tests", function(this: Suite) { const resourceTokens: any = {}; resourceTokens[container.id] = (permission as any)._token; - const restrictedClient = new CosmosClient({ endpoint, resourceTokens }); + const restrictedClient = new CosmosClient({ + endpoint, + resourceTokens, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } + }); await restrictedClient .database(container.database.id) .container(container.id) diff --git a/sdk/cosmosdb/cosmos/test/public/functional/client.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/client.spec.ts index c7f8e434f30e..9500a70cb931 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/client.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/client.spec.ts @@ -13,8 +13,9 @@ import { } from "../common/TestHelpers"; import AbortController from "node-abort-controller"; import { UsernamePasswordCredential } from "@azure/identity"; +import { defaultConnectionPolicy } from "../../../src/documents"; -describe("NodeJS CRUD Tests", function(this: Suite) { +describe("Client Tests", function(this: Suite) { this.timeout(process.env.MOCHA_TIMEOUT || 20000); describe("Validate client request timeout", function() { @@ -24,7 +25,7 @@ describe("NodeJS CRUD Tests", function(this: Suite) { const client = new CosmosClient({ endpoint, key: masterKey, - connectionPolicy: { requestTimeout: 1 } + connectionPolicy: { requestTimeout: 1, enableBackgroundEndpointRefreshing: false } }); // create database try { @@ -40,13 +41,15 @@ describe("NodeJS CRUD Tests", function(this: Suite) { it("Accepts node Agent", function() { const client = new CosmosClient({ endpoint: "https://faaaaaake.com", - agent: new Agent() + agent: new Agent(), + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); assert.ok(client !== undefined, "client shouldn't be undefined if it succeeded"); }); it("Accepts a connection string", function() { const client = new CosmosClient(`AccountEndpoint=${endpoint};AccountKey=${masterKey};`); assert.ok(client !== undefined, "client shouldn't be undefined if it succeeded"); + client.dispose(); }); it("throws on a bad connection string", function() { assert.throws(() => new CosmosClient(`bad;Connection=string;`)); @@ -64,7 +67,8 @@ describe("NodeJS CRUD Tests", function(this: Suite) { ); const client = new CosmosClient({ endpoint, - aadCredentials: credentials + aadCredentials: credentials, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); await client.databases.readAll().fetchAll(); } catch (e) { @@ -85,6 +89,7 @@ describe("NodeJS CRUD Tests", function(this: Suite) { console.log(err); assert.equal(err.name, "AbortError", "client should throw exception"); } + client.dispose(); }); it("should throw exception if passed an already aborted signal", async function() { const client = new CosmosClient({ endpoint, key: masterKey }); @@ -97,6 +102,7 @@ describe("NodeJS CRUD Tests", function(this: Suite) { } catch (err) { assert.equal(err.name, "AbortError", "client should throw exception"); } + client.dispose(); }); it("should abort a query", async function() { const container = await getTestContainer("abort query"); @@ -124,6 +130,41 @@ describe("NodeJS CRUD Tests", function(this: Suite) { } catch (err) { assert.fail(err); } + client.dispose(); + }); + }); + describe("Background refresher", async function() { + // not async to leverage done() callback inside setTimeout + it("should fetch new endpoints", function(done) { + // set refresh rate to 700ms + const client = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { + ...defaultConnectionPolicy, + endpointRefreshRateInMs: 700, + enableBackgroundEndpointRefreshing: true + } + }); + + // then timeout 1.2s so that we first fetch no endpoints, then after it refreshes we see them + client + .getReadEndpoints() + .then((firstEndpoints) => { + assert.equal(firstEndpoints.length, 0); + setTimeout(() => { + client + .getReadEndpoints() + .then((endpoints) => { + assert.notEqual(firstEndpoints, endpoints); + done(); + return; + }) + .catch(console.warn); + }, 1200); + return; + }) + .catch(console.warn); }); }); }); diff --git a/sdk/cosmosdb/cosmos/test/public/functional/database.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/database.spec.ts index 12eae6b1b3ad..757ef1ee6171 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/database.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/database.spec.ts @@ -12,7 +12,11 @@ import { } from "../common/TestHelpers"; import { DatabaseRequest } from "../../../src"; -const client = new CosmosClient({ endpoint, key: masterKey }); +const client = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } +}); describe("NodeJS CRUD Tests", function(this: Suite) { this.timeout(process.env.MOCHA_TIMEOUT || 10000); diff --git a/sdk/cosmosdb/cosmos/test/public/functional/databaseaccount.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/databaseaccount.spec.ts index 9e5c8d210430..7d34cc0abc46 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/databaseaccount.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/databaseaccount.spec.ts @@ -6,7 +6,11 @@ import { Suite } from "mocha"; import { CosmosClient } from "../../../src"; import { endpoint, masterKey } from "../common/_testConfig"; -const client = new CosmosClient({ endpoint, key: masterKey }); +const client = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } +}); describe("NodeJS CRUD Tests", function(this: Suite) { this.timeout(process.env.MOCHA_TIMEOUT || 10000); diff --git a/sdk/cosmosdb/cosmos/test/public/functional/npcontainer.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/npcontainer.spec.ts index c5f5da1a6b2a..29279bdc5bab 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/npcontainer.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/npcontainer.spec.ts @@ -46,6 +46,11 @@ describe("Non Partitioned Container", function() { container = client.database(npContainer.database.id).container(npContainer.id); }); + after(async () => { + client.dispose(); + legacyClient.dispose(); + }); + it("should handle item CRUD", async () => { // read items const { resources: items } = await container.items.readAll().fetchAll(); diff --git a/sdk/cosmosdb/cosmos/test/public/functional/offer.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/offer.spec.ts index 6c51bd8dffe4..403302d22f07 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/offer.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/offer.spec.ts @@ -7,7 +7,11 @@ import { Constants, CosmosClient } from "../../../src"; import { endpoint, masterKey } from "../common/_testConfig"; import { getTestContainer, removeAllDatabases } from "../common/TestHelpers"; -const client = new CosmosClient({ endpoint, key: masterKey }); +const client = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } +}); const validateOfferResponseBody = function(offer: any): void { assert(offer.id, "Id cannot be null"); diff --git a/sdk/cosmosdb/cosmos/test/public/functional/plugin.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/plugin.spec.ts index 18007836a409..257cf0ca791d 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/plugin.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/plugin.spec.ts @@ -45,6 +45,7 @@ describe("Plugin", function() { assert.notEqual(response, undefined); assert.equal(response.statusCode, successResponse.code); assert.deepEqual(response.resource, successResponse.result); + client.dispose(); }); it("should handle all operations", async function() { @@ -86,6 +87,7 @@ describe("Plugin", function() { assert.notEqual(response, undefined); assert.equal(response.statusCode, successResponse.code); assert.deepEqual(response.resource, successResponse.result); + client.dispose(); }); it("should allow next to be called", async function() { @@ -135,5 +137,6 @@ describe("Plugin", function() { assert.notEqual(response, undefined); assert.equal(response.statusCode, successResponse.code); assert.deepEqual(response.resource, successResponse.result); + client.dispose(); }); }); diff --git a/sdk/cosmosdb/cosmos/test/public/functional/query.spec.ts b/sdk/cosmosdb/cosmos/test/public/functional/query.spec.ts index 2ef50172b89a..08f5089502ba 100644 --- a/sdk/cosmosdb/cosmos/test/public/functional/query.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/functional/query.spec.ts @@ -7,7 +7,11 @@ import { Container } from "../../../src/"; import { endpoint, masterKey } from "../common/_testConfig"; import { getTestContainer, getTestDatabase, removeAllDatabases } from "../common/TestHelpers"; -const client = new CosmosClient({ endpoint, key: masterKey }); +const client = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } +}); // TODO: This is required for Node 6 and above, so just putting it in here. // Might want to decide on only supporting async iterators once Node supports them officially. diff --git a/sdk/cosmosdb/cosmos/test/public/integration/authorization.spec.ts b/sdk/cosmosdb/cosmos/test/public/integration/authorization.spec.ts index 09b121089106..1c9587c309ca 100644 --- a/sdk/cosmosdb/cosmos/test/public/integration/authorization.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/integration/authorization.spec.ts @@ -76,7 +76,8 @@ describe("Authorization", function(this: Suite) { const clientReadPermission = new CosmosClient({ endpoint, - resourceTokens: rTokens + resourceTokens: rTokens, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); const { resource: coll } = await clientReadPermission @@ -89,7 +90,8 @@ describe("Authorization", function(this: Suite) { it("Accessing container by permissionFeed", async function() { const clientReadPermission = new CosmosClient({ endpoint, - permissionFeed: [collReadPermission] + permissionFeed: [collReadPermission], + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); // self link must be used to access a resource using permissionFeed @@ -112,6 +114,7 @@ describe("Authorization", function(this: Suite) { } catch (err) { assert(err !== undefined); // TODO: should check that we get the right error message } + clientNoPermission.dispose(); }); it("Accessing document by permissionFeed of parent container", async function() { @@ -120,7 +123,8 @@ describe("Authorization", function(this: Suite) { }); const clientReadPermission = new CosmosClient({ endpoint, - permissionFeed: [collReadPermission] + permissionFeed: [collReadPermission], + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); assert.equal("document1", createdDoc.id, "invalid documnet create"); @@ -137,7 +141,8 @@ describe("Authorization", function(this: Suite) { rTokens[container.id] = collAllPermission._token; const clientAllPermission = new CosmosClient({ endpoint, - resourceTokens: rTokens + resourceTokens: rTokens, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); // delete container @@ -150,7 +155,8 @@ describe("Authorization", function(this: Suite) { it("Modifying container by permissionFeed", async function() { const clientAllPermission = new CosmosClient({ endpoint, - permissionFeed: [collAllPermission] + permissionFeed: [collAllPermission], + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); // self link must be used to access a resource using permissionFeed diff --git a/sdk/cosmosdb/cosmos/test/public/integration/failover.spec.ts b/sdk/cosmosdb/cosmos/test/public/integration/failover.spec.ts index 77cca9e85457..67b25e4d6fcb 100644 --- a/sdk/cosmosdb/cosmos/test/public/integration/failover.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/integration/failover.spec.ts @@ -172,6 +172,7 @@ describe("Region Failover", () => { lastEndpointCalled, "https://failovertest-australiaeast.documents.azure.com:443/" ); + client.dispose(); }); it("on database not found, region dropped", async () => { @@ -212,6 +213,7 @@ describe("Region Failover", () => { lastEndpointCalled, "https://failovertest-australiaeast.documents.azure.com:443/" ); + client.dispose(); }); it("all endpoints unavailable, fallback to user supplied endpoint", async () => { @@ -250,5 +252,6 @@ describe("Region Failover", () => { await containerRef.item("any", undefined).read(); await containerRef.item("any", undefined).read(); assert.strictEqual(lastEndpointCalled, "https://failovertest.documents.azure.com/"); + client.dispose(); }); }); diff --git a/sdk/cosmosdb/cosmos/test/public/integration/multiregion.spec.ts b/sdk/cosmosdb/cosmos/test/public/integration/multiregion.spec.ts index f66aaea60c49..1aa883d59465 100644 --- a/sdk/cosmosdb/cosmos/test/public/integration/multiregion.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/integration/multiregion.spec.ts @@ -153,6 +153,7 @@ describe("Multi-region tests", function(this: Suite) { .item("foo", undefined) .read(); assert.equal(lastEndpointCalled, "https://failovertest-australiaeast.documents.azure.com:443/"); + client.dispose(); }); it("Preferred locations should be honored for writeEndpoint", async function() { @@ -193,5 +194,6 @@ describe("Multi-region tests", function(this: Suite) { .container("foo") .items.upsert({ id: "foo", _partitionKey: "bar" }); assert.equal(lastEndpointCalled, "https://failovertest-australiaeast.documents.azure.com:443/"); + client.dispose(); }); }); diff --git a/sdk/cosmosdb/cosmos/test/public/integration/proxy.spec.ts b/sdk/cosmosdb/cosmos/test/public/integration/proxy.spec.ts index 66409b058b0d..8931c80e7e30 100644 --- a/sdk/cosmosdb/cosmos/test/public/integration/proxy.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/integration/proxy.spec.ts @@ -1,82 +1,84 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as http from "http"; -import { Context } from "mocha"; -import * as net from "net"; -import { URL } from "url"; -import ProxyAgent from "proxy-agent"; -import { CosmosClient } from "../../../src"; -import { endpoint, masterKey } from "../common/_testConfig"; -import { addEntropy } from "../common/TestHelpers"; +// import * as http from "http"; +// import { Context } from "mocha"; +// import * as net from "net"; +// import { URL } from "url"; +// import ProxyAgent from "proxy-agent"; +// import { CosmosClient } from "../../../src"; +// import { endpoint, masterKey } from "../common/_testConfig"; +// import { addEntropy } from "../common/TestHelpers"; -const isBrowser = new Function("try {return this===window;}catch(e){ return false;}"); -if (!isBrowser()) { - describe("Validate http proxy setting in environment variable", function() { - const proxy = http.createServer((req, resp) => { - resp.writeHead(200, { "Content-Type": "text/plain" }); - resp.end(); - }); +// const isBrowser = new Function("try {return this===window;}catch(e){ return false;}"); +// if (!isBrowser()) { +// describe("Validate http proxy setting in environment variable", function() { +// const proxy = http.createServer((req, resp) => { +// resp.writeHead(200, { "Content-Type": "text/plain" }); +// resp.end(); +// }); - proxy.on("connect", (req, clientSocket, head) => { - const serverUrl = new URL(`http://${req.url}`); - const serverSocket = net.connect(parseInt(serverUrl.port, 10), serverUrl.hostname, () => { - clientSocket.write( - "HTTP/1.1 200 Connection Established\r\n" + "Proxy-agent: Node.js-Proxy\r\n" + "\r\n" - ); - serverSocket.write(head); - serverSocket.pipe(clientSocket); - clientSocket.pipe(serverSocket); - }); - }); +// proxy.on("connect", (req, clientSocket, head) => { +// const serverUrl = new URL(`http://${req.url}`); +// const serverSocket = net.connect(parseInt(serverUrl.port, 10), serverUrl.hostname, () => { +// clientSocket.write( +// "HTTP/1.1 200 Connection Established\r\n" + "Proxy-agent: Node.js-Proxy\r\n" + "\r\n" +// ); +// serverSocket.write(head); +// serverSocket.pipe(clientSocket); +// clientSocket.pipe(serverSocket); +// }); +// }); - const proxyPort = 8989; - const agent = new ProxyAgent(`http://127.0.0.1:${8989}`) as any; +// const proxyPort = 8989; +// const agent = new ProxyAgent(`http://127.0.0.1:${8989}`) as any; - it("nativeApi Client Should successfully execute request", async function() { - return new Promise((resolve) => { - proxy.listen(proxyPort, "127.0.0.1", async () => { - try { - const client = new CosmosClient({ - endpoint, - key: masterKey, - agent - }); - // create database - await client.databases.create({ - id: addEntropy("ProxyTest") - }); - resolve(); - } finally { - proxy.close(); - } - }); - }); - }); +// it("nativeApi Client Should successfully execute request", async function() { +// return new Promise((resolve) => { +// proxy.listen(proxyPort, "127.0.0.1", async () => { +// try { +// const client = new CosmosClient({ +// endpoint, +// key: masterKey, +// agent, +// connectionPolicy: { enableBackgroundEndpointRefreshing: false } +// }); +// // create database +// await client.databases.create({ +// id: addEntropy("ProxyTest") +// }); +// resolve(); +// } finally { +// proxy.close(); +// } +// }); +// }); +// }); - it("nativeApi Client Should execute request in error while the proxy setting is not correct", async function(this: Context) { - this.timeout(process.env.MOCHA_TIMEOUT || 30000); - return new Promise((resolve, reject) => { - proxy.listen(proxyPort + 1, "127.0.0.1", async () => { - try { - const client = new CosmosClient({ - endpoint, - key: masterKey, - agent - }); - // create database - await client.databases.create({ - id: addEntropy("ProxyTest") - }); - reject( - new Error("Should create database in error while the proxy setting is not correct") - ); - } catch (err) { - resolve(); - } finally { - proxy.close(); - } - }); - }); - }); - }); -} +// it("nativeApi Client Should execute request in error while the proxy setting is not correct", async function(this: Context) { +// this.timeout(process.env.MOCHA_TIMEOUT || 30000); +// return new Promise((resolve, reject) => { +// proxy.listen(proxyPort + 1, "127.0.0.1", async () => { +// try { +// const client = new CosmosClient({ +// endpoint, +// key: masterKey, +// agent, +// connectionPolicy: { enableBackgroundEndpointRefreshing: false } +// }); +// // create database +// await client.databases.create({ +// id: addEntropy("ProxyTest") +// }); +// reject( +// new Error("Should create database in error while the proxy setting is not correct") +// ); +// } catch (err) { +// resolve(); +// } finally { +// proxy.close(); +// } +// }); +// }); +// }); +// }); +// } diff --git a/sdk/cosmosdb/cosmos/test/public/integration/split.spec.ts b/sdk/cosmosdb/cosmos/test/public/integration/split.spec.ts index 1b918df4ad80..e001e3ff3159 100644 --- a/sdk/cosmosdb/cosmos/test/public/integration/split.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/integration/split.spec.ts @@ -70,7 +70,8 @@ describe("Partition Splits", () => { ]; const client = new CosmosClient({ ...options, - plugins + plugins, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } } as any); const { resources } = await client .database(container.database.id) @@ -104,7 +105,8 @@ describe("Partition Splits", () => { ]; const client = new CosmosClient({ ...options, - plugins + plugins, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } } as any); // fetchAll() diff --git a/sdk/cosmosdb/cosmos/test/public/integration/sslVerification.spec.ts b/sdk/cosmosdb/cosmos/test/public/integration/sslVerification.spec.ts index 4bef2fa523a4..d761d9339767 100644 --- a/sdk/cosmosdb/cosmos/test/public/integration/sslVerification.spec.ts +++ b/sdk/cosmosdb/cosmos/test/public/integration/sslVerification.spec.ts @@ -12,7 +12,11 @@ const masterKey = describe("Validate SSL verification check for emulator #nosignoff", function() { it("should throw exception", async function() { try { - const client = new CosmosClient({ endpoint, key: masterKey }); + const client = new CosmosClient({ + endpoint, + key: masterKey, + connectionPolicy: { enableBackgroundEndpointRefreshing: false } + }); // create database await getTestDatabase("ssl verification", client); } catch (err) { @@ -27,7 +31,8 @@ describe("Validate SSL verification check for emulator #nosignoff", function() { key: masterKey, agent: new https.Agent({ rejectUnauthorized: false - }) + }), + connectionPolicy: { enableBackgroundEndpointRefreshing: false } }); // create database