@azure/cosmos
Version:
Microsoft Azure Cosmos DB Service Node.js SDK for NOSQL API
389 lines (388 loc) • 16 kB
JavaScript
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
});