Skip to content

Commit de2449e

Browse files
Adding throughput bucket settings in Data Explorer (#2044)
* Added throughput bucketing * fix bugs * enable/disable per autoscale selection * Added logic * change query bucket to group * Updated to a tab * Fixed unit tests * Edit package-lock * Compile build fix * fix unit tests * moving the throughput bucket flag to the client generation level
1 parent 9937858 commit de2449e

33 files changed

+560
-96
lines changed

src/Common/CosmosClient.ts

+2
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,10 @@ export function client(): Cosmos.CosmosClient {
203203
}
204204

205205
let _defaultHeaders: Cosmos.CosmosHeaders = {};
206+
206207
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
207208
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
209+
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
208210

209211
if (
210212
userContext.authType === AuthType.ConnectionString ||

src/Common/dataAccess/readCollectionOffer.ts

+4
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
105105
? parseInt(resource.softAllowedMaximumThroughput)
106106
: resource.softAllowedMaximumThroughput;
107107

108+
const throughputBuckets = resource?.throughputBuckets;
109+
108110
if (autoscaleSettings) {
109111
return {
110112
id: offerId,
@@ -114,6 +116,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
114116
offerReplacePending: resource.offerReplacePending === "true",
115117
instantMaximumThroughput,
116118
softAllowedMaximumThroughput,
119+
throughputBuckets,
117120
};
118121
}
119122

@@ -125,6 +128,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
125128
offerReplacePending: resource.offerReplacePending === "true",
126129
instantMaximumThroughput,
127130
softAllowedMaximumThroughput,
131+
throughputBuckets,
128132
};
129133
}
130134

src/Common/dataAccess/updateOffer.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
22
import { AuthType } from "../../AuthType";
3-
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
3+
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
44
import { userContext } from "../../UserContext";
55
import {
66
migrateCassandraKeyspaceToAutoscale,
@@ -359,6 +359,13 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
359359
body.properties.resource.throughput = params.manualThroughput;
360360
}
361361

362+
if (params.throughputBuckets) {
363+
const throughputBuckets = params.throughputBuckets.filter(
364+
(bucket: ThroughputBucket) => bucket.maxThroughputPercentage !== 100,
365+
);
366+
body.properties.resource.throughputBuckets = throughputBuckets;
367+
}
368+
362369
return body;
363370
};
364371

src/Contracts/DataModels.ts

+7
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ export interface Offer {
275275
offerReplacePending: boolean;
276276
instantMaximumThroughput?: number;
277277
softAllowedMaximumThroughput?: number;
278+
throughputBuckets?: ThroughputBucket[];
279+
}
280+
281+
export interface ThroughputBucket {
282+
id: number;
283+
maxThroughputPercentage: number;
278284
}
279285

280286
export interface SDKOfferDefinition extends Resource {
@@ -397,6 +403,7 @@ export interface UpdateOfferParams {
397403
collectionId?: string;
398404
migrateToAutoPilot?: boolean;
399405
migrateToManual?: boolean;
406+
throughputBuckets?: ThroughputBucket[];
400407
}
401408

402409
export interface Notification {

src/Explorer/Controls/Settings/SettingsComponent.test.tsx

+40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { AuthType } from "AuthType";
12
import { shallow } from "enzyme";
23
import ko from "knockout";
4+
import { Features } from "Platform/Hosted/extractFeatures";
35
import React from "react";
46
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
57
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
@@ -247,4 +249,42 @@ describe("SettingsComponent", () => {
247249
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
248250
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
249251
});
252+
253+
it("should save throughput bucket changes when Save button is clicked", async () => {
254+
updateUserContext({
255+
apiType: "SQL",
256+
features: { enableThroughputBuckets: true } as Features,
257+
authType: AuthType.AAD,
258+
});
259+
260+
const wrapper = shallow(<SettingsComponent {...baseProps} />);
261+
262+
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
263+
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
264+
expect(isEnabled).toBe(true);
265+
266+
wrapper.setState({
267+
isThroughputBucketsSaveable: true,
268+
throughputBuckets: [
269+
{ id: 1, maxThroughputPercentage: 70 },
270+
{ id: 2, maxThroughputPercentage: 60 },
271+
],
272+
});
273+
274+
await settingsComponentInstance.onSaveClick();
275+
276+
expect(updateOffer).toHaveBeenCalledWith({
277+
databaseId: collection.databaseId,
278+
collectionId: collection.id(),
279+
currentOffer: expect.any(Object),
280+
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
281+
manualThroughput: collection.offer().manualThroughput,
282+
throughputBuckets: [
283+
{ id: 1, maxThroughputPercentage: 70 },
284+
{ id: 2, maxThroughputPercentage: 60 },
285+
],
286+
});
287+
288+
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
289+
});
250290
});

src/Explorer/Controls/Settings/SettingsComponent.tsx

+66-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
ContainerPolicyComponent,
88
ContainerPolicyComponentProps,
99
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
10+
import {
11+
ThroughputBucketsComponent,
12+
ThroughputBucketsComponentProps,
13+
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
1014
import { useDatabases } from "Explorer/useDatabases";
1115
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
1216
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
@@ -86,6 +90,8 @@ export interface SettingsComponentState {
8690
wasAutopilotOriginallySet: boolean;
8791
isScaleSaveable: boolean;
8892
isScaleDiscardable: boolean;
93+
throughputBuckets: DataModels.ThroughputBucket[];
94+
throughputBucketsBaseline: DataModels.ThroughputBucket[];
8995
throughputError: string;
9096

9197
timeToLive: TtlType;
@@ -104,6 +110,7 @@ export interface SettingsComponentState {
104110
changeFeedPolicyBaseline: ChangeFeedPolicyState;
105111
isSubSettingsSaveable: boolean;
106112
isSubSettingsDiscardable: boolean;
113+
isThroughputBucketsSaveable: boolean;
107114

108115
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
109116
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
@@ -158,6 +165,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
158165
private isVectorSearchEnabled: boolean;
159166
private isFullTextSearchEnabled: boolean;
160167
private totalThroughputUsed: number;
168+
private throughputBucketsEnabled: boolean;
161169
public mongoDBCollectionResource: MongoDBCollectionResource;
162170

163171
constructor(props: SettingsComponentProps) {
@@ -175,6 +183,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
175183
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
176184

177185
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
186+
this.throughputBucketsEnabled =
187+
userContext.apiType === "SQL" &&
188+
userContext.features.enableThroughputBuckets &&
189+
userContext.authType === AuthType.AAD;
178190

179191
// Mongo container with system partition key still treat as "Fixed"
180192
this.isFixedContainer =
@@ -193,6 +205,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
193205
wasAutopilotOriginallySet: false,
194206
isScaleSaveable: false,
195207
isScaleDiscardable: false,
208+
throughputBuckets: undefined,
209+
throughputBucketsBaseline: undefined,
196210
throughputError: undefined,
197211

198212
timeToLive: undefined,
@@ -211,6 +225,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
211225
changeFeedPolicyBaseline: undefined,
212226
isSubSettingsSaveable: false,
213227
isSubSettingsDiscardable: false,
228+
isThroughputBucketsSaveable: false,
214229

215230
vectorEmbeddingPolicy: undefined,
216231
vectorEmbeddingPolicyBaseline: undefined,
@@ -327,7 +342,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
327342
this.state.isIndexingPolicyDirty ||
328343
this.state.isConflictResolutionDirty ||
329344
this.state.isComputedPropertiesDirty ||
330-
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
345+
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
346+
this.state.isThroughputBucketsSaveable
331347
);
332348
};
333349

@@ -339,7 +355,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
339355
this.state.isIndexingPolicyDirty ||
340356
this.state.isConflictResolutionDirty ||
341357
this.state.isComputedPropertiesDirty ||
342-
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
358+
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
359+
this.state.isThroughputBucketsSaveable
343360
);
344361
};
345362

@@ -419,6 +436,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
419436

420437
this.setState({
421438
throughput: this.state.throughputBaseline,
439+
throughputBuckets: this.state.throughputBucketsBaseline,
440+
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
422441
timeToLive: this.state.timeToLiveBaseline,
423442
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
424443
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
@@ -441,6 +460,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
441460
isScaleSaveable: false,
442461
isScaleDiscardable: false,
443462
isSubSettingsSaveable: false,
463+
isThroughputBucketsSaveable: false,
444464
isSubSettingsDiscardable: false,
445465
isContainerPolicyDirty: false,
446466
isIndexingPolicyDirty: false,
@@ -479,6 +499,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
479499
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
480500
this.setState({ indexingPolicyContent: newIndexingPolicy });
481501

502+
private onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
503+
this.setState({ isThroughputBucketsSaveable: isSaveable });
504+
};
505+
482506
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
483507

484508
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
@@ -749,9 +773,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
749773
] as DataModels.ComputedProperties;
750774
}
751775

776+
const throughputBuckets = this.offer?.throughputBuckets;
777+
752778
return {
753779
throughput: offerThroughput,
754780
throughputBaseline: offerThroughput,
781+
throughputBuckets,
782+
throughputBucketsBaseline: throughputBuckets,
755783
changeFeedPolicy: changeFeedPolicy,
756784
changeFeedPolicyBaseline: changeFeedPolicy,
757785
timeToLive: timeToLive,
@@ -839,6 +867,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
839867
this.setState({ throughput: newThroughput, throughputError });
840868
};
841869

870+
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
871+
this.setState({ throughputBuckets });
872+
};
873+
842874
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
843875
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
844876

@@ -1029,6 +1061,24 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
10291061
}
10301062
}
10311063

1064+
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
1065+
const updatedOffer: DataModels.Offer = await updateOffer({
1066+
databaseId: this.collection.databaseId,
1067+
collectionId: this.collection.id(),
1068+
currentOffer: this.collection.offer(),
1069+
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
1070+
? this.collection.offer().autoscaleMaxThroughput
1071+
: undefined,
1072+
manualThroughput: this.collection.offer().manualThroughput
1073+
? this.collection.offer().manualThroughput
1074+
: undefined,
1075+
throughputBuckets: this.state.throughputBuckets,
1076+
});
1077+
this.collection.offer(updatedOffer);
1078+
this.offer = updatedOffer;
1079+
this.setState({ isThroughputBucketsSaveable: false });
1080+
}
1081+
10321082
if (this.state.isScaleSaveable) {
10331083
const updateOfferParams: DataModels.UpdateOfferParams = {
10341084
databaseId: this.collection.databaseId,
@@ -1209,6 +1259,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
12091259
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
12101260
};
12111261

1262+
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
1263+
currentBuckets: this.state.throughputBuckets,
1264+
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
1265+
onBucketsChange: this.onThroughputBucketChange,
1266+
onSaveableChange: this.onThroughputBucketsSaveableChange,
1267+
};
1268+
12121269
const partitionKeyComponentProps: PartitionKeyComponentProps = {
12131270
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
12141271
collection: this.collection,
@@ -1271,6 +1328,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
12711328
});
12721329
}
12731330

1331+
if (this.throughputBucketsEnabled) {
1332+
tabs.push({
1333+
tab: SettingsV2TabTypes.ThroughputBucketsTab,
1334+
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
1335+
});
1336+
}
1337+
12741338
const pivotProps: IPivotProps = {
12751339
onLinkClick: this.onPivotChange,
12761340
selectedKey: SettingsV2TabTypes[this.state.selectedTab],

0 commit comments

Comments
 (0)