Skip to content

Commit 4230ce1

Browse files
[load] support multi-region load test configuration
1 parent 63fda5c commit 4230ce1

15 files changed

+3007
-7
lines changed

src/load/azext_load/data_plane/load_test/custom.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def create_test(
4646
autostop=None,
4747
autostop_error_rate=None,
4848
autostop_error_rate_time_window=None,
49+
regionwise_engines=None,
4950
):
5051
client = get_admin_data_plane_client(cmd, load_test_resource, resource_group_name)
5152
logger.info("Create test has started for test ID : %s", test_id)
@@ -79,6 +80,7 @@ def create_test(
7980
split_csv=split_csv,
8081
disable_public_ip=disable_public_ip,
8182
autostop_criteria=autostop_criteria,
83+
regionwise_engines=regionwise_engines,
8284
)
8385
else:
8486
yaml = load_yaml(load_test_config_file)
@@ -97,7 +99,8 @@ def create_test(
9799
subnet_id=subnet_id,
98100
split_csv=split_csv,
99101
disable_public_ip=disable_public_ip,
100-
autostop_criteria=autostop_criteria
102+
autostop_criteria=autostop_criteria,
103+
regionwise_engines=regionwise_engines,
101104
)
102105
logger.debug("Creating test with test ID: %s and body : %s", test_id, body)
103106
response = client.create_or_update_test(test_id=test_id, body=body)
@@ -135,6 +138,7 @@ def update_test(
135138
autostop=None,
136139
autostop_error_rate=None,
137140
autostop_error_rate_time_window=None,
141+
regionwise_engines=None,
138142
):
139143
client = get_admin_data_plane_client(cmd, load_test_resource, resource_group_name)
140144
logger.info("Update test has started for test ID : %s", test_id)
@@ -166,7 +170,8 @@ def update_test(
166170
subnet_id=subnet_id,
167171
split_csv=split_csv,
168172
disable_public_ip=disable_public_ip,
169-
autostop_criteria=autostop_criteria
173+
autostop_criteria=autostop_criteria,
174+
regionwise_engines=regionwise_engines,
170175
)
171176
else:
172177
body = create_or_update_test_without_config(
@@ -182,7 +187,8 @@ def update_test(
182187
subnet_id=subnet_id,
183188
split_csv=split_csv,
184189
disable_public_ip=disable_public_ip,
185-
autostop_criteria=autostop_criteria
190+
autostop_criteria=autostop_criteria,
191+
regionwise_engines=regionwise_engines,
186192
)
187193
logger.info("Updating test with test ID: %s", test_id)
188194
response = client.create_or_update_test(test_id=test_id, body=body)

src/load/azext_load/data_plane/load_test/params.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def load_arguments(self, _):
3232
c.argument("autostop", argtypes.autostop)
3333
c.argument("autostop_error_rate", argtypes.autostop_error_rate)
3434
c.argument("autostop_error_rate_time_window", argtypes.autostop_error_rate_time_window)
35+
c.argument("regionwise_engines", argtypes.regionwise_engines)
3536

3637
with self.argument_context("load test update") as c:
3738
c.argument("load_test_config_file", argtypes.load_test_config_file)
@@ -52,6 +53,7 @@ def load_arguments(self, _):
5253
c.argument("autostop", argtypes.autostop)
5354
c.argument("autostop_error_rate", argtypes.autostop_error_rate)
5455
c.argument("autostop_error_rate_time_window", argtypes.autostop_error_rate_time_window)
56+
c.argument("regionwise_engines", argtypes.regionwise_engines)
5557

5658
with self.argument_context("load test download-files") as c:
5759
c.argument("path", argtypes.dir_path)

src/load/azext_load/data_plane/utils/argtypes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,10 @@
362362
validator=validators.validate_autostop_error_rate_time_window,
363363
help="Time window during which the error percentage should be evaluated in seconds.",
364364
)
365+
366+
regionwise_engines = CLIArgumentType(
367+
options_list=["--regionwise-engines"],
368+
validator=validators.validate_regionwise_engines,
369+
nargs="+",
370+
help="Regionwise engine count in the format of region1=engineCount1 region2=engineCount2 ... The region name should be of format accepted by ARM, and should be a region supported by Azure Load Testing.",
371+
)

src/load/azext_load/data_plane/utils/utils.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,21 @@ def parse_env(envs):
257257
return env_dict
258258

259259

260+
def parse_regionwise_loadtest_config(regionwise_loadtest_config):
261+
logger.debug("Parsing regionwise load test configuration")
262+
regional_load_test_config = []
263+
for region_load in regionwise_loadtest_config:
264+
region_name = region_load.get("region")
265+
if region_name is None or not isinstance(region_name, str):
266+
raise RequiredArgumentMissingError("Region name is required of type string")
267+
engine_instances = region_load.get("engineInstances")
268+
if engine_instances is None or not isinstance(engine_instances, int):
269+
raise InvalidArgumentValueError("Engine instances is required of type integer")
270+
regional_load_test_config.append({"region": region_name.lower(), "engineInstances": engine_instances})
271+
logger.debug("Successfully parsed regionwise load test configuration: %s", regional_load_test_config)
272+
return regional_load_test_config
273+
274+
260275
def create_autostop_criteria_from_args(autostop, error_rate, time_window):
261276
if (autostop is None and error_rate is None and time_window is None):
262277
return None
@@ -304,9 +319,12 @@ def convert_yaml_to_test(data):
304319
new_body["subnetId"] = data["subnetId"]
305320

306321
new_body["loadTestConfiguration"] = {}
307-
new_body["loadTestConfiguration"]["engineInstances"] = data.get(
308-
"engineInstances", 1
309-
)
322+
new_body["loadTestConfiguration"]["engineInstances"] = data.get("engineInstances")
323+
if data.get("regionalLoadTestConfig") is not None:
324+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"] = parse_regionwise_loadtest_config(
325+
data.get("regionalLoadTestConfig")
326+
)
327+
310328
if data.get("certificates"):
311329
new_body["certificate"] = parse_cert(data.get("certificates"))
312330
if data.get("secrets"):
@@ -399,6 +417,7 @@ def create_or_update_test_with_config(
399417
split_csv=None,
400418
disable_public_ip=None,
401419
autostop_criteria=None,
420+
regionwise_engines=None,
402421
):
403422
logger.info(
404423
"Creating a request body for create or update test using config and parameters."
@@ -480,7 +499,25 @@ def create_or_update_test_with_config(
480499
"loadTestConfiguration"
481500
]["engineInstances"]
482501
else:
483-
new_body["loadTestConfiguration"]["engineInstances"] = 1
502+
new_body["loadTestConfiguration"]["engineInstances"] = body.get(
503+
"loadTestConfiguration", {}
504+
).get("engineInstances", 1)
505+
if regionwise_engines:
506+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"] = regionwise_engines
507+
elif (
508+
yaml_test_body.get("loadTestConfiguration", {}).get("regionalLoadTestConfig")
509+
is not None
510+
):
511+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"] = yaml_test_body[
512+
"loadTestConfiguration"
513+
]["regionalLoadTestConfig"]
514+
else:
515+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"] = body.get(
516+
"loadTestConfiguration", {}
517+
).get("regionalLoadTestConfig")
518+
validate_engine_data_with_regionwiseload_data(
519+
new_body["loadTestConfiguration"]["engineInstances"],
520+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"])
484521
# quick test is not supported in CLI
485522
new_body["loadTestConfiguration"]["quickStartTest"] = False
486523

@@ -554,6 +591,7 @@ def create_or_update_test_without_config(
554591
split_csv=None,
555592
disable_public_ip=None,
556593
autostop_criteria=None,
594+
regionwise_engines=None,
557595
):
558596
logger.info(
559597
"Creating a request body for test using parameters and old test body (in case of update)."
@@ -610,6 +648,15 @@ def create_or_update_test_without_config(
610648
new_body["loadTestConfiguration"]["engineInstances"] = body.get(
611649
"loadTestConfiguration", {}
612650
).get("engineInstances", 1)
651+
if regionwise_engines:
652+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"] = regionwise_engines
653+
else:
654+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"] = body.get(
655+
"loadTestConfiguration", {}
656+
).get("regionalLoadTestConfig")
657+
validate_engine_data_with_regionwiseload_data(
658+
new_body["loadTestConfiguration"]["engineInstances"],
659+
new_body["loadTestConfiguration"]["regionalLoadTestConfig"])
613660
# quick test is not supported in CLI
614661
new_body["loadTestConfiguration"]["quickStartTest"] = False
615662
if split_csv is not None:
@@ -804,6 +851,19 @@ def upload_files_helper(
804851
load_test_config_file=load_test_config_file, existing_test_files=files, wait=wait)
805852

806853

854+
def validate_engine_data_with_regionwiseload_data(engine_instances, regionwise_engines):
855+
if regionwise_engines is None:
856+
return
857+
total_engines = 0
858+
for region in regionwise_engines:
859+
total_engines += region["engineInstances"]
860+
if total_engines != engine_instances:
861+
raise InvalidArgumentValueError(
862+
f"Sum of engine instances in regionwise load test configuration ({total_engines}) "
863+
f"should be equal to total engine instances ({engine_instances})"
864+
)
865+
866+
807867
def validate_failure_criteria(failure_criteria):
808868
parts = failure_criteria.split("(")
809869
if len(parts) != 2:

src/load/azext_load/data_plane/utils/validators.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,3 +464,32 @@ def _validate_autostop_criteria_configfile(error_rate, time_window):
464464
raise InvalidArgumentValueError(
465465
"Invalid value for timeWindow. Value should be an integer greater than or equal to 0"
466466
)
467+
468+
469+
def validate_regionwise_engines(namespace):
470+
if namespace.regionwise_engines is None:
471+
return
472+
if not isinstance(namespace.regionwise_engines, list):
473+
raise InvalidArgumentValueError(
474+
f"Invalid regionwise-engines type: {type(namespace.regionwise_engines)}. \
475+
Expected list in the format of region1=engineCount1 region2=engineCount2"
476+
)
477+
regionwise_engines = []
478+
for item in namespace.regionwise_engines:
479+
if not isinstance(item, str) or "=" not in item:
480+
raise InvalidArgumentValueError(
481+
f"Invalid regionwise-engines item type: {type(item)}. Expected region=engineCount"
482+
)
483+
key, value = item.split("=", 1)
484+
if not key or not value:
485+
raise InvalidArgumentValueError(
486+
f"Invalid regionwise-engines item: {item}. Region or engine count cannot be empty"
487+
)
488+
try:
489+
value = int(value.strip())
490+
except ValueError:
491+
raise InvalidArgumentValueError(
492+
f"Invalid regionwise-engines item value: {value}. Expected integer"
493+
)
494+
regionwise_engines.append({"region": key.strip().lower(), "engineInstances": value})
495+
namespace.regionwise_engines = regionwise_engines

src/load/azext_load/tests/latest/constants.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ class LoadConstants:
3333
INVALID_ZIP_ARTIFACT_WITH_SUBDIR_NAME = "sample-ZIP-artifact-subdir.zip"
3434
INVALID_ZIP_ARTIFACT_WITH_SUBDIR_FILE = os.path.join(TEST_RESOURCES_DIR, r"sample-ZIP-artifact-subdir.zip")
3535

36+
# Constants for Regional Load Config Unit Tests
37+
REGIONAL_LOAD_CONFIG_FILE = os.path.join(TEST_RESOURCES_DIR, r"config-regionwise-engines.yaml")
38+
REGIONAL_LOAD_CONFIG_FILE_COUNT_MISMATCH = os.path.join(TEST_RESOURCES_DIR, r"config-regionwise-engines-count-mismatch.yaml")
39+
REGIONAL_LOAD_CONFIG_FILE_INVALID_REGION = os.path.join(TEST_RESOURCES_DIR, r"config-regionwise-engines-invalid-region.yaml")
40+
REGIONAL_LOAD_CONFIG_FILE_INVALID_TYPE_FLOAT = os.path.join(TEST_RESOURCES_DIR, r"config-regionwise-engines-invalid-type-float.yaml")
41+
REGIONAL_LOAD_CONFIG_FILE_INVALID_TYPE_STRING = os.path.join(TEST_RESOURCES_DIR, r"config-regionwise-engines-invalid-type-string.yaml")
42+
REGIONAL_LOAD_CONFIG_FILE_NO_PARENT_REGION = os.path.join(TEST_RESOURCES_DIR, r"config-regionwise-engines-no-parent-region.yaml")
43+
REGIONAL_LOAD_CONFIG_FILE_NO_TOTAL = os.path.join(TEST_RESOURCES_DIR, r"config-regionwise-engines-no-total.yaml")
44+
ENGINE_INSTANCES = 5
45+
REGIONWISE_ENGINES = "germanywestcentral=2 eastus=3"
46+
REGIONWISE_ENGINES_1 = "eastus=1"
47+
REGIONWISE_ENGINES_2 = '"southcentralus = 4" "eastus = 1"'
48+
REGIONWISE_ENGINES_3 = '"southcentralus = 2" "southcentralus = 2" "eastus = 1"'
49+
REGIONWISE_ENGINES_INVALID_REGION = "invalidregion=2 eastus=3"
50+
REGIONWISE_ENGINES_INVALID_TYPE_FLOAT = "germanywestcentral=2 eastus=3.5"
51+
REGIONWISE_ENGINES_INVALID_TYPE_STRING = "germanywestcentral=2 eastus=three"
52+
REGIONWISE_ENGINES_NO_PARENT_REGION = "germanywestcentral=2 uksouth=3"
53+
REGIONWISE_ENGINES_INVALID_FORMAT_1 = {"germanywestcentral": 2, "eastus": 3}
54+
REGIONWISE_ENGINES_INVALID_FORMAT_2 = "germanywestcentral=2 eastus:3"
55+
REGIONWISE_ENGINES_INVALID_FORMAT_3 = "=2 eastus=3"
56+
3657
ENV_VAR_DURATION_NAME = "duration_in_sec"
3758
ENV_VAR_DURATION_SHORT = "1"
3859
ENV_VAR_DURATION_LONG = "120"
@@ -98,6 +119,7 @@ class LoadTestConstants(LoadConstants):
98119
APP_COMPONENT_TEST_ID = "app-component-test-case"
99120
SERVER_METRIC_TEST_ID = "server-metric-test-case"
100121
FILE_TEST_ID = "file-test-case"
122+
REGIONAL_LOAD_CONFIG_TEST_ID = "regional-load-config-test-case"
101123

102124
INVALID_UPDATE_TEST_ID = "invalid-update-test-case"
103125
INVALID_PF_TEST_ID = "invalid-pf-test-case"

0 commit comments

Comments
 (0)