-
Notifications
You must be signed in to change notification settings - Fork 4
Failover support #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Failover support #98
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
6b836f7
resolve conflicts
linglingye001 7150416
resolve conflicts
linglingye001 19edd02
resolve conflicts
linglingye001 ac4ab59
add tests
linglingye001 25dd268
resolve conflicts and update
linglingye001 5c9b423
fix lint
linglingye001 0b8a8f6
resolve conflicts
linglingye001 f0dbc81
resolve comments
linglingye001 232bb85
update package-lock
linglingye001 f01a34b
update
linglingye001 6357f59
update
linglingye001 7fbf05f
update failover error
linglingye001 cc13f34
update
linglingye001 8802f74
update
linglingye001 3b4839c
update failoverable error with 'ENOTFOUND'
linglingye001 a43af88
fix lint
linglingye001 9e59790
update
linglingye001 c52599a
added ENOENT error
linglingye001 a7b7bfb
update
linglingye001 5adb089
update error message in test
linglingye001 0905cd9
update test
linglingye001 f42d3b1
update test
linglingye001 1c8f00d
update
linglingye001 1ea1d51
resolve conflicts
linglingye001 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAd | |
| import { RefreshTimer } from "./refresh/RefreshTimer.js"; | ||
| import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; | ||
| import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; | ||
| import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; | ||
|
|
||
| type PagedSettingSelector = SettingSelector & { | ||
| /** | ||
|
|
@@ -56,10 +57,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| */ | ||
| #sortedTrimKeyPrefixes: string[] | undefined; | ||
| readonly #requestTracingEnabled: boolean; | ||
| #client: AppConfigurationClient; | ||
| #clientEndpoint: string | undefined; | ||
| #clientManager: ConfigurationClientManager; | ||
| #options: AzureAppConfigurationOptions | undefined; | ||
| #isInitialLoadCompleted: boolean = false; | ||
| #isFailoverRequest: boolean = false; | ||
|
|
||
| // Refresh | ||
| #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; | ||
|
|
@@ -78,13 +79,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| #featureFlagSelectors: PagedSettingSelector[] = []; | ||
|
|
||
| constructor( | ||
| client: AppConfigurationClient, | ||
| clientEndpoint: string | undefined, | ||
| options: AzureAppConfigurationOptions | undefined | ||
| clientManager: ConfigurationClientManager, | ||
| options: AzureAppConfigurationOptions | undefined, | ||
| ) { | ||
| this.#client = client; | ||
| this.#clientEndpoint = clientEndpoint; | ||
| this.#options = options; | ||
| this.#clientManager = clientManager; | ||
|
|
||
| // Enable request tracing if not opt-out | ||
| this.#requestTracingEnabled = requestTracingEnabled(); | ||
|
|
@@ -197,35 +196,66 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| return { | ||
| requestTracingEnabled: this.#requestTracingEnabled, | ||
| initialLoadCompleted: this.#isInitialLoadCompleted, | ||
| appConfigOptions: this.#options | ||
| appConfigOptions: this.#options, | ||
| isFailoverRequest: this.#isFailoverRequest | ||
| }; | ||
| } | ||
|
|
||
| async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> { | ||
| const loadedSettings: ConfigurationSetting[] = []; | ||
| async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> { | ||
| const clientWrappers = await this.#clientManager.getClients(); | ||
|
|
||
| // validate selectors | ||
| const selectors = getValidKeyValueSelectors(this.#options?.selectors); | ||
| let successful: boolean; | ||
| for (const clientWrapper of clientWrappers) { | ||
| successful = false; | ||
| try { | ||
| const result = await funcToExecute(clientWrapper.client); | ||
| this.#isFailoverRequest = false; | ||
| successful = true; | ||
| clientWrapper.updateBackoffStatus(successful); | ||
| return result; | ||
| } catch (error) { | ||
| if (isFailoverableError(error)) { | ||
| clientWrapper.updateBackoffStatus(successful); | ||
| this.#isFailoverRequest = true; | ||
| continue; | ||
| } | ||
|
|
||
| for (const selector of selectors) { | ||
| const listOptions: ListConfigurationSettingsOptions = { | ||
| keyFilter: selector.keyFilter, | ||
| labelFilter: selector.labelFilter | ||
| }; | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| const settings = listConfigurationSettingsWithTrace( | ||
| this.#requestTraceOptions, | ||
| this.#client, | ||
| listOptions | ||
| ); | ||
| this.#clientManager.refreshClients(); | ||
| throw new Error("All clients failed to get configuration settings."); | ||
| } | ||
|
|
||
| for await (const setting of settings) { | ||
| if (!isFeatureFlag(setting)) { // exclude feature flags | ||
| loadedSettings.push(setting); | ||
| async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> { | ||
| // validate selectors | ||
| const selectors = getValidKeyValueSelectors(this.#options?.selectors); | ||
zhiyuanliang-ms marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const funcToExecute = async (client) => { | ||
| const loadedSettings: ConfigurationSetting[] = []; | ||
| for (const selector of selectors) { | ||
| const listOptions: ListConfigurationSettingsOptions = { | ||
| keyFilter: selector.keyFilter, | ||
| labelFilter: selector.labelFilter | ||
| }; | ||
|
|
||
| const settings = listConfigurationSettingsWithTrace( | ||
| this.#requestTraceOptions, | ||
| client, | ||
| listOptions | ||
| ); | ||
|
|
||
| for await (const setting of settings) { | ||
| if (!isFeatureFlag(setting)) { // exclude feature flags | ||
| loadedSettings.push(setting); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return loadedSettings; | ||
| return loadedSettings; | ||
| }; | ||
|
|
||
| return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -279,29 +309,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| } | ||
|
|
||
| async #loadFeatureFlags() { | ||
| const featureFlagSettings: ConfigurationSetting[] = []; | ||
| for (const selector of this.#featureFlagSelectors) { | ||
| const listOptions: ListConfigurationSettingsOptions = { | ||
| keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, | ||
| labelFilter: selector.labelFilter | ||
| }; | ||
| // Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting | ||
| const funcToExecute = async (client) => { | ||
| const featureFlagSettings: ConfigurationSetting[] = []; | ||
| // deep copy selectors to avoid modification if current client fails | ||
| const selectors = JSON.parse( | ||
linglingye001 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| JSON.stringify(this.#featureFlagSelectors) | ||
| ); | ||
|
|
||
| const pageEtags: string[] = []; | ||
| const pageIterator = listConfigurationSettingsWithTrace( | ||
| this.#requestTraceOptions, | ||
| this.#client, | ||
| listOptions | ||
| ).byPage(); | ||
| for await (const page of pageIterator) { | ||
| pageEtags.push(page.etag ?? ""); | ||
| for (const setting of page.items) { | ||
| if (isFeatureFlag(setting)) { | ||
| featureFlagSettings.push(setting); | ||
| for (const selector of selectors) { | ||
| const listOptions: ListConfigurationSettingsOptions = { | ||
| keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, | ||
| labelFilter: selector.labelFilter | ||
| }; | ||
|
|
||
| const pageEtags: string[] = []; | ||
| const pageIterator = listConfigurationSettingsWithTrace( | ||
| this.#requestTraceOptions, | ||
| client, | ||
| listOptions | ||
| ).byPage(); | ||
| for await (const page of pageIterator) { | ||
| pageEtags.push(page.etag ?? ""); | ||
| for (const setting of page.items) { | ||
| if (isFeatureFlag(setting)) { | ||
| featureFlagSettings.push(setting); | ||
| } | ||
| } | ||
| } | ||
| selector.pageEtags = pageEtags; | ||
| } | ||
| selector.pageEtags = pageEtags; | ||
| } | ||
|
|
||
| this.#featureFlagSelectors = selectors; | ||
| return featureFlagSettings; | ||
| }; | ||
|
|
||
| const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; | ||
|
|
||
| // parse feature flags | ||
| const featureFlags = await Promise.all( | ||
|
|
@@ -389,7 +432,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| // check if any refresh task failed | ||
| for (const result of results) { | ||
| if (result.status === "rejected") { | ||
| throw result.reason; | ||
| console.warn("Refresh failed:", result.reason); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -430,13 +473,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| } | ||
|
|
||
| if (needRefresh) { | ||
| try { | ||
| await this.#loadSelectedAndWatchedKeyValues(); | ||
| } catch (error) { | ||
| // if refresh failed, backoff | ||
| this.#refreshTimer.backoff(); | ||
| throw error; | ||
| } | ||
| await this.#loadSelectedAndWatchedKeyValues(); | ||
| } | ||
|
|
||
| this.#refreshTimer.reset(); | ||
|
|
@@ -454,39 +491,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| } | ||
|
|
||
| // check if any feature flag is changed | ||
| let needRefresh = false; | ||
| for (const selector of this.#featureFlagSelectors) { | ||
| const listOptions: ListConfigurationSettingsOptions = { | ||
| keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, | ||
| labelFilter: selector.labelFilter, | ||
| pageEtags: selector.pageEtags | ||
| }; | ||
| const pageIterator = listConfigurationSettingsWithTrace( | ||
| this.#requestTraceOptions, | ||
| this.#client, | ||
| listOptions | ||
| ).byPage(); | ||
|
|
||
| for await (const page of pageIterator) { | ||
| if (page._response.status === 200) { // created or changed | ||
| needRefresh = true; | ||
| break; | ||
| const funcToExecute = async (client) => { | ||
| for (const selector of this.#featureFlagSelectors) { | ||
| const listOptions: ListConfigurationSettingsOptions = { | ||
linglingye001 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, | ||
| labelFilter: selector.labelFilter, | ||
| pageEtags: selector.pageEtags | ||
| }; | ||
|
|
||
| const pageIterator = listConfigurationSettingsWithTrace( | ||
| this.#requestTraceOptions, | ||
| client, | ||
| listOptions | ||
| ).byPage(); | ||
|
|
||
| for await (const page of pageIterator) { | ||
| if (page._response.status === 200) { // created or changed | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| }; | ||
|
|
||
| if (needRefresh) { | ||
| break; // short-circuit if result from any of the selectors is changed | ||
| } | ||
| } | ||
|
|
||
| const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute); | ||
| if (needRefresh) { | ||
| try { | ||
| await this.#loadFeatureFlags(); | ||
| } catch (error) { | ||
| // if refresh failed, backoff | ||
| this.#featureFlagRefreshTimer.backoff(); | ||
| throw error; | ||
| } | ||
| await this.#loadFeatureFlags(); | ||
| } | ||
|
|
||
| this.#featureFlagRefreshTimer.reset(); | ||
|
|
@@ -540,14 +570,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| * Get a configuration setting by key and label. If the setting is not found, return undefine instead of throwing an error. | ||
| */ | ||
| async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, customOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> { | ||
| let response: GetConfigurationSettingResponse | undefined; | ||
| try { | ||
| response = await getConfigurationSettingWithTrace( | ||
| const funcToExecute = async (client) => { | ||
| return getConfigurationSettingWithTrace( | ||
| this.#requestTraceOptions, | ||
| this.#client, | ||
| client, | ||
| configurationSettingId, | ||
| customOptions | ||
| ); | ||
| }; | ||
|
|
||
| let response: GetConfigurationSettingResponse | undefined; | ||
| try { | ||
| response = await this.#executeWithFailoverPolicy(funcToExecute); | ||
| } catch (error) { | ||
| if (isRestError(error) && error.statusCode === 404) { | ||
| response = undefined; | ||
|
|
@@ -634,7 +668,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |
| } | ||
|
|
||
| #createFeatureFlagReference(setting: ConfigurationSetting<string>): string { | ||
| let featureFlagReference = `${this.#clientEndpoint}kv/${setting.key}`; | ||
| let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`; | ||
| if (setting.label && setting.label.trim().length !== 0) { | ||
| featureFlagReference += `?label=${setting.label}`; | ||
| } | ||
|
|
@@ -794,3 +828,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel | |
| return getValidSelectors(selectors); | ||
| } | ||
| } | ||
|
|
||
| function isFailoverableError(error: any): boolean { | ||
| // ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory | ||
| return isRestError(error) && (error.code === "ENOTFOUND" || error.code === "ENOENT" || | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://nodejs.org/api/errors.html#common-system-errors Missed some error type |
||
| (error.statusCode !== undefined && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500))); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.