From f4c8b71c66be529d74858f7c0ff3f36543d4a657 Mon Sep 17 00:00:00 2001 From: Chuck Skelton Date: Tue, 16 Jun 2026 18:12:04 -0700 Subject: [PATCH 1/2] Initial version of Hot Partition Key Rate Limiting Policy --- .vscode/settings.json | 7 +- src/Common/Constants.ts | 1 + src/Common/dataAccess/readCollectionOffer.ts | 3 + src/Common/dataAccess/updateOffer.ts | 8 ++ src/Contracts/DataModels.ts | 7 ++ .../Controls/Settings/SettingsComponent.tsx | 38 ++++-- .../ScaleComponent.test.tsx | 3 + .../SettingsSubComponents/ScaleComponent.tsx | 8 +- ...roughputInputAutoPilotV3Component.test.tsx | 15 +++ .../ThroughputInputAutoPilotV3Component.tsx | 61 +++++++++- ...putInputAutoPilotV3Component.test.tsx.snap | 114 ++++++++++++++++++ .../Controls/Settings/SettingsUtils.tsx | 5 +- .../SettingsComponent.test.tsx.snap | 3 + src/Localization/en/Resources.json | 4 +- src/Utils/CapabilityUtils.test.ts | 47 ++++++++ src/Utils/CapabilityUtils.ts | 7 ++ .../arm/generatedClients/cosmos/types.ts | 7 ++ tsconfig.strict.json | 1 + 18 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 src/Utils/CapabilityUtils.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index a57d40961..9985587ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,12 +17,17 @@ "test/out/**": true, "workers/libs/**": true }, + "js/ts.tsdk.path": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "explicit" }, + "js/ts.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifier": "non-relative", - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index eb1cb44c8..7803a305b 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -88,6 +88,7 @@ export class CapabilityNames { public static readonly EnableDynamicDataMasking: string = "EnableDynamicDataMasking"; public static readonly EnableNoSQLFullTextSearchPreviewFeatures: string = "EnableNoSQLFullTextSearchPreviewFeatures"; public static readonly EnableOnlineCopyFeature: string = "EnableOnlineContainerCopy"; + public static readonly EnableHotPartitionKeyThrottling: string = "EnableHotPartitionKeyThrottling"; } export enum CapacityMode { diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index 6712f274b..073635d59 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -112,6 +112,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri : resource.softAllowedMaximumThroughput; const throughputBuckets = resource?.throughputBuckets; + const hotPartitionKeyRateLimitingPolicy = resource?.hotPartitionKeyRateLimitingPolicy; if (autoscaleSettings) { return { @@ -123,6 +124,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri instantMaximumThroughput, softAllowedMaximumThroughput, throughputBuckets, + hotPartitionKeyRateLimitingPolicy, }; } @@ -135,6 +137,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri instantMaximumThroughput, softAllowedMaximumThroughput, throughputBuckets, + hotPartitionKeyRateLimitingPolicy, }; } diff --git a/src/Common/dataAccess/updateOffer.ts b/src/Common/dataAccess/updateOffer.ts index cd20fcd4c..42217dfd1 100644 --- a/src/Common/dataAccess/updateOffer.ts +++ b/src/Common/dataAccess/updateOffer.ts @@ -367,6 +367,10 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd body.properties.resource.throughputBuckets = throughputBuckets; } + if (params.hotPartitionKeyRateLimitingPolicy !== undefined) { + body.properties.resource.hotPartitionKeyRateLimitingPolicy = params.hotPartitionKeyRateLimitingPolicy; + } + return body; }; @@ -409,6 +413,10 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise => newOffer.content.offerAutopilotSettings = { maxThroughput: 0 }; } + if (params.hotPartitionKeyRateLimitingPolicy !== undefined) { + newOffer.content.hotPartitionKeyRateLimitingPolicy = params.hotPartitionKeyRateLimitingPolicy; + } + const sdkResponse = await client() .offer(params.currentOffer.id) // TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660) diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 770ee7948..3e80d9ab9 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -343,6 +343,11 @@ export interface Offer { instantMaximumThroughput?: number; softAllowedMaximumThroughput?: number; throughputBuckets?: ThroughputBucket[]; + hotPartitionKeyRateLimitingPolicy?: HotPartitionKeyRateLimitingPolicy; +} + +export interface HotPartitionKeyRateLimitingPolicy { + maximumPerPartitionKeyThroughputUtilizationPercent: number; } export interface ThroughputBucket { @@ -359,6 +364,7 @@ export interface SDKOfferDefinition extends Resource { offerIsRUPerMinuteThroughputEnabled?: boolean; collectionThroughputInfo?: OfferThroughputInfo; offerAutopilotSettings?: AutoPilotOfferSettings; + hotPartitionKeyRateLimitingPolicy?: HotPartitionKeyRateLimitingPolicy; }; resource?: string; offerResourceId?: string; @@ -492,6 +498,7 @@ export interface UpdateOfferParams { migrateToAutoPilot?: boolean; migrateToManual?: boolean; throughputBuckets?: ThroughputBucket[]; + hotPartitionKeyRateLimitingPolicy?: HotPartitionKeyRateLimitingPolicy | null; } export interface Notification { diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 4aca3ce6b..391ae94f7 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -17,25 +17,25 @@ import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView"; import { useDatabases } from "Explorer/useDatabases"; import { Keys, t } from "Localization"; import { isFabricNative } from "Platform/Fabric/FabricUtil"; -import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; -import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import * as React from "react"; +import { isHotPartitionKeyThrottlingEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; +import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import DiscardIcon from "../../../../images/discard.svg"; import SaveIcon from "../../../../images/save-cosmos.svg"; import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; -import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateOffer } from "../../../Common/dataAccess/updateOffer"; +import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../../UserContext"; -import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; +import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { PartitionKeyComponent, @@ -65,16 +65,16 @@ import { AddMongoIndexProps, ChangeFeedPolicyState, GeospatialConfigType, - MongoIndexTypes, - SettingsV2TabTypes, - TtlType, getMongoNotification, getTabTitle, hasDatabaseSharedThroughput, isDataMaskingEnabled, isDirty, + MongoIndexTypes, parseConflictResolutionMode, parseConflictResolutionProcedure, + SettingsV2TabTypes, + TtlType, } from "./SettingsUtils"; interface SettingsV2TabInfo { tab: SettingsV2TabTypes; @@ -103,6 +103,8 @@ export interface SettingsComponentState { throughputBuckets: DataModels.ThroughputBucket[]; throughputBucketsBaseline: DataModels.ThroughputBucket[]; throughputError: string; + hotPartitionKeyRateLimitingPolicy: DataModels.HotPartitionKeyRateLimitingPolicy; + hotPartitionKeyRateLimitingPolicyBaseline: DataModels.HotPartitionKeyRateLimitingPolicy; timeToLive: TtlType; timeToLiveBaseline: TtlType; @@ -225,6 +227,8 @@ export class SettingsComponent extends React.Component { + this.setState({ hotPartitionKeyRateLimitingPolicy }); + }; + private onAutoPilotSelected = (isAutoPilotSelected: boolean): void => this.setState({ isAutoPilotSelected: isAutoPilotSelected }); @@ -999,6 +1014,9 @@ export class SettingsComponent extends React.Component { onScaleDiscardableChange: () => { return; }, + onHotPartitionKeyRateLimitingPolicyChange: () => { + return; + }, }; it("autoScale disabled", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 1a0aed184..1cafc5633 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -2,7 +2,7 @@ import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluen import { Keys, t } from "Localization"; import * as React from "react"; import * as Constants from "../../../../Common/Constants"; -import { Platform, configContext } from "../../../../ConfigContext"; +import { configContext, Platform } from "../../../../ConfigContext"; import * as DataModels from "../../../../Contracts/DataModels"; import * as ViewModels from "../../../../Contracts/ViewModels"; import * as SharedConstants from "../../../../Shared/Constants"; @@ -36,6 +36,9 @@ export interface ScaleComponentProps { onScaleSaveableChange: (isScaleSaveable: boolean) => void; onScaleDiscardableChange: (isScaleDiscardable: boolean) => void; throughputError?: string; + hotPartitionKeyRateLimitingPolicy?: DataModels.HotPartitionKeyRateLimitingPolicy; + hotPartitionKeyRateLimitingPolicyBaseline?: DataModels.HotPartitionKeyRateLimitingPolicy; + onHotPartitionKeyRateLimitingPolicyChange: (newPolicy: DataModels.HotPartitionKeyRateLimitingPolicy) => void; } export class ScaleComponent extends React.Component { @@ -148,6 +151,9 @@ export class ScaleComponent extends React.Component { instantMaximumThroughput={this.offer?.instantMaximumThroughput} softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput} isGlobalSecondaryIndex={this.props.isGlobalSecondaryIndex} + hotPartitionKeyRateLimitingPolicy={this.props.hotPartitionKeyRateLimitingPolicy} + hotPartitionKeyRateLimitingPolicyBaseline={this.props.hotPartitionKeyRateLimitingPolicyBaseline} + onHotPartitionKeyRateLimitingPolicyChange={this.props.onHotPartitionKeyRateLimitingPolicyChange} /> ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index a02d907b3..6738c9e80 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -1,12 +1,24 @@ import { shallow } from "enzyme"; import React from "react"; +import * as Constants from "../../../../../Common/Constants"; import * as DataModels from "../../../../../Contracts/DataModels"; +import { updateUserContext } from "../../../../../UserContext"; import { ThroughputInputAutoPilotV3Component, ThroughputInputAutoPilotV3Props, } from "./ThroughputInputAutoPilotV3Component"; describe("ThroughputInputAutoPilotV3Component", () => { + beforeAll(() => { + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: Constants.CapabilityNames.EnableHotPartitionKeyThrottling }], + }, + } as DataModels.DatabaseAccount, + }); + }); + const baseProps: ThroughputInputAutoPilotV3Props = { databaseAccount: {} as DataModels.DatabaseAccount, databaseName: "test", @@ -45,6 +57,9 @@ describe("ThroughputInputAutoPilotV3Component", () => { instantMaximumThroughput: 5000, softAllowedMaximumThroughput: 1000000, isGlobalSecondaryIndex: false, + onHotPartitionKeyRateLimitingPolicyChange: () => { + return; + }, }; it("throughput input visible", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 816c05299..4a9d0c0f5 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -12,9 +12,11 @@ import { MessageBarType, ProgressIndicator, Separator, + Slider, Stack, Text, TextField, + Toggle, } from "@fluentui/react"; import { Keys, t } from "Localization"; import React from "react"; @@ -25,10 +27,10 @@ import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryPr import { userContext } from "../../../../../UserContext"; import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils"; import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils"; +import { isHotPartitionKeyThrottlingEnabled } from "../../../../../Utils/CapabilityUtils"; import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { - PriceBreakdown, checkBoxAndInputStackProps, getChoiceGroupStyles, getEstimatedSpendingElement, @@ -40,11 +42,12 @@ import { getUpdateThroughputBeyondSupportLimitMessage, manualToAutoscaleDisclaimerElement, noLeftPaddingCheckBoxStyle, + PriceBreakdown, relaxedSpacingStackProps, saveThroughputWarningMessage, titleAndInputStackProps, } from "../../SettingsRenderUtils"; -import { IsComponentDirtyResult, getSanitizedInputValue, isDirty } from "../../SettingsUtils"; +import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils"; import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; export interface ThroughputInputAutoPilotV3Props { @@ -82,6 +85,9 @@ export interface ThroughputInputAutoPilotV3Props { instantMaximumThroughput: number; softAllowedMaximumThroughput: number; isGlobalSecondaryIndex: boolean; + hotPartitionKeyRateLimitingPolicy?: DataModels.HotPartitionKeyRateLimitingPolicy; + hotPartitionKeyRateLimitingPolicyBaseline?: DataModels.HotPartitionKeyRateLimitingPolicy; + onHotPartitionKeyRateLimitingPolicyChange: (newPolicy: DataModels.HotPartitionKeyRateLimitingPolicy) => void; } interface ThroughputInputAutoPilotV3State { @@ -137,6 +143,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< if (this.hasProvisioningTypeChanged()) { isSaveable = true; isDiscardable = true; + } else if ( + isHotPartitionKeyThrottlingEnabled() && + isDirty(this.props.hotPartitionKeyRateLimitingPolicy, this.props.hotPartitionKeyRateLimitingPolicyBaseline) + ) { + isSaveable = true; + isDiscardable = true; } else if (this.props.isAutoPilotSelected) { if (isDirty(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)) { isDiscardable = true; @@ -865,6 +877,50 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< ); }; + private renderPartitionKeyRateLimitingPolicy = (): JSX.Element => { + return ( + + + + { + if (checked) { + this.props.onHotPartitionKeyRateLimitingPolicyChange({ + maximumPerPartitionKeyThroughputUtilizationPercent: + this.props.hotPartitionKeyRateLimitingPolicyBaseline + ?.maximumPerPartitionKeyThroughputUtilizationPercent ?? 75, + }); + } else { + this.props.onHotPartitionKeyRateLimitingPolicyChange(null); + } + }} + /> + + `${value} percent`} + valueFormat={(value: number) => `${value}%`} + showValue + value={this.props.hotPartitionKeyRateLimitingPolicy?.maximumPerPartitionKeyThroughputUtilizationPercent ?? 75} + onChange={(value: number) => + this.props.onHotPartitionKeyRateLimitingPolicyChange({ + maximumPerPartitionKeyThroughputUtilizationPercent: value, + }) + } + /> + + ); + }; + public render(): JSX.Element { return ( @@ -872,6 +928,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< {this.renderThroughputModeChoices()} {this.renderThroughputComponent()} + {isHotPartitionKeyThrottlingEnabled() && this.renderPartitionKeyRateLimitingPolicy()} ); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index 9945e37b4..f6b97c2c8 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -797,6 +797,44 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` + + + + + + + `; @@ -1372,6 +1410,44 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = ` + + + + + + + `; @@ -1930,5 +2006,43 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` + + + + + + + `; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 5d8e2766c..a8dbf5d6e 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -3,8 +3,8 @@ import * as Constants from "../../../Common/Constants"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; import { userContext } from "../../../UserContext"; -import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; +import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils"; const zeroValue = 0; export type isDirtyTypes = @@ -17,7 +17,8 @@ export type isDirtyTypes = | DataModels.VectorIndex[] | DataModels.FullTextPolicy | DataModels.ThroughputBucket[] - | DataModels.DataMaskingPolicy; + | DataModels.DataMaskingPolicy + | DataModels.HotPartitionKeyRateLimitingPolicy; export const TtlOff = "off"; export const TtlOn = "on"; export const TtlOnNoDefault = "on-nodefault"; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 53ab9c658..6fe99c744 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -179,10 +179,13 @@ exports[`SettingsComponent renders 1`] = ` "vectorEmbeddingPolicy": [Function], } } + hotPartitionKeyRateLimitingPolicy={null} + hotPartitionKeyRateLimitingPolicyBaseline={null} isAutoPilotSelected={false} isFixedContainer={false} isGlobalSecondaryIndex={true} onAutoPilotSelected={[Function]} + onHotPartitionKeyRateLimitingPolicyChange={[Function]} onMaxAutoPilotThroughputChange={[Function]} onScaleDiscardableChange={[Function]} onScaleSaveableChange={[Function]} diff --git a/src/Localization/en/Resources.json b/src/Localization/en/Resources.json index 6ac748936..60b50f63d 100644 --- a/src/Localization/en/Resources.json +++ b/src/Localization/en/Resources.json @@ -894,7 +894,9 @@ "autoScaleCustomSettings": "Your account has custom settings that prevents setting throughput at the container level. Please work with your Cosmos DB engineering team point of contact to make changes.", "keyspaceSharedThroughput": "This table shared throughput is configured at the keyspace", "throughputRangeLabel": "Throughput ({{min}} - {{max}} RU/s)", - "unlimited": "unlimited" + "unlimited": "unlimited", + "rateLimitingPolicyTitle": "Rate limiting policy", + "rateLimitPolicyMaxThroughputUtilizationLabel": "Max per partition key Throughput utilization" }, "partitionKeyEditor": { "changePartitionKey": "Change {{partitionKeyName}}", diff --git a/src/Utils/CapabilityUtils.test.ts b/src/Utils/CapabilityUtils.test.ts new file mode 100644 index 000000000..0ce7c9a21 --- /dev/null +++ b/src/Utils/CapabilityUtils.test.ts @@ -0,0 +1,47 @@ +import * as Constants from "../Common/Constants"; +import { DatabaseAccount } from "../Contracts/DataModels"; +import { updateUserContext } from "../UserContext"; +import { isHotPartitionKeyThrottlingEnabled } from "./CapabilityUtils"; + +describe("CapabilityUtils", () => { + describe("isHotPartitionKeyThrottlingEnabled", () => { + it("returns true for a SQL account with the EnableHotPartitionKeyThrottling capability", () => { + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: Constants.CapabilityNames.EnableHotPartitionKeyThrottling }], + }, + } as DatabaseAccount, + }); + + expect(isHotPartitionKeyThrottlingEnabled()).toBe(true); + }); + + it("returns false for a SQL account without the capability", () => { + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: Constants.CapabilityNames.EnableAutoScale }], + }, + } as DatabaseAccount, + }); + + expect(isHotPartitionKeyThrottlingEnabled()).toBe(false); + }); + + it("returns false for a non-SQL account even with the capability", () => { + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [ + { name: Constants.CapabilityNames.EnableCassandra }, + { name: Constants.CapabilityNames.EnableHotPartitionKeyThrottling }, + ], + }, + } as DatabaseAccount, + }); + + expect(isHotPartitionKeyThrottlingEnabled()).toBe(false); + }); + }); +}); diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index de341bb13..eb9162fb2 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -34,3 +34,10 @@ export const isFullTextSearchPreviewFeaturesEnabled = (targetAccountOverride?: A isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearchPreviewFeatures, targetAccountOverride) ); }; + +export const isHotPartitionKeyThrottlingEnabled = (targetAccountOverride?: AccountOverride): boolean => { + return ( + userContext.apiType === "SQL" && + isCapabilityEnabled(Constants.CapabilityNames.EnableHotPartitionKeyThrottling, targetAccountOverride) + ); +}; diff --git a/src/Utils/arm/generatedClients/cosmos/types.ts b/src/Utils/arm/generatedClients/cosmos/types.ts index 0e74697d2..df8242c97 100644 --- a/src/Utils/arm/generatedClients/cosmos/types.ts +++ b/src/Utils/arm/generatedClients/cosmos/types.ts @@ -1096,6 +1096,11 @@ export interface CassandraViewCreateUpdateProperties { options?: CreateUpdateOptions; } +export interface HotPartitionKeyRateLimitingPolicy { + /* Maximum throughput utilization for partition keys (in percent) */ + maximumPerPartitionKeyThroughputUtilizationPercent: number; +} + /* Cosmos DB resource throughput object. Either throughput is required or autoscaleSettings is required, but not both. */ export interface ThroughputSettingsResource { /* Value of the Cosmos DB resource throughput. Either throughput is required or autoscaleSettings is required, but not both. */ @@ -1113,6 +1118,8 @@ export interface ThroughputSettingsResource { readonly softAllowedMaximumThroughput?: string; /* Array of throughput bucket limits to be applied to the Cosmos DB container */ throughputBuckets?: ThroughputBucketResource[]; + /* Object describing the Rate Limiting policy for Hot Partition Keys */ + hotPartitionKeyRateLimitingPolicy?: HotPartitionKeyRateLimitingPolicy | null; } /* Cosmos DB provisioned throughput settings object */ diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 876d2334e..2b93dff66 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -98,6 +98,7 @@ "./src/Utils/Base64Utils.ts", "./src/Utils/BlobUtils.ts", "./src/Utils/CapabilityUtils.ts", + "./src/Utils/CapabilityUtils.test.ts", "./src/Utils/CloudUtils.ts", "./src/Utils/EndpointUtils.ts", "./src/Utils/GitHubUtils.test.ts", From eec68b1e3566df4b8ebed863fd7e8ae116099834 Mon Sep 17 00:00:00 2001 From: Chuck Skelton Date: Tue, 16 Jun 2026 18:35:55 -0700 Subject: [PATCH 2/2] Minor visual updates --- .../ThroughputInputAutoPilotV3Component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 4a9d0c0f5..e3c114d04 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -879,8 +879,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< private renderPartitionKeyRateLimitingPolicy = (): JSX.Element => { return ( - - + +