Skip to content

Commit bd6f841

Browse files
authored
fix: resolve e2e import test concurrency races (#1067)
* fix: resolve e2e import test concurrency races Fix two independent concurrency issues causing flaky e2e import tests: 1. TOCTOU race in evaluator import (import-evaluator.ts): The beforeConfigWrite hook lists all online eval configs then fetches details for each with Promise.all. If a config is deleted between the list and get calls, the API throws 'Online evaluation configuration not found' and the entire import fails. Fixed by using Promise.allSettled and filtering out disappeared configs. 2. Resource name collisions across parallel CI shards (setup_*.py): Python setup scripts generated resource names using int(time.time()) (second-level precision). Parallel CI shards starting in the same second would collide with ConflictException. The test already passes a unique RESOURCE_SUFFIX env var but scripts ignored it for naming. Added NAME_SUFFIX to common.py that prefers RESOURCE_SUFFIX when set, and updated all setup scripts to use it. * chore: remove unused time imports from setup scripts
1 parent e9dfc16 commit bd6f841

6 files changed

Lines changed: 25 additions & 17 deletions

File tree

e2e-tests/fixtures/import/common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
import json
33
import os
44
import time
5+
import uuid
56
import zipfile
67
import tempfile
78

89
import boto3
910

1011
REGION = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1"
1112
RESOURCE_SUFFIX = os.environ.get("RESOURCE_SUFFIX", "")
13+
# Unique suffix for resource names — avoids collisions across parallel CI shards.
14+
NAME_SUFFIX = RESOURCE_SUFFIX or uuid.uuid4().hex[:12]
1215
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
1316
APP_DIR = os.path.join(SCRIPT_DIR, "app")
1417
_resources_name = f"bugbash-resources-{RESOURCE_SUFFIX}.json" if RESOURCE_SUFFIX else "bugbash-resources.json"

e2e-tests/fixtures/import/setup_evaluator.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,18 @@
88
import os
99
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1010

11-
import time
1211
from common import (
1312
get_control_client, save_resource, tag_resource,
1413
wait_for_evaluator, print_import_command,
14+
NAME_SUFFIX,
1515
)
1616

1717
DEFAULT_EVALUATOR_MODEL = os.environ.get("DEFAULT_EVALUATOR_MODEL", "us.anthropic.claude-sonnet-4-5-20250929-v1:0")
1818

1919

2020
def main():
2121
client = get_control_client()
22-
ts = int(time.time())
23-
evaluator_name = f"bugbash_eval_{ts}"
22+
evaluator_name = f"bugbash_eval_{NAME_SUFFIX}"
2423

2524
print(f"Creating evaluator: {evaluator_name}")
2625
resp = client.create_evaluator(

e2e-tests/fixtures/import/setup_gateway.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,17 @@
1212
import os
1313
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1414

15-
import time
1615
from common import (
1716
REGION, get_control_client, ensure_role, save_resource,
1817
tag_resource, wait_for_gateway, wait_for_gateway_target,
18+
NAME_SUFFIX,
1919
)
2020

2121

2222
def main():
2323
role_arn = ensure_role()
2424
client = get_control_client()
25-
ts = int(time.time())
26-
gateway_name = f"bugbashGw{ts}"
25+
gateway_name = f"bugbashGw{NAME_SUFFIX}"
2726

2827
# ------------------------------------------------------------------
2928
# 1. Create gateway

e2e-tests/fixtures/import/setup_memory_full.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@
88
import os
99
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1010

11-
import time
1211
from common import (
1312
ensure_role, get_control_client, wait_for_memory,
1413
save_resource, print_import_command, tag_resource,
14+
NAME_SUFFIX,
1515
)
1616

1717

1818
def main():
1919
role_arn = ensure_role()
2020
client = get_control_client()
21-
memory_name = f"bugbash_memory_{int(time.time())}"
21+
memory_name = f"bugbash_memory_{NAME_SUFFIX}"
2222

2323
print(f"Creating memory: {memory_name}")
2424
resp = client.create_memory(
2525
name=memory_name,
26-
clientToken=f"bugbash-{int(time.time())}",
26+
clientToken=f"bugbash-{NAME_SUFFIX}",
2727
eventExpiryDuration=30,
2828
memoryExecutionRoleArn=role_arn,
2929
memoryStrategies=[

e2e-tests/fixtures/import/setup_runtime_basic.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,19 @@
77
import os
88
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
99

10-
import time
1110
from common import (
1211
ensure_role, get_control_client, wait_for_runtime,
1312
save_resource, print_import_command, upload_code,
13+
NAME_SUFFIX,
1414
)
1515

1616

1717
def main():
1818
role_arn = ensure_role()
1919
client = get_control_client()
20-
ts = int(time.time())
21-
runtime_name = f"bugbash_basic_{ts}"
20+
runtime_name = f"bugbash_basic_{NAME_SUFFIX}"
2221

23-
bucket, s3_key = upload_code(f"bugbash-basic-{ts}")
22+
bucket, s3_key = upload_code(f"bugbash-basic-{NAME_SUFFIX}")
2423

2524
print(f"Creating basic runtime: {runtime_name}")
2625
resp = client.create_agent_runtime(

src/cli/commands/import/import-evaluator.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ANSI } from './constants';
1010
import { failResult, parseAndValidateArn } from './import-utils';
1111
import { executeResourceImport } from './resource-import';
1212
import type { ImportResourceOptions, ImportResourceResult, ResourceImportDescriptor } from './types';
13+
import { ResourceNotFoundException } from '@aws-sdk/client-bedrock-agentcore-control';
1314
import type { Command } from '@commander-js/extra-typings';
1415

1516
/**
@@ -92,11 +93,18 @@ const evaluatorDescriptor: ResourceImportDescriptor<GetEvaluatorResult, Evaluato
9293

9394
const oecSummaries = await listAllOnlineEvaluationConfigs({ region: target.region });
9495
if (oecSummaries.length > 0) {
95-
const oecDetails = await Promise.all(
96-
oecSummaries.map(s =>
97-
getOnlineEvaluationConfig({ region: target.region, configId: s.onlineEvaluationConfigId })
96+
// Configs can be deleted between list and get (TOCTOU race).
97+
// Skip ResourceNotFoundException — a deleted config can't be locking our evaluator.
98+
const oecDetails = (
99+
await Promise.all(
100+
oecSummaries.map(s =>
101+
getOnlineEvaluationConfig({ region: target.region, configId: s.onlineEvaluationConfigId }).catch(err => {
102+
if (err instanceof ResourceNotFoundException) return null;
103+
throw err;
104+
})
105+
)
98106
)
99-
);
107+
).filter(r => r !== null);
100108

101109
const referencingOec = oecDetails.find(oec => oec.evaluatorIds?.includes(detail.evaluatorId));
102110

0 commit comments

Comments
 (0)