Skip to content

Commit

Permalink
support snapshot
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiyuanliang-ms committed Dec 19, 2024
1 parent 71aebab commit 1f41e0d
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 48 deletions.
138 changes: 106 additions & 32 deletions src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration";
import {
AppConfigurationClient,
ConfigurationSetting,
ConfigurationSettingId,
GetConfigurationSettingOptions,
GetConfigurationSettingResponse,
ListConfigurationSettingsOptions,
featureFlagPrefix,
isFeatureFlag,
GetSnapshotOptions,
GetSnapshotResponse,
KnownSnapshotComposition
} from "@azure/app-configuration";
import { isRestError } from "@azure/core-rest-pipeline";
import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js";
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
Expand Down Expand Up @@ -35,7 +47,14 @@ import {
} from "./featureManagement/constants.js";
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
import { RefreshTimer } from "./refresh/RefreshTimer.js";
import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
import {
RequestTracingOptions,
getConfigurationSettingWithTrace,
listConfigurationSettingsWithTrace,
getSnapshotWithTrace,
listConfigurationSettingsForSnapshotWithTrace,
requestTracingEnabled
} from "./requestTracing/utils.js";
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
Expand Down Expand Up @@ -363,26 +382,49 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
);

for (const selector of selectorsToUpdate) {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: 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 (loadFeatureFlag === isFeatureFlag(setting)) {
loadedSettings.push(setting);
if (selector.snapshotName === undefined) {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: 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 (loadFeatureFlag === isFeatureFlag(setting)) {
loadedSettings.push(setting);
}
}
}
selector.pageEtags = pageEtags;
} else { // snapshot selector
const snapshot = await this.#getSnapshot(selector.snapshotName);
if (snapshot === undefined) {
throw new Error(`Could not find snapshot with name ${selector.snapshotName}.`);
}
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
throw new Error(`Composition type for the selected snapshot with name ${selector.snapshotName} must be 'key'.`);
}
const pageIterator = listConfigurationSettingsForSnapshotWithTrace(
this.#requestTraceOptions,
client,
selector.snapshotName
).byPage();

for await (const page of pageIterator) {
for (const setting of page.items) {
if (loadFeatureFlag === isFeatureFlag(setting)) {
loadedSettings.push(setting);
}
}
}
}
selector.pageEtags = pageEtags;
}

if (loadFeatureFlag) {
Expand Down Expand Up @@ -530,6 +572,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
async #checkConfigurationSettingsChange(selectors: PagedSettingSelector[]): Promise<boolean> {
const funcToExecute = async (client) => {
for (const selector of selectors) {
if (selector.snapshotName) { // skip snapshot selector
continue;
}
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: selector.keyFilter,
labelFilter: selector.labelFilter,
Expand Down Expand Up @@ -581,6 +626,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return response;
}

async #getSnapshot(snapshotName: string, customOptions?: GetSnapshotOptions): Promise<GetSnapshotResponse | undefined> {
const funcToExecute = async (client) => {
return getSnapshotWithTrace(
this.#requestTraceOptions,
client,
snapshotName,
customOptions
);
};

let response: GetSnapshotResponse | undefined;
try {
response = await this.#executeWithFailoverPolicy(funcToExecute);
} catch (error) {
if (isRestError(error) && error.statusCode === 404) {
response = undefined;
} else {
throw error;
}
}
return response;
}

async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
let clientWrappers = await this.#clientManager.getClients();
if (this.#options?.loadBalancingEnabled && this.#lastSuccessfulEndpoint !== "" && clientWrappers.length > 1) {
Expand Down Expand Up @@ -862,11 +930,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}
}

function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
// below code deduplicates selectors by keyFilter and labelFilter, the latter selector wins
function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
// below code deduplicates selectors, the latter selector wins
const uniqueSelectors: SettingSelector[] = [];
for (const selector of selectors) {
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter);
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter && s.snapshotName === selector.snapshotName);
if (existingSelectorIndex >= 0) {
uniqueSelectors.splice(existingSelectorIndex, 1);
}
Expand All @@ -875,14 +943,20 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {

return uniqueSelectors.map(selectorCandidate => {
const selector = { ...selectorCandidate };
if (!selector.keyFilter) {
throw new Error("Key filter cannot be null or empty.");
}
if (!selector.labelFilter) {
selector.labelFilter = LabelFilter.Null;
}
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
throw new Error("The characters '*' and ',' are not supported in label filters.");
if (selector.snapshotName) {
if (selector.keyFilter || selector.labelFilter) {
throw new Error("Key or label filter should not be used for a snapshot.");
}
} else {
if (!selector.keyFilter) {
throw new Error("Key filter cannot be null or empty.");
}
if (!selector.labelFilter) {
selector.labelFilter = LabelFilter.Null;
}
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
throw new Error("The characters '*' and ',' are not supported in label filters.");
}
}
return selector;
});
Expand All @@ -893,7 +967,7 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect
// Default selector: key: *, label: \0
return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];
}
return getValidSelectors(selectors);
return getValidSettingSelectors(selectors);
}

function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] {
Expand All @@ -904,7 +978,7 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
selectors.forEach(selector => {
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
});
return getValidSelectors(selectors);
return getValidSettingSelectors(selectors);
}
}

Expand Down
47 changes: 31 additions & 16 deletions src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration";
import { OperationOptions } from "@azure/core-client";
import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions, GetSnapshotOptions, ListConfigurationSettingsForSnapshotOptions } from "@azure/app-configuration";
import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js";
import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions.js";
import {
Expand Down Expand Up @@ -45,15 +46,7 @@ export function listConfigurationSettingsWithTrace(
client: AppConfigurationClient,
listOptions: ListConfigurationSettingsOptions
) {
const actualListOptions = { ...listOptions };
if (requestTracingOptions.enabled) {
actualListOptions.requestOptions = {
customHeaders: {
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
}
};
}

const actualListOptions = applyRequestTracing(requestTracingOptions, listOptions);
return client.listConfigurationSettings(actualListOptions);
}

Expand All @@ -63,20 +56,43 @@ export function getConfigurationSettingWithTrace(
configurationSettingId: ConfigurationSettingId,
getOptions?: GetConfigurationSettingOptions,
) {
const actualGetOptions = { ...getOptions };
const actualGetOptions = applyRequestTracing(requestTracingOptions, getOptions);
return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
}

export function getSnapshotWithTrace(
requestTracingOptions: RequestTracingOptions,
client: AppConfigurationClient,
snapshotName: string,
getOptions?: GetSnapshotOptions
) {
const actualGetOptions = applyRequestTracing(requestTracingOptions, getOptions);
return client.getSnapshot(snapshotName, actualGetOptions);
}

export function listConfigurationSettingsForSnapshotWithTrace(
requestTracingOptions: RequestTracingOptions,
client: AppConfigurationClient,
snapshotName: string,
listOptions?: ListConfigurationSettingsForSnapshotOptions
) {
const actualListOptions = applyRequestTracing(requestTracingOptions, listOptions);
return client.listConfigurationSettingsForSnapshot(snapshotName, actualListOptions);
}

function applyRequestTracing<T extends OperationOptions>(requestTracingOptions: RequestTracingOptions, operationOptions?: T) {
const actualOptions = { ...operationOptions };
if (requestTracingOptions.enabled) {
actualGetOptions.requestOptions = {
actualOptions.requestOptions = {
customHeaders: {
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
}
};
}

return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
return actualOptions;
}

export function createCorrelationContextHeader(requestTracingOptions: RequestTracingOptions): string {
function createCorrelationContextHeader(requestTracingOptions: RequestTracingOptions): string {
/*
RequestType: 'Startup' during application starting up, 'Watch' after startup completed.
Host: identify with defined envs
Expand Down Expand Up @@ -200,4 +216,3 @@ export function isWebWorker() {

return workerGlobalScopeDefined && importScriptsAsGlobalFunction && isNavigatorDefinedAsExpected;
}

9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export type SettingSelector = {
* @defaultValue `LabelFilter.Null`, matching key-values without a label.
*/
labelFilter?: string

/**
* The name of snapshot to load from App Configuration.
*
* @remarks
* Snapshot is a set of key-values selected from the App Configuration store based on the composition type and filters. Once created, it is stored as an immutable entity that can be referenced by name.
* If snapshot name is used in a selector, no key and label filter should be used for it. Otherwise, an exception will be thrown.
*/
snapshotName?: string
};

/**
Expand Down

0 comments on commit 1f41e0d

Please sign in to comment.