UNPKG

@azure/cosmos

Version:
269 lines • 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GlobalEndpointManager = void 0; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. const index_js_1 = require("./common/index.js"); const constants_js_1 = require("./common/constants.js"); const CosmosDiagnostics_js_1 = require("./CosmosDiagnostics.js"); const diagnostics_js_1 = require("./utils/diagnostics.js"); const checkURL_js_1 = require("./utils/checkURL.js"); /** * @hidden * This internal class implements the logic for endpoint management for geo-replicated database accounts. */ class GlobalEndpointManager { /** * @param options - The document client instance. * @internal */ constructor(options, readDatabaseAccount) { this.readDatabaseAccount = readDatabaseAccount; this.writeableLocations = []; this.readableLocations = []; this.unavailableReadableLocations = []; this.unavailableWriteableLocations = []; this.options = options; this.defaultEndpoint = options.endpoint; this.enableEndpointDiscovery = options.connectionPolicy.enableEndpointDiscovery; this.isRefreshing = false; this.preferredLocations = this.options.connectionPolicy.preferredLocations; this.preferredLocationsCount = this.preferredLocations ? this.preferredLocations.length : 0; } /** * Gets the current read endpoint from the endpoint cache. */ async getReadEndpoint(diagnosticNode) { return this.resolveServiceEndpoint(diagnosticNode, index_js_1.ResourceType.item, index_js_1.OperationType.Read); } /** * Gets the current write endpoint from the endpoint cache. */ async getWriteEndpoint(diagnosticNode) { return this.resolveServiceEndpoint(diagnosticNode, index_js_1.ResourceType.item, index_js_1.OperationType.Replace); } async getReadEndpoints() { return this.readableLocations.map((loc) => loc.databaseAccountEndpoint); } async getWriteEndpoints() { return this.writeableLocations.map((loc) => loc.databaseAccountEndpoint); } /** * Gets the read locations from the endpoint cache. */ async getReadLocations() { return this.readableLocations; } async markCurrentLocationUnavailableForRead(diagnosticNode, endpoint) { await this.refreshEndpointList(diagnosticNode); const location = this.readableLocations.find((loc) => loc.databaseAccountEndpoint === endpoint); if (location) { location.unavailable = true; location.lastUnavailabilityTimestampInMs = Date.now(); this.unavailableReadableLocations.push(location); } } async markCurrentLocationUnavailableForWrite(diagnosticNode, endpoint) { await this.refreshEndpointList(diagnosticNode); const location = this.writeableLocations.find((loc) => loc.databaseAccountEndpoint === endpoint); if (location) { location.unavailable = true; location.lastUnavailabilityTimestampInMs = Date.now(); this.unavailableWriteableLocations.push(location); } } canUseMultipleWriteLocations(resourceType, operationType) { let canUse = this.options.connectionPolicy.useMultipleWriteLocations && this.enableMultipleWriteLocations; if (resourceType) { canUse = canUse && (resourceType === index_js_1.ResourceType.item || (resourceType === index_js_1.ResourceType.sproc && operationType === index_js_1.OperationType.Execute)); } return canUse; } async resolveServiceEndpoint(diagnosticNode, resourceType, operationType, startServiceEndpointIndex = 0) { // If endpoint discovery is disabled, always use the user provided endpoint if (!this.options.connectionPolicy.enableEndpointDiscovery) { diagnosticNode.addData({ readFromCache: true }, "default_endpoint"); diagnosticNode.recordEndpointResolution(this.defaultEndpoint); return this.defaultEndpoint; } // If getting the database account, always use the user provided endpoint if (resourceType === index_js_1.ResourceType.none) { diagnosticNode.addData({ readFromCache: true }, "none_resource"); diagnosticNode.recordEndpointResolution(this.defaultEndpoint); return this.defaultEndpoint; } if (this.readableLocations.length === 0 || this.writeableLocations.length === 0) { const resourceResponse = await (0, diagnostics_js_1.withMetadataDiagnostics)(async (metadataNode) => { return this.readDatabaseAccount(metadataNode, { urlConnection: this.defaultEndpoint, }); }, diagnosticNode, CosmosDiagnostics_js_1.MetadataLookUpType.DatabaseAccountLookUp); this.writeableLocations = resourceResponse.resource.writableLocations; this.readableLocations = resourceResponse.resource.readableLocations; this.enableMultipleWriteLocations = resourceResponse.resource.enableMultipleWritableLocations; } const locations = (0, index_js_1.isReadRequest)(operationType) ? this.readableLocations : this.writeableLocations; let location; // If we have preferred locations, try each one in order and use the first available one if (this.preferredLocations && this.preferredLocations.length > 0 && startServiceEndpointIndex < this.preferredLocations.length) { for (let i = startServiceEndpointIndex; i < this.preferredLocations.length; i++) { const preferredLocation = this.preferredLocations[i]; location = locations.find((loc) => loc.unavailable !== true && (0, checkURL_js_1.normalizeEndpoint)(loc.name) === (0, checkURL_js_1.normalizeEndpoint)(preferredLocation)); if (location) { break; } } } // If no preferred locations or one did not match, just grab the first one that is available if (!location) { const startIndexValid = startServiceEndpointIndex >= 0 && startServiceEndpointIndex < locations.length; const locationsToSearch = startIndexValid ? locations.slice(startServiceEndpointIndex) : locations; location = locationsToSearch.find((loc) => { return loc.unavailable !== true; }); } location = location ? location : { name: "", databaseAccountEndpoint: this.defaultEndpoint }; diagnosticNode.recordEndpointResolution(location.databaseAccountEndpoint); return location.databaseAccountEndpoint; } /** * Refreshes the endpoint list by clearning stale unavailability and then * retrieving the writable and readable locations from the geo-replicated database account * and then updating the locations cache. * We skip the refreshing if enableEndpointDiscovery is set to False */ async refreshEndpointList(diagnosticNode) { if (!this.isRefreshing && this.enableEndpointDiscovery) { this.isRefreshing = true; const databaseAccount = await this.getDatabaseAccountFromAnyEndpoint(diagnosticNode); if (databaseAccount) { this.refreshStaleUnavailableLocations(); this.refreshEndpoints(databaseAccount); } this.isRefreshing = false; } } refreshEndpoints(databaseAccount) { for (const location of databaseAccount.writableLocations) { const existingLocation = this.writeableLocations.find((loc) => loc.name === location.name); if (!existingLocation) { this.writeableLocations.push(location); } } for (const location of databaseAccount.readableLocations) { const existingLocation = this.readableLocations.find((loc) => loc.name === location.name); if (!existingLocation) { this.readableLocations.push(location); } } } refreshStaleUnavailableLocations() { const now = Date.now(); this.updateLocation(now, this.unavailableReadableLocations, this.readableLocations); this.unavailableReadableLocations = this.cleanUnavailableLocationList(now, this.unavailableReadableLocations); this.updateLocation(now, this.unavailableWriteableLocations, this.writeableLocations); this.unavailableWriteableLocations = this.cleanUnavailableLocationList(now, this.unavailableWriteableLocations); } /** * update the locationUnavailability to undefined if the location is available again * @param now - current time * @param unavailableLocations - list of unavailable locations * @param allLocations - list of all locations */ updateLocation(now, unavailableLocations, allLocations) { for (const location of unavailableLocations) { const unavaialableLocation = allLocations.find((loc) => loc.name === location.name); if (unavaialableLocation && now - unavaialableLocation.lastUnavailabilityTimestampInMs > constants_js_1.Constants.LocationUnavailableExpirationTimeInMs) { unavaialableLocation.unavailable = false; } } } cleanUnavailableLocationList(now, unavailableLocations) { return unavailableLocations.filter((loc) => { if (loc && now - loc.lastUnavailabilityTimestampInMs >= constants_js_1.Constants.LocationUnavailableExpirationTimeInMs) { return false; } return true; }); } /** * Gets the database account first by using the default endpoint, and if that doesn't returns * use the endpoints for the preferred locations in the order they are specified to get * the database account. */ async getDatabaseAccountFromAnyEndpoint(diagnosticNode) { try { const options = { urlConnection: this.defaultEndpoint }; const { resource: databaseAccount } = await this.readDatabaseAccount(diagnosticNode, options); return databaseAccount; // If for any reason(non - globaldb related), we are not able to get the database // account from the above call to readDatabaseAccount, // we would try to get this information from any of the preferred locations that the user // might have specified (by creating a locational endpoint) // and keeping eating the exception until we get the database account and return None at the end, // if we are not able to get that info from any endpoints } catch (err) { // TODO: Tracing } if (this.preferredLocations) { for (const location of this.preferredLocations) { try { const locationalEndpoint = GlobalEndpointManager.getLocationalEndpoint(this.defaultEndpoint, location); const options = { urlConnection: locationalEndpoint }; const { resource: databaseAccount } = await this.readDatabaseAccount(diagnosticNode, options); if (databaseAccount) { return databaseAccount; } } catch (err) { // TODO: Tracing } } } } /** * Gets the locational endpoint using the location name passed to it using the default endpoint. * * @param defaultEndpoint - The default endpoint to use for the endpoint. * @param locationName - The location name for the azure region like "East US". */ static getLocationalEndpoint(defaultEndpoint, locationName) { // For defaultEndpoint like 'https://contoso.documents.azure.com:443/' parse it to generate URL format // This defaultEndpoint should be global endpoint(and cannot be a locational endpoint) // and we agreed to document that const endpointUrl = new URL(defaultEndpoint); // hostname attribute in endpointUrl will return 'contoso.documents.azure.com' if (endpointUrl.hostname) { const hostnameParts = endpointUrl.hostname.toString().toLowerCase().split("."); if (hostnameParts) { // globalDatabaseAccountName will return 'contoso' const globalDatabaseAccountName = hostnameParts[0]; // Prepare the locationalDatabaseAccountName as contoso-EastUS for location_name 'East US' const locationalDatabaseAccountName = globalDatabaseAccountName + "-" + locationName.replace(" ", ""); // Replace 'contoso' with 'contoso-EastUS' and // return locationalEndpoint as https://contoso-EastUS.documents.azure.com:443/ const locationalEndpoint = defaultEndpoint .toLowerCase() .replace(globalDatabaseAccountName, locationalDatabaseAccountName); return locationalEndpoint; } } return null; } } exports.GlobalEndpointManager = GlobalEndpointManager; //# sourceMappingURL=globalEndpointManager.js.map