UNPKG

9.21 kBJavaScriptView Raw
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3import { OperationType, ResourceType, isReadRequest } from "./common";
4/**
5 * @hidden
6 * This internal class implements the logic for endpoint management for geo-replicated database accounts.
7 */
8export class GlobalEndpointManager {
9 /**
10 * @param options - The document client instance.
11 */
12 constructor(options, readDatabaseAccount) {
13 this.readDatabaseAccount = readDatabaseAccount;
14 this.writeableLocations = [];
15 this.readableLocations = [];
16 this.options = options;
17 this.defaultEndpoint = options.endpoint;
18 this.enableEndpointDiscovery = options.connectionPolicy.enableEndpointDiscovery;
19 this.isRefreshing = false;
20 this.preferredLocations = this.options.connectionPolicy.preferredLocations;
21 }
22 /**
23 * Gets the current read endpoint from the endpoint cache.
24 */
25 async getReadEndpoint() {
26 return this.resolveServiceEndpoint(ResourceType.item, OperationType.Read);
27 }
28 /**
29 * Gets the current write endpoint from the endpoint cache.
30 */
31 async getWriteEndpoint() {
32 return this.resolveServiceEndpoint(ResourceType.item, OperationType.Replace);
33 }
34 async getReadEndpoints() {
35 return this.readableLocations.map((loc) => loc.databaseAccountEndpoint);
36 }
37 async getWriteEndpoints() {
38 return this.writeableLocations.map((loc) => loc.databaseAccountEndpoint);
39 }
40 async markCurrentLocationUnavailableForRead(endpoint) {
41 await this.refreshEndpointList();
42 const location = this.readableLocations.find((loc) => loc.databaseAccountEndpoint === endpoint);
43 if (location) {
44 location.unavailable = true;
45 }
46 }
47 async markCurrentLocationUnavailableForWrite(endpoint) {
48 await this.refreshEndpointList();
49 const location = this.writeableLocations.find((loc) => loc.databaseAccountEndpoint === endpoint);
50 if (location) {
51 location.unavailable = true;
52 }
53 }
54 canUseMultipleWriteLocations(resourceType, operationType) {
55 let canUse = this.options.connectionPolicy.useMultipleWriteLocations;
56 if (resourceType) {
57 canUse =
58 canUse &&
59 (resourceType === ResourceType.item ||
60 (resourceType === ResourceType.sproc && operationType === OperationType.Execute));
61 }
62 return canUse;
63 }
64 async resolveServiceEndpoint(resourceType, operationType) {
65 // If endpoint discovery is disabled, always use the user provided endpoint
66 if (!this.options.connectionPolicy.enableEndpointDiscovery) {
67 return this.defaultEndpoint;
68 }
69 // If getting the database account, always use the user provided endpoint
70 if (resourceType === ResourceType.none) {
71 return this.defaultEndpoint;
72 }
73 if (this.readableLocations.length === 0 || this.writeableLocations.length === 0) {
74 const { resource: databaseAccount } = await this.readDatabaseAccount({
75 urlConnection: this.defaultEndpoint,
76 });
77 this.writeableLocations = databaseAccount.writableLocations;
78 this.readableLocations = databaseAccount.readableLocations;
79 }
80 const locations = isReadRequest(operationType)
81 ? this.readableLocations
82 : this.writeableLocations;
83 let location;
84 // If we have preferred locations, try each one in order and use the first available one
85 if (this.preferredLocations && this.preferredLocations.length > 0) {
86 for (const preferredLocation of this.preferredLocations) {
87 location = locations.find((loc) => loc.unavailable !== true &&
88 normalizeEndpoint(loc.name) === normalizeEndpoint(preferredLocation));
89 if (location) {
90 break;
91 }
92 }
93 }
94 // If no preferred locations or one did not match, just grab the first one that is available
95 if (!location) {
96 location = locations.find((loc) => {
97 return loc.unavailable !== true;
98 });
99 }
100 return location ? location.databaseAccountEndpoint : this.defaultEndpoint;
101 }
102 /**
103 * Refreshes the endpoint list by retrieving the writable and readable locations
104 * from the geo-replicated database account and then updating the locations cache.
105 * We skip the refreshing if enableEndpointDiscovery is set to False
106 */
107 async refreshEndpointList() {
108 if (!this.isRefreshing && this.enableEndpointDiscovery) {
109 this.isRefreshing = true;
110 const databaseAccount = await this.getDatabaseAccountFromAnyEndpoint();
111 if (databaseAccount) {
112 this.refreshEndpoints(databaseAccount);
113 }
114 this.isRefreshing = false;
115 }
116 }
117 refreshEndpoints(databaseAccount) {
118 for (const location of databaseAccount.writableLocations) {
119 const existingLocation = this.writeableLocations.find((loc) => loc.name === location.name);
120 if (!existingLocation) {
121 this.writeableLocations.push(location);
122 }
123 }
124 for (const location of databaseAccount.writableLocations) {
125 const existingLocation = this.readableLocations.find((loc) => loc.name === location.name);
126 if (!existingLocation) {
127 this.readableLocations.push(location);
128 }
129 }
130 }
131 /**
132 * Gets the database account first by using the default endpoint, and if that doesn't returns
133 * use the endpoints for the preferred locations in the order they are specified to get
134 * the database account.
135 */
136 async getDatabaseAccountFromAnyEndpoint() {
137 try {
138 const options = { urlConnection: this.defaultEndpoint };
139 const { resource: databaseAccount } = await this.readDatabaseAccount(options);
140 return databaseAccount;
141 // If for any reason(non - globaldb related), we are not able to get the database
142 // account from the above call to readDatabaseAccount,
143 // we would try to get this information from any of the preferred locations that the user
144 // might have specified (by creating a locational endpoint)
145 // and keeping eating the exception until we get the database account and return None at the end,
146 // if we are not able to get that info from any endpoints
147 }
148 catch (err) {
149 // TODO: Tracing
150 }
151 if (this.preferredLocations) {
152 for (const location of this.preferredLocations) {
153 try {
154 const locationalEndpoint = GlobalEndpointManager.getLocationalEndpoint(this.defaultEndpoint, location);
155 const options = { urlConnection: locationalEndpoint };
156 const { resource: databaseAccount } = await this.readDatabaseAccount(options);
157 if (databaseAccount) {
158 return databaseAccount;
159 }
160 }
161 catch (err) {
162 // TODO: Tracing
163 }
164 }
165 }
166 }
167 /**
168 * Gets the locational endpoint using the location name passed to it using the default endpoint.
169 *
170 * @param defaultEndpoint - The default endpoint to use for the endpoint.
171 * @param locationName - The location name for the azure region like "East US".
172 */
173 static getLocationalEndpoint(defaultEndpoint, locationName) {
174 // For defaultEndpoint like 'https://contoso.documents.azure.com:443/' parse it to generate URL format
175 // This defaultEndpoint should be global endpoint(and cannot be a locational endpoint)
176 // and we agreed to document that
177 const endpointUrl = new URL(defaultEndpoint);
178 // hostname attribute in endpointUrl will return 'contoso.documents.azure.com'
179 if (endpointUrl.hostname) {
180 const hostnameParts = endpointUrl.hostname.toString().toLowerCase().split(".");
181 if (hostnameParts) {
182 // globalDatabaseAccountName will return 'contoso'
183 const globalDatabaseAccountName = hostnameParts[0];
184 // Prepare the locationalDatabaseAccountName as contoso-EastUS for location_name 'East US'
185 const locationalDatabaseAccountName = globalDatabaseAccountName + "-" + locationName.replace(" ", "");
186 // Replace 'contoso' with 'contoso-EastUS' and
187 // return locationalEndpoint as https://contoso-EastUS.documents.azure.com:443/
188 const locationalEndpoint = defaultEndpoint
189 .toLowerCase()
190 .replace(globalDatabaseAccountName, locationalDatabaseAccountName);
191 return locationalEndpoint;
192 }
193 }
194 return null;
195 }
196}
197function normalizeEndpoint(endpoint) {
198 return endpoint.split(" ").join("").toLowerCase();
199}
200//# sourceMappingURL=globalEndpointManager.js.map
\No newline at end of file