UNPKG

@azure/cosmos

Version:
389 lines (388 loc) • 16 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var globalEndpointManager_exports = {}; __export(globalEndpointManager_exports, { GlobalEndpointManager: () => GlobalEndpointManager }); module.exports = __toCommonJS(globalEndpointManager_exports); var import_common = require("./common/index.js"); var import_constants = require("./common/constants.js"); var import_CosmosDiagnostics = require("./CosmosDiagnostics.js"); var import_diagnostics = require("./utils/diagnostics.js"); var import_checkURL = require("./utils/checkURL.js"); var import_helper = require("./common/helper.js"); class GlobalEndpointManager { /** * @param options - The document client instance. * @internal */ constructor(options, readDatabaseAccount) { this.readDatabaseAccount = readDatabaseAccount; 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; this.enablePartitionLevelFailover = options.connectionPolicy.enablePartitionLevelFailover; this.enablePartitionLevelCircuitBreaker = options.connectionPolicy.enablePartitionLevelCircuitBreaker; this.lastKnownPPCBEnabled = options.connectionPolicy.enablePartitionLevelCircuitBreaker; this.lastKnownPPAFEnabled = false; } /** * The endpoint used to create the client instance. */ defaultEndpoint; /** * Flag to enable/disable automatic redirecting of requests based on read/write operations. */ enableEndpointDiscovery; isRefreshing; options; /** * List of azure regions to be used as preferred locations for read requests. */ preferredLocations; writeableLocations = []; readableLocations = []; unavailableReadableLocations = []; unavailableWriteableLocations = []; enableMultipleWriteLocations; preferredLocationsCount; /** * Flag to enable/disable the Per Partition Level Failover (PPAF). Contains the value from the Connection policy. * @internal */ enablePartitionLevelFailover; /** * Flag to enable/disable the Per Partition Level Circuit Breaker (PPCB). Contains the value from the Connection policy. * @internal */ enablePartitionLevelCircuitBreaker; /** * Cached PPAF enablement status from the last account refresh * @internal */ lastKnownPPAFEnabled; /** * Cached circuit breaker timer enablement status * @internal */ lastKnownPPCBEnabled; /** * Event that is raised when circuit breaker timer should start or stop based on PPAF/PPCB status changes * @internal */ onEnablePartitionLevelFailoverConfigChanged; /** * Gets the current read endpoint from the endpoint cache. */ async getReadEndpoint(diagnosticNode) { return this.resolveServiceEndpoint(diagnosticNode, import_common.ResourceType.item, import_common.OperationType.Read); } /** * Gets the current write endpoint from the endpoint cache. */ async getWriteEndpoint(diagnosticNode) { return this.resolveServiceEndpoint(diagnosticNode, import_common.ResourceType.item, import_common.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 === import_common.ResourceType.item || resourceType === import_common.ResourceType.sproc && operationType === import_common.OperationType.Execute); } return canUse; } getEffectiveExcludedLocations(excludedLocations = [], resourceType) { if (!(0, import_helper.canApplyExcludedLocations)(resourceType)) { return /* @__PURE__ */ new Set(); } return excludedLocations.length ? new Set(excludedLocations.map(import_checkURL.normalizeEndpoint)) : /* @__PURE__ */ new Set(); } filterExcludedLocations(preferredLocations, excludedLocations) { if (!excludedLocations || excludedLocations.size === 0) { return preferredLocations; } const filteredLocations = preferredLocations.filter((location) => { return !excludedLocations.has((0, import_checkURL.normalizeEndpoint)(location)); }); return filteredLocations; } async resolveServiceEndpoint(diagnosticNode, resourceType, operationType, startServiceEndpointIndex = 0) { return this.resolveServiceEndpointInternal({ diagnosticNode, resourceType, operationType, startServiceEndpointIndex }); } /** * @internal */ async resolveServiceEndpointInternal(resolveServiceEndpointOptions) { const { diagnosticNode, resourceType, operationType, startServiceEndpointIndex, excludedLocations = [] } = resolveServiceEndpointOptions; if (!this.options.connectionPolicy.enableEndpointDiscovery) { diagnosticNode.addData({ readFromCache: true }, "default_endpoint"); diagnosticNode.recordEndpointResolution(this.defaultEndpoint); return this.defaultEndpoint; } if (resourceType === import_common.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, import_diagnostics.withMetadataDiagnostics)( async (metadataNode) => { return this.readDatabaseAccount(metadataNode, { urlConnection: this.defaultEndpoint }); }, diagnosticNode, import_CosmosDiagnostics.MetadataLookUpType.DatabaseAccountLookUp ); this.writeableLocations = resourceResponse.resource.writableLocations; this.readableLocations = resourceResponse.resource.readableLocations; this.enableMultipleWriteLocations = resourceResponse.resource.enableMultipleWritableLocations; if (this.enablePartitionLevelFailover) { this.refreshPPAFFeatureFlag(resourceResponse.resource); } } const locations = (0, import_common.isReadRequest)(operationType) ? this.readableLocations : this.writeableLocations; const effectiveExcludedLocations = this.getEffectiveExcludedLocations( excludedLocations, resourceType ); diagnosticNode.addData( { excludedLocations: Array.from(effectiveExcludedLocations) }, "excluded_locations" ); const availableLocations = this.filterExcludedLocations( this.preferredLocations, effectiveExcludedLocations ); let location; if (availableLocations && availableLocations.length > 0 && startServiceEndpointIndex < availableLocations.length) { this.preferredLocationsCount = availableLocations.length; for (let i = startServiceEndpointIndex; i < availableLocations.length; i++) { const preferredLocation = availableLocations[i]; location = locations.find( (loc) => loc.unavailable !== true && (0, import_checkURL.normalizeEndpoint)(loc.name) === (0, import_checkURL.normalizeEndpoint)(preferredLocation) ); if (location) { break; } } } if (!location) { const startIndexValid = startServiceEndpointIndex >= 0 && startServiceEndpointIndex < locations.length; const locationsToSearch = startIndexValid ? locations.slice(startServiceEndpointIndex) : locations; location = locationsToSearch.find((loc) => { return loc.unavailable !== true && !effectiveExcludedLocations.has((0, import_checkURL.normalizeEndpoint)(loc.name)); }); } 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) { if (this.enablePartitionLevelFailover) { this.refreshPPAFFeatureFlag(databaseAccount); } this.refreshStaleUnavailableLocations(); this.refreshEndpoints(databaseAccount); } this.isRefreshing = false; } } refreshEndpoints(databaseAccount) { const oldWritableLocations = this.writeableLocations; const oldReadableLocations = this.readableLocations; function merge(loc, oldList) { const prev = oldList.find((o) => o.name === loc.name); if (prev) { loc.unavailable = prev.unavailable; loc.lastUnavailabilityTimestampInMs = prev.lastUnavailabilityTimestampInMs; } return loc; } this.writeableLocations = databaseAccount.writableLocations.map( (loc) => merge({ ...loc }, oldWritableLocations) ); this.readableLocations = databaseAccount.readableLocations.map( (loc) => merge({ ...loc }, oldReadableLocations) ); } 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 unavailableLocation = allLocations.find((loc) => loc.name === location.name); if (unavailableLocation && now - unavailableLocation.lastUnavailabilityTimestampInMs > import_constants.Constants.LocationUnavailableExpirationTimeInMs) { unavailableLocation.unavailable = false; } } } cleanUnavailableLocationList(now, unavailableLocations) { return unavailableLocations.filter((loc) => { return now - loc.lastUnavailabilityTimestampInMs < import_constants.Constants.LocationUnavailableExpirationTimeInMs; }); } /** * 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; } catch (err) { } 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) { } } } } /** * 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) { const endpointUrl = new URL(defaultEndpoint); if (endpointUrl.hostname) { const hostnameParts = endpointUrl.hostname.toString().toLowerCase().split("."); if (hostnameParts) { const globalDatabaseAccountName = hostnameParts[0]; const locationalDatabaseAccountName = globalDatabaseAccountName + "-" + locationName.replace(" ", ""); const locationalEndpoint = defaultEndpoint.toLowerCase().replace(globalDatabaseAccountName, locationalDatabaseAccountName); return locationalEndpoint; } } return null; } /** * Checks for changes in PPAF enablement status and raises events if they have changed. * It also manages circuit breaker timer state. * @internal */ refreshPPAFFeatureFlag(databaseAccount) { let shouldEnableCircuitBreakerTimer = false; if (this.enablePartitionLevelCircuitBreaker) { shouldEnableCircuitBreakerTimer = true; } else { if (!this.enablePartitionLevelFailover) { shouldEnableCircuitBreakerTimer = false; } else { shouldEnableCircuitBreakerTimer = databaseAccount.enablePerPartitionFailover ?? this.lastKnownPPAFEnabled ?? false; } } this.lastKnownPPAFEnabled = databaseAccount.enablePerPartitionFailover; if (this.lastKnownPPCBEnabled !== shouldEnableCircuitBreakerTimer) { this.lastKnownPPCBEnabled = shouldEnableCircuitBreakerTimer; this.onEnablePartitionLevelFailoverConfigChanged?.(shouldEnableCircuitBreakerTimer); } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GlobalEndpointManager });