[Perf framework] Use Proxy tool to mock/replay the requests (#16518)
* test-proxy in options * recordingClient from recorder-new * getRecordingClient public method * blobServiceClient uses getRecordingClient * not browser * checkpoint * add login steps in getting started * checkpoint * NaN bug fix * remove temp-location note * logs and minor tweaks - some bug at playback * remove logs * update options * rushx format * fix sendRequest * removing unneeded details * getting started and removing console.log * improve type readability * do not redeclare static PerfStressTest.recorder * rushx format * simplified tsconfig * create project * create project * simple and batch tests * core-v2 prototype with addPolicy * readem disclaimer * Address Mike's feedback * rename recorder to testProxyHttpClient * rename * fix build failures * rushx format * fix test * readme and getting started * pnpm-lock file * rename policy * swap v1 and v2 * bug fix * update getting started * update getting started * rushx format * changelog * Scott says no need to login anymore * Add workflow in comments * description * pnpm-lock file * RecordingStateManager * URLBuilder -> URL * rushx format * readme * lock file * Update sdk/test-utils/perfstress/src/options.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * configureClientOptionsCoreV1 & configureClient * update types * comments * getting started * testProxyClient is not set, please make sure the client/options are configured properly. * if (!request.headers.get("x-recording-upstream-base-uri")) set upstream uri * Add undici * Investigate hanging docker or image (#33) * getting started * testProxyClient is not set, please make sure the client/options are configured properly. * if (!request.headers.get("x-recording-upstream-base-uri")) set upstream uri * Add undici * checkpoint * make core-v2 client identical to core-v1 except for sendReq * update error message * formatting * TestProxyHttpClientV1 depends on V2 * rushx format * Mike's final minor feedback * For corev1, extend TestProxyHttpClient instead of DefaultHttpClient * update tests file * no instaceof checks * move to http.request * Jeff's feedback * CachedProxyClients * keep clients on the test class * bad merge conflict resolution * Update sdk/test-utils/perfstress/GettingStarted.md * final minor feedback * remove CachedProxyClients wrapper Co-authored-by: Jose Manuel Heredia Hidalgo <joheredi@microsoft.com> Co-authored-by: Mike Harder <mharder@microsoft.com>
This commit is contained in:
Родитель
a78a4c8b8f
Коммит
f931704f53
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -27,7 +27,7 @@ export abstract class StorageBlobTest<TOptions> extends PerfStressTest<TOptions>
|
|||
getValueInConnString(connectionString, "AccountName"),
|
||||
getValueInConnString(connectionString, "AccountKey")
|
||||
);
|
||||
this.blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
|
||||
this.blobServiceClient = BlobServiceClient.fromConnectionString(connectionString, this.configureClientOptionsCoreV1({}));
|
||||
this.containerClient = this.blobServiceClient.getContainerClient(StorageBlobTest.containerName);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.package",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"declarationDir": "./typings/latest",
|
||||
"outDir": "./dist-esm",
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
2. Copy the `sample.env` file and name it as `.env`.
|
||||
3. Create a Storage or CosmosDB account and populate the `.env` file with the relevant credentials.
|
||||
4. Refer to the [Storage](https://docs.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#storage-limits) or [CosmosDB](https://docs.microsoft.com/azure/cosmos-db/concepts-limits) rate limits and then run the tests as follows
|
||||
- `npm run perf-test:node -- CreateSimpleEntityTest`
|
||||
- `npm run perf-test:node -- CreateSimpleEntityBatchTest`
|
||||
- `npm run perf-test:node -- CreateComplexEntityTest`
|
||||
- `npm run perf-test:node -- CreateComplexEntityBatchTest`
|
||||
- `npm run perf-test:node -- ListSimpleEntitiesTest`
|
||||
- `npm run perf-test:node -- ListComplexEntitiesTest`
|
||||
- `npm run perf-test:node -- CreateSimpleEntityTest`
|
||||
- `npm run perf-test:node -- CreateSimpleEntityBatchTest`
|
||||
- `npm run perf-test:node -- CreateComplexEntityTest`
|
||||
- `npm run perf-test:node -- CreateComplexEntityBatchTest`
|
||||
- `npm run perf-test:node -- ListSimpleEntitiesTest`
|
||||
- `npm run perf-test:node -- ListComplexEntitiesTest`
|
||||
|
|
|
@ -9,7 +9,7 @@ export abstract class TablesTest<TOptions = Record<string, unknown>> extends Per
|
|||
constructor(tableName: string) {
|
||||
super();
|
||||
const connectionString = getEnvVar("SAS_CONNECTION_STRING");
|
||||
this.client = TableClient.fromConnectionString(connectionString, tableName);
|
||||
this.client = this.configureClient(TableClient.fromConnectionString(connectionString, tableName));
|
||||
}
|
||||
|
||||
public async globalSetup() {
|
||||
|
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
## 1.0.0 (Unreleased)
|
||||
|
||||
### 2021-08-05
|
||||
|
||||
- Adds test-proxy tool support to the perf framework. With this, the tests can avoid service throttling by hitting the test-proxy instead to get the recorded responses.
|
||||
[#16518](https://github.com/Azure/azure-sdk-for-js/pull/16518)
|
||||
|
||||
### 2021-07-26
|
||||
|
||||
- Average number of requests so far was reported as NaN when the lastMillisecondsElapsed=0.
|
||||
Fixed in [#16583](https://github.com/Azure/azure-sdk-for-js/pull/16583)
|
||||
|
||||
### 2021-07-14
|
||||
|
||||
- Removed the run method in the `PerfStressTest` class as we only deal with the async methods when it comes to performance.
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
- [Command to run](#command-to-run)
|
||||
- [Adding Readme/Instructions](#adding-readme/instructions)
|
||||
- [Testing an older track 2 version](#testing-an-older-track-2-version)
|
||||
- [Using Proxy Tool](#using-proxy-tool)
|
||||
|
||||
## [Setting up the project](#setting-up-the-project)
|
||||
|
||||
|
@ -261,3 +262,47 @@ Example: Currently `@azure/<service-sdk>` is at 12.4.0 on master and you want to
|
|||
- Navigate to `sdk\storage\perf-tests\<service-sdk>`
|
||||
- `rush build -t perf-test-<service-sdk>`
|
||||
- Run the tests as suggested before, example `npm run perf-test:node -- TestClassName --warmup 2 --duration 7 --iterations 2 --parallel 50`
|
||||
|
||||
## [Using Proxy Tool](#using-proxy-tool)
|
||||
|
||||
### Using the testProxy option
|
||||
|
||||
To be able to leverage the powers of playing back the requests using the test proxy, add the following to your code.
|
||||
|
||||
```ts
|
||||
/// Core V1 SDKs - For services depending on core-http
|
||||
/// Call this.configureClientOptionsCoreV1 method on your client options
|
||||
this.blobServiceClient = BlobServiceClient.fromConnectionString(connectionString, this.configureClientOptionsCoreV1({}));
|
||||
|
||||
/// Core V2 SDKs - For services depending on core-rest-pipeline
|
||||
/// this.configureClient call to modify your client
|
||||
this.client = this.configureClient(TableClient.fromConnectionString(connectionString, tableName));
|
||||
|
||||
// Not all core-v1 SDKs allow passing httpClient option.
|
||||
// Not all core-v2 SDKs allow adding policies via pipeline option.
|
||||
// Please reach out if your service doesn't support.
|
||||
```
|
||||
|
||||
### Running the proxy server
|
||||
|
||||
Run this command
|
||||
|
||||
- `docker run -p 5000:5000 azsdkengsys.azurecr.io/engsys/ubuntu_testproxy_server:latest`
|
||||
|
||||
Reference: https://github.com/Azure/azure-sdk-tools/tree/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy#via-docker-image
|
||||
|
||||
To use the proxy-tool in your test pass this option in cli `--test-proxy http://localhost:5000`(Make sure the port is same as what you have used to run the `docker run` command).
|
||||
|
||||
Sample command(using storage-blob perf tests as example (Core-v1)!)
|
||||
|
||||
> npm run perf-test:node -- StorageBlobDownloadTest --warmup 2 --duration 7 --iterations 2 --test-proxy http://localhost:5000
|
||||
|
||||
> npm run perf-test:node -- StorageBlobDownloadTest --warmup 2 --duration 7 --iterations 2 --parallel 2 --test-proxy http://localhost:5000
|
||||
|
||||
Sample command(using data-tables perf tests as example (Core-v2)!)
|
||||
|
||||
> npm run perf-test:node -- ListComplexEntitiesTest --duration 7 --iterations 2 --parallel 2 --test-proxy http://localhost:5000
|
||||
|
||||
> npm run perf-test:node -- ListComplexEntitiesTest --duration 7 --iterations 2 --parallel 2
|
||||
|
||||
**Using proxy-tool** part is still under construction. Please reach out to the owners/team if you face issues.
|
||||
|
|
|
@ -9,12 +9,37 @@ Link to the wiki - [Writing-Performance-Tests](https://github.com/Azure/azure-sd
|
|||
## KeyConcepts
|
||||
|
||||
- A **PerfStressTest** test is a test that will be executed repeatedly to show both the performance of the program, and how it behaves under stress.
|
||||
- Tests can have both a synchronous method called `run`, and an asynchronous method called `runAsync`. By default, `runAsync` will be the only method executed. If the command line parameter `--sync` is passed, only the `run` method will be executed instead.
|
||||
- Tests have an asynchronous method called `runAsync` which is executed based on the duration, iterations, and parallel options provided for the perf test. More about options below.
|
||||
- A **PerfStressOption** is a command line parameter. We use `minimist` to parse them appropriately, and then to consolidate them in a dictionary of options that is called `PerfStressOptionDictionary<string>`. The dictionary class accepts a union type of strings that defines the options that are allowed by each test.
|
||||
- Some default options are parsed by the PerfStress program. Their longer names are: `help`, `no-cleanups`, `parallel`, `duration`, `warmup`, `iterations`, `no-cleanup` and `milliseconds-to-log`.
|
||||
- PerfStress tests are executed as many times as possible until the `duration` parameter is specified. This process may repeat as many `iterations` are given. Before each iteration, tests might be called for a period of time up to `warmup`, to adjust to possible runtime optimizations. In each iteration, as many as `parallel` instances of the same test are called without waiting for each other, letting the event loop decide which one is prioritized (it's not true parallelism, but it's an approximation that aligns with the design in other languages, we might improve it over time).
|
||||
- Each test can have a `globalSetup` method, which is called once before the process begins, a `globalCleanup` method, which is called once after the process finishes.
|
||||
- Each test can have a `setup` method, which is called as many times as test instances are created (up to `parallel`), and help specify local state for each test instance. A `cleanup` method is also optional, called the same amount of times, but after finishing running the tests.
|
||||
- `test-proxy` url option - this option can be leveraged to avoid hitting throttling scenarios while testing the services. This option lets the requests go through the proxy server based on the url provided, we run runAsync method once in record mode to save the requests and responses in memory and then a ton of times in playback. Workflow with the test-proxy below.
|
||||
|
||||
## Workflow with test proxy
|
||||
|
||||
Steps below constitute the workflow of a typical perf test.
|
||||
|
||||
- test resources are setup
|
||||
- hitting the live service
|
||||
- then start record
|
||||
- making a request to the proxy server to start recording
|
||||
- proxy server gives a recording id, we'll use this id to save the actual requests and responses
|
||||
- run the runAsync once
|
||||
- proxy-server saves all the requests and responses in memory
|
||||
- stop record
|
||||
- making a request to the proxy server to stop recording
|
||||
- start playback
|
||||
- making a request to the proxy server to start playback
|
||||
- we use the same recording-id that we used in the record mode since that's the only way proxy-server knows what requests are supposed to be played back
|
||||
- As a response, we get a new recording-id, which will be used for future playback requests
|
||||
- run runAsync again
|
||||
- based on the duration, iterations, and parallel options provided for the perf test
|
||||
- all the requests in the runAsync method are played back since we have already recorded them before
|
||||
- when the runAsync loops end, stop playback
|
||||
- making a request to the proxy server to stop playing back
|
||||
- delete the live resources that we have created before
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-http": "^2.0.0",
|
||||
"@azure/core-rest-pipeline": "^1.1.0",
|
||||
"tslib": "^2.2.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"minimist": "~1.2.5",
|
||||
|
|
|
@ -65,6 +65,7 @@ export interface DefaultPerfStressOptions {
|
|||
iterations: number;
|
||||
"no-cleanup": boolean;
|
||||
"milliseconds-to-log": number;
|
||||
"test-proxy": string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,6 +99,10 @@ export const defaultPerfStressOptions: PerfStressOptionDictionary<DefaultPerfStr
|
|||
"no-cleanup": {
|
||||
description: "Disables test cleanup"
|
||||
},
|
||||
"test-proxy": {
|
||||
description: "URI of TestProxy server",
|
||||
defaultValue: undefined
|
||||
},
|
||||
"milliseconds-to-log": {
|
||||
description: "Log frequency in milliseconds",
|
||||
shortName: "mtl",
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
DefaultPerfStressOptions
|
||||
} from "./options";
|
||||
import { PerfStressParallel } from "./parallel";
|
||||
import { TestProxyHttpClientV1, TestProxyHttpClient } from "./testProxyHttpClient";
|
||||
|
||||
export type TestType = "";
|
||||
|
||||
|
@ -101,13 +102,17 @@ export class PerfStressProgram {
|
|||
const secondsPerOperation = 1 / operationsPerSecond;
|
||||
const weightedAverage = totalOperations / operationsPerSecond;
|
||||
console.log(
|
||||
`Completed ${totalOperations.toLocaleString(undefined, { maximumFractionDigits: 0 })} ` +
|
||||
`Completed ${totalOperations.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0
|
||||
})} ` +
|
||||
`operations in a weighted-average of ` +
|
||||
`${weightedAverage.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 2
|
||||
})}s ` +
|
||||
`(${operationsPerSecond.toLocaleString(undefined, { maximumFractionDigits: 2 })} ops/s, ` +
|
||||
`(${operationsPerSecond.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2
|
||||
})} ops/s, ` +
|
||||
`${secondsPerOperation.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 3,
|
||||
minimumFractionDigits: 3
|
||||
|
@ -281,6 +286,10 @@ export class PerfStressProgram {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.tests[0].parsedOptions["test-proxy"].value) {
|
||||
await this.recordAndStartPlayback(this.tests[0]);
|
||||
}
|
||||
|
||||
if (Number(options.warmup.value) > 0) {
|
||||
await this.runTest(0, Number(options.warmup.value), "warmup");
|
||||
}
|
||||
|
@ -290,6 +299,10 @@ export class PerfStressProgram {
|
|||
await this.runTest(i, Number(options.duration.value), "test");
|
||||
}
|
||||
|
||||
if (this.tests[0].parsedOptions["test-proxy"].value) {
|
||||
await this.stopPlayback(this.tests[0]);
|
||||
}
|
||||
|
||||
if (!options["no-cleanup"].value && this.tests[0].cleanup) {
|
||||
console.log(
|
||||
`=== Calling cleanup() for the ${this.parallelNumber} instantiated ${this.testName} tests ===`
|
||||
|
@ -308,4 +321,63 @@ export class PerfStressProgram {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method records the requests-responses and lets the proxy-server know when to playback.
|
||||
* We run runAsync once in record mode to save the requests and responses in memory and then a ton of times in playback.
|
||||
*
|
||||
* ## Workflow of the perf test
|
||||
* - test resources are setup
|
||||
* - hitting the live service
|
||||
* - then start record
|
||||
* - making a request to the proxy server to start recording
|
||||
* - proxy server gives a recording id, we'll use this id to save the actual requests and responses
|
||||
* - run the runAsync once
|
||||
* - proxy-server saves all the requests and responses in memory
|
||||
* - stop record
|
||||
* - making a request to the proxy server to stop recording
|
||||
* - start playback
|
||||
* - making a request to the proxy server to start playback
|
||||
* - we use the same recording-id that we used in the record mode since that's the only way proxy-server knows what requests are supposed to be played back
|
||||
* - as a response, we get a new recording-id, which will be used for future playback requests
|
||||
* - run runAsync again
|
||||
* - based on the duration, iterations, and parallel options provided for the perf test
|
||||
* - all the requests in the runAsync method are played back since we have already recorded them before
|
||||
* - when the runAsync loops end, stop playback
|
||||
* - making a request to the proxy server to stop playing back
|
||||
* - delete the live resources that we have created before
|
||||
*/
|
||||
private async recordAndStartPlayback(test: PerfStressTest) {
|
||||
// If test-proxy,
|
||||
// => then start record
|
||||
// => run the runAsync
|
||||
// => stop record
|
||||
// => start playback
|
||||
let recorder: TestProxyHttpClientV1 | TestProxyHttpClient;
|
||||
if (test.testProxyHttpClient) {
|
||||
recorder = test.testProxyHttpClient;
|
||||
} else if (test.testProxyHttpClientV1) {
|
||||
recorder = test.testProxyHttpClientV1;
|
||||
} else {
|
||||
throw new Error(
|
||||
"testProxyClient is not set, please make sure the client/options are configured properly."
|
||||
);
|
||||
}
|
||||
|
||||
await recorder.startRecording();
|
||||
recorder._mode = "record";
|
||||
await test.runAsync!();
|
||||
|
||||
await recorder.stopRecording();
|
||||
await recorder.startPlayback();
|
||||
recorder._mode = "playback";
|
||||
}
|
||||
|
||||
private async stopPlayback(test: PerfStressTest) {
|
||||
if (test.testProxyHttpClient) {
|
||||
await test.testProxyHttpClient.stopPlayback();
|
||||
} else if (test.testProxyHttpClientV1) {
|
||||
await test.testProxyHttpClientV1.stopPlayback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { HttpClient, HttpOperationResponse } from "@azure/core-http";
|
||||
import { DefaultHttpClient, WebResourceLike } from "@azure/core-http";
|
||||
import {
|
||||
PipelinePolicy,
|
||||
PipelineRequest,
|
||||
PipelineResponse,
|
||||
SendRequest
|
||||
} from "@azure/core-rest-pipeline";
|
||||
import { RequestOptions } from "http";
|
||||
import { makeRequest } from "./utils";
|
||||
|
||||
const paths = {
|
||||
playback: "/playback",
|
||||
record: "/record",
|
||||
start: "/start",
|
||||
stop: "/stop"
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper class to manage the recording state to make sure the proxy-tool is not flooded with unintended requests.
|
||||
*
|
||||
* => then start record
|
||||
* => run the runAsync
|
||||
* => stop record
|
||||
* => start playback
|
||||
* => stop playback
|
||||
*/
|
||||
export class RecordingStateManager {
|
||||
public state:
|
||||
| "started-recording"
|
||||
| "stopped-recording"
|
||||
| "started-playback"
|
||||
| "stopped-playback"
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* validateState
|
||||
*/
|
||||
public validateState(
|
||||
currentFlow:
|
||||
| "starting-recording"
|
||||
| "stopping-recording"
|
||||
| "starting-playback"
|
||||
| "stopping-playback"
|
||||
) {
|
||||
if (currentFlow === "starting-recording") {
|
||||
if (this.state === "started-recording") {
|
||||
throw new Error("Already started recording, should not have called again.");
|
||||
}
|
||||
}
|
||||
if (currentFlow === "stopping-recording") {
|
||||
if (this.state === "stopped-recording") {
|
||||
throw new Error("Already stopped recording, should not have called again.");
|
||||
}
|
||||
if (this.state !== "started-recording") {
|
||||
throw new Error("Please start recording before calling stop.");
|
||||
}
|
||||
}
|
||||
if (currentFlow === "starting-playback") {
|
||||
if (this.state !== "stopped-recording") {
|
||||
throw new Error("Did not stop recording, stop recording before starting playback.");
|
||||
}
|
||||
}
|
||||
if (currentFlow === "stopping-playback") {
|
||||
if (this.state !== "started-playback") {
|
||||
throw new Error("Did not start playback, start playback before calling stop.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* setState
|
||||
*/
|
||||
public setState(
|
||||
state: "started-recording" | "stopped-recording" | "started-playback" | "stopped-playback"
|
||||
) {
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
export class TestProxyHttpClient {
|
||||
private _uri: string;
|
||||
public _recordingId?: string;
|
||||
public _mode!: string;
|
||||
private stateManager: RecordingStateManager = new RecordingStateManager();
|
||||
|
||||
constructor(uri: string) {
|
||||
this._uri = uri;
|
||||
}
|
||||
// For core-v1
|
||||
redirectRequest(request: WebResourceLike, recordingId: string): WebResourceLike;
|
||||
// For core-v2
|
||||
redirectRequest(request: PipelineRequest, recordingId: string): PipelineRequest;
|
||||
redirectRequest(request: WebResourceLike | PipelineRequest, recordingId: string) {
|
||||
request.headers.set("x-recording-id", recordingId);
|
||||
request.headers.set("x-recording-mode", this._mode);
|
||||
request.headers.set("x-recording-remove", "false");
|
||||
const redirectedUrl = new URL(request.url);
|
||||
const providedUrl = new URL(this._uri);
|
||||
redirectedUrl.host = providedUrl.host;
|
||||
redirectedUrl.port = providedUrl.port;
|
||||
redirectedUrl.protocol = providedUrl.protocol;
|
||||
if (!request.headers.get("x-recording-upstream-base-uri")) {
|
||||
const upstreamUrl = new URL(request.url);
|
||||
upstreamUrl.pathname = "";
|
||||
request.headers.set("x-recording-upstream-base-uri", upstreamUrl.toString());
|
||||
}
|
||||
request.url = redirectedUrl.toString();
|
||||
return request;
|
||||
}
|
||||
|
||||
async modifyRequest(request: PipelineRequest): Promise<PipelineRequest> {
|
||||
if (this._recordingId && (this._mode === "record" || this._mode === "playback")) {
|
||||
request = this.redirectRequest(request, this._recordingId);
|
||||
request.allowInsecureConnection = true;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
async startRecording(): Promise<void> {
|
||||
this.stateManager.validateState("starting-recording");
|
||||
const options = this._createRecordingRequestOptions({
|
||||
path: paths.record + paths.start
|
||||
});
|
||||
const rsp = await makeRequest(this._uri, options);
|
||||
if (rsp.statusCode !== 200) {
|
||||
throw new Error("Start request failed.");
|
||||
}
|
||||
if (!rsp.headers) {
|
||||
throw new Error("Headers are not defined, something is wrong.");
|
||||
}
|
||||
const id = rsp.headers["x-recording-id"];
|
||||
if (!id) {
|
||||
throw new Error("No recording ID returned.");
|
||||
}
|
||||
if (typeof id !== "string") {
|
||||
throw new Error("recording ID returned is not a string.");
|
||||
}
|
||||
this._recordingId = id;
|
||||
this.stateManager.setState("started-recording");
|
||||
}
|
||||
|
||||
async stopRecording(): Promise<void> {
|
||||
this.stateManager.validateState("stopping-recording");
|
||||
const options = this._createRecordingRequestOptions({
|
||||
path: paths.record + paths.stop
|
||||
});
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
"x-recording-id": this._recordingId
|
||||
};
|
||||
await makeRequest(this._uri, options);
|
||||
this.stateManager.setState("stopped-recording");
|
||||
}
|
||||
|
||||
async startPlayback(): Promise<void> {
|
||||
this.stateManager.validateState("starting-playback");
|
||||
const options = this._createRecordingRequestOptions({
|
||||
path: paths.playback + paths.start
|
||||
});
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
"x-recording-id": this._recordingId
|
||||
};
|
||||
const rsp = await makeRequest(this._uri, options);
|
||||
if (rsp.statusCode !== 200) {
|
||||
throw new Error("Start request failed.");
|
||||
}
|
||||
if (!rsp.headers) {
|
||||
throw new Error("Headers are not defined, something is wrong.");
|
||||
}
|
||||
const id = rsp.headers["x-recording-id"];
|
||||
if (!id) {
|
||||
throw new Error("No recording ID returned.");
|
||||
}
|
||||
if (typeof id !== "string") {
|
||||
throw new Error("recording ID returned is not a string.");
|
||||
}
|
||||
this._recordingId = id;
|
||||
this.stateManager.setState("started-playback");
|
||||
}
|
||||
|
||||
async stopPlayback(): Promise<void> {
|
||||
this.stateManager.validateState("stopping-playback");
|
||||
const options = this._createRecordingRequestOptions({
|
||||
path: paths.playback + paths.stop
|
||||
});
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
"x-recording-id": this._recordingId,
|
||||
"x-purge-inmemory-recording": "true"
|
||||
};
|
||||
await makeRequest(this._uri, options);
|
||||
this._mode = "live";
|
||||
this._recordingId = undefined;
|
||||
this.stateManager.setState("stopped-playback");
|
||||
}
|
||||
|
||||
private _createRecordingRequestOptions(options: RequestOptions): RequestOptions {
|
||||
if (this._recordingId !== undefined) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
"x-recording-id": this._recordingId
|
||||
};
|
||||
}
|
||||
return { ...options, method: "POST" };
|
||||
}
|
||||
}
|
||||
|
||||
export function testProxyHttpPolicy(testProxyHttpClient: TestProxyHttpClient): PipelinePolicy {
|
||||
return {
|
||||
name: "recording policy",
|
||||
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
|
||||
const modifiedRequest = await testProxyHttpClient.modifyRequest(request);
|
||||
return next(modifiedRequest);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class TestProxyHttpClientV1 extends TestProxyHttpClient {
|
||||
public _httpClient: HttpClient;
|
||||
constructor(uri: string) {
|
||||
super(uri);
|
||||
this._httpClient = new DefaultHttpClient();
|
||||
}
|
||||
|
||||
async sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
|
||||
if (this._recordingId && (this._mode === "record" || this._mode === "playback")) {
|
||||
request = this.redirectRequest(request, this._recordingId);
|
||||
}
|
||||
return await this._httpClient.sendRequest(request);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,14 @@ import {
|
|||
DefaultPerfStressOptions,
|
||||
defaultPerfStressOptions
|
||||
} from "./options";
|
||||
import {
|
||||
TestProxyHttpClient,
|
||||
TestProxyHttpClientV1,
|
||||
testProxyHttpPolicy
|
||||
} from "./testProxyHttpClient";
|
||||
import { HttpClient } from "@azure/core-http";
|
||||
import { Pipeline } from "@azure/core-rest-pipeline";
|
||||
import { getHttpClient, getHttpClientV1 } from "./utils";
|
||||
|
||||
/**
|
||||
* Defines the behavior of the PerfStressTest constructor, to use the class as a value.
|
||||
|
@ -27,6 +35,8 @@ export interface PerfStressTestConstructor<TOptions extends {} = {}> {
|
|||
* (initializations are as many as the "parallel" command line parameter specifies).
|
||||
*/
|
||||
export abstract class PerfStressTest<TOptions = {}> {
|
||||
public testProxyHttpClient!: TestProxyHttpClient;
|
||||
public testProxyHttpClientV1!: TestProxyHttpClientV1;
|
||||
public abstract options: PerfStressOptionDictionary<TOptions>;
|
||||
|
||||
public get parsedOptions(): PerfStressOptionDictionary<TOptions & DefaultPerfStressOptions> {
|
||||
|
@ -48,6 +58,38 @@ export abstract class PerfStressTest<TOptions = {}> {
|
|||
public cleanup?(): void | Promise<void>;
|
||||
|
||||
public async runAsync?(abortSignal?: AbortSignalLike): Promise<void>;
|
||||
|
||||
/**
|
||||
* configureClientOptionsCoreV1
|
||||
*
|
||||
* For core-v1 - libraries depending on core-http
|
||||
* Apply this method on the client options to get the proxy tool support
|
||||
*
|
||||
* Note: httpClient must be part of the options bag, it is required for the perf framework to update the underlying client properly
|
||||
*/
|
||||
public configureClientOptionsCoreV1<T>(options: T & { httpClient?: HttpClient }): T {
|
||||
if (this.parsedOptions["test-proxy"].value) {
|
||||
this.testProxyHttpClientV1 = getHttpClientV1(this.parsedOptions["test-proxy"].value!);
|
||||
options.httpClient = this.testProxyHttpClientV1;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* configureClient
|
||||
*
|
||||
* For core-v2 - libraries depending on core-rest-pipeline
|
||||
* Apply this method on the client to get the proxy tool support.
|
||||
*
|
||||
* Note: Client must expose the pipeline property which is required for the perf framework to add its policies correctly
|
||||
*/
|
||||
public configureClient<T>(client: T & { pipeline: Pipeline }): T {
|
||||
if (this.parsedOptions["test-proxy"].value) {
|
||||
this.testProxyHttpClient = getHttpClient(this.parsedOptions["test-proxy"].value!);
|
||||
client.pipeline.addPolicy(testProxyHttpPolicy(this.testProxyHttpClient));
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
import { IncomingMessage, RequestOptions, request } from "http";
|
||||
import { TestProxyHttpClient, TestProxyHttpClientV1 } from "./testProxyHttpClient";
|
||||
|
||||
/**
|
||||
* Returns the environment variable, throws an error if not defined.
|
||||
|
@ -28,3 +30,37 @@ export async function drainStream(stream: NodeJS.ReadableStream) {
|
|||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
export async function makeRequest(
|
||||
uri: string,
|
||||
requestOptions: RequestOptions
|
||||
): Promise<IncomingMessage> {
|
||||
return new Promise<IncomingMessage>((resolve, reject) => {
|
||||
const req = request(uri, requestOptions, resolve);
|
||||
|
||||
req.once("error", reject);
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
const _cachedProxyClients: {
|
||||
v1: TestProxyHttpClientV1 | undefined;
|
||||
v2: TestProxyHttpClient | undefined;
|
||||
} = {
|
||||
v1: undefined,
|
||||
v2: undefined
|
||||
};
|
||||
|
||||
export function getHttpClientV1(url: string): TestProxyHttpClientV1 {
|
||||
if (!_cachedProxyClients.v1) {
|
||||
_cachedProxyClients.v1 = new TestProxyHttpClientV1(url);
|
||||
}
|
||||
return _cachedProxyClients.v1;
|
||||
}
|
||||
|
||||
export function getHttpClient(url: string): TestProxyHttpClient {
|
||||
if (!_cachedProxyClients.v2) {
|
||||
_cachedProxyClients.v2 = new TestProxyHttpClient(url);
|
||||
}
|
||||
return _cachedProxyClients.v2;
|
||||
}
|
||||
|
|
|
@ -1,23 +1,10 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.package",
|
||||
"compilerOptions": {
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"newLine": "LF",
|
||||
"target": "es5",
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strict": true,
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist-esm",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"importHelpers": true,
|
||||
"declarationDir": "./typings",
|
||||
"lib": ["dom", "es5", "es6", "es7", "esnext"],
|
||||
"esModuleInterop": true
|
||||
"outDir": "./dist-esm",
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "es5", "es6", "es7", "esnext"]
|
||||
},
|
||||
"exclude": ["node_modules", "./samples/*"],
|
||||
"include": ["./src/**/*.ts", "./test/**/*.ts"]
|
||||
|
|
Загрузка…
Ссылка в новой задаче