feat(worker): 实现集群限流与自适应负载
保留平台模型 RPM、TPM 和并发策略语义,增加 PostgreSQL 集群级租约、饱和候选重选和多平台自动负载,避免突发任务固定等待首个平台。\n\n新增 Worker 实时负载采样、自适应 active/heavy 容量、心跳与管理端指标,并扩展本地 acceptance runner,覆盖三 Worker、同模型三平台 2/4/6 并发和 48 个带图视频突发任务。\n\n验证:go test ./...、go vet ./...、PostgreSQL 跨 Store 集成测试、gofmt、bash -n、ShellCheck 及本地集群 provider-burst 验收通过;48/48 成功,无越限、重复提交、重复计费、重复回调或终态资源泄漏。
This commit is contained in:
@@ -41,7 +41,8 @@ load_lock() {
|
||||
}
|
||||
# shellcheck source=/dev/null
|
||||
source "$lock_file"
|
||||
: "${K3D_VERSION:?}" "${CRANE_VERSION:?}" "${K3S_IMAGE:?}" "${CNPG_MANIFEST_URL:?}" "${CNPG_MANIFEST_SHA256:?}"
|
||||
: "${K3D_VERSION:?}" "${CRANE_VERSION:?}" "${K3S_IMAGE:?}" "${K3S_PAUSE_IMAGE:?}" "${K3S_BUSYBOX_IMAGE:?}"
|
||||
: "${CNPG_MANIFEST_URL:?}" "${CNPG_MANIFEST_SHA256:?}"
|
||||
: "${CNPG_CONTROLLER_IMAGE:?}" "${CNPG_POSTGRES_IMAGE:?}" "${K3S_COREDNS_IMAGE:?}"
|
||||
: "${K3S_METRICS_SERVER_IMAGE:?}" "${K3S_LOCAL_PATH_IMAGE:?}"
|
||||
}
|
||||
@@ -123,6 +124,9 @@ host_available_disk_bytes() {
|
||||
}
|
||||
|
||||
preflight() {
|
||||
local required_disk_gib=${1:-30} required_disk_bytes
|
||||
[[ $required_disk_gib =~ ^[0-9]+$ && $required_disk_gib -ge 10 ]]
|
||||
required_disk_bytes=$((required_disk_gib * 1024 * 1024 * 1024))
|
||||
load_lock
|
||||
require_command docker
|
||||
require_command kubectl
|
||||
@@ -165,12 +169,12 @@ preflight() {
|
||||
exit 1
|
||||
}
|
||||
available_disk_bytes=$(host_available_disk_bytes)
|
||||
[[ $available_disk_bytes =~ ^[0-9]+$ && $available_disk_bytes -ge 32212254720 ]] || {
|
||||
[[ $available_disk_bytes =~ ^[0-9]+$ && $available_disk_bytes -ge $required_disk_bytes ]] || {
|
||||
available_disk_gib=$(awk -v bytes="${available_disk_bytes:-0}" 'BEGIN {printf "%.1f", bytes/1024/1024/1024}')
|
||||
echo "host free disk is ${available_disk_gib} GiB; local acceptance requires at least 30 GiB" >&2
|
||||
echo "host free disk is ${available_disk_gib} GiB; local acceptance requires at least ${required_disk_gib} GiB" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "local_acceptance_preflight=PASS docker_memory_bytes=$memory_bytes docker_cpus=$cpu_count host_available_disk_bytes=$available_disk_bytes architecture=$architecture k3d=$K3D_VERSION"
|
||||
echo "local_acceptance_preflight=PASS docker_memory_bytes=$memory_bytes docker_cpus=$cpu_count host_available_disk_bytes=$available_disk_bytes required_disk_gib=$required_disk_gib architecture=$architecture k3d=$K3D_VERSION"
|
||||
}
|
||||
|
||||
ensure_private_material() {
|
||||
@@ -224,8 +228,8 @@ render_k3d_config() {
|
||||
host_memory_mib=$((host_memory_bytes / 1024 / 1024))
|
||||
site_system_cpu="$(((host_cpu - 4) * 1000 + 750))m"
|
||||
witness_system_cpu="$(((host_cpu - 2) * 1000 + 750))m"
|
||||
site_system_memory="$((host_memory_mib - 8192 + 1024))Mi"
|
||||
witness_system_memory="$((host_memory_mib - 4096 + 1024))Mi"
|
||||
site_system_memory="$((host_memory_mib - 8192 + 512))Mi"
|
||||
witness_system_memory="$((host_memory_mib - 4096 + 512))Mi"
|
||||
sed \
|
||||
-e "s|EASYAI_ACCEPTANCE_MEDIA_DIR|$media_root|g" \
|
||||
-e "s|EASYAI_ACCEPTANCE_K3S_IMAGE|${K3S_IMAGE%%@*}|g" \
|
||||
@@ -294,7 +298,8 @@ remove_cluster_network() {
|
||||
|
||||
pull_dependency_images() {
|
||||
local image
|
||||
for image in "$K3S_IMAGE" "$CNPG_CONTROLLER_IMAGE" "$CNPG_POSTGRES_IMAGE" \
|
||||
for image in "$K3S_IMAGE" "$K3S_PAUSE_IMAGE" "$K3S_BUSYBOX_IMAGE" \
|
||||
"$CNPG_CONTROLLER_IMAGE" "$CNPG_POSTGRES_IMAGE" \
|
||||
"$K3S_COREDNS_IMAGE" "$K3S_METRICS_SERVER_IMAGE" "$K3S_LOCAL_PATH_IMAGE"; do
|
||||
pull_dependency_image "$image"
|
||||
done
|
||||
@@ -596,6 +601,10 @@ render_and_apply_application() {
|
||||
-f "$repository_root/deploy/kubernetes/production/service-account-rbac.yaml" >/dev/null
|
||||
kubectl --context "$context" -n "$namespace" apply -f "$manifest_root/local-config.yaml" >/dev/null
|
||||
kubectl --context "$context" -n "$namespace" apply -f "$rendered" >/dev/null
|
||||
for workload in easyai-worker-ningbo easyai-worker-hongkong; do
|
||||
kubectl --context "$context" -n "$namespace" patch deployment "$workload" \
|
||||
--type=merge -p='{"spec":{"template":{"spec":{"affinity":null}}}}' >/dev/null
|
||||
done
|
||||
|
||||
for workload in easyai-api-ningbo easyai-worker-ningbo; do
|
||||
kubectl --context "$context" -n "$namespace" set env deployment/"$workload" \
|
||||
@@ -791,10 +800,14 @@ up_cluster() {
|
||||
echo 'local K3s node allocatable capacity does not match 4/8, 4/8, 2/4 resource envelope' >&2
|
||||
exit 1
|
||||
}
|
||||
"$k3d" image import -c "$cluster_name" \
|
||||
local cluster_image
|
||||
for cluster_image in \
|
||||
"$api_image" "$web_image" "$netem_image" \
|
||||
"${K3S_PAUSE_IMAGE%%@*}" "${K3S_BUSYBOX_IMAGE%%@*}" \
|
||||
"${CNPG_CONTROLLER_IMAGE%%@*}" "${CNPG_POSTGRES_IMAGE%%@*}" \
|
||||
"${K3S_COREDNS_IMAGE%%@*}" "${K3S_METRICS_SERVER_IMAGE%%@*}" "${K3S_LOCAL_PATH_IMAGE%%@*}"
|
||||
"${K3S_COREDNS_IMAGE%%@*}" "${K3S_METRICS_SERVER_IMAGE%%@*}" "${K3S_LOCAL_PATH_IMAGE%%@*}"; do
|
||||
"$k3d" image import -c "$cluster_name" "$cluster_image"
|
||||
done
|
||||
|
||||
local cnpg_manifest="$state_root/cnpg.yaml"
|
||||
curl -fsSL "$CNPG_MANIFEST_URL" -o "$cnpg_manifest"
|
||||
@@ -844,7 +857,7 @@ up_cluster() {
|
||||
|
||||
new_run() {
|
||||
load_lock
|
||||
preflight
|
||||
preflight 10
|
||||
[[ -z $(git -C "$repository_root" status --short) ]] || {
|
||||
echo 'local acceptance requires a clean source tree for a fresh Run ID' >&2
|
||||
exit 1
|
||||
|
||||
@@ -61,11 +61,12 @@ verify_local_node_capacity() {
|
||||
elif endswith("Gi") then rtrimstr("Gi") | tonumber * 1024
|
||||
else 0 end;
|
||||
([.items[] | select(.metadata.labels["easyai.io/workload"] == "true") |
|
||||
select(.metadata.labels["easyai.io/worker"] == "true") |
|
||||
select((.status.allocatable.cpu | cpu_m) == 3000 and
|
||||
(.status.allocatable.memory | memory_mi) == 6656)] | length) == 2 and
|
||||
(.status.allocatable.memory | memory_mi | floor) == 6656)] | length) == 2 and
|
||||
([.items[] | select(.metadata.labels["easyai.io/site"] == "los-angeles") |
|
||||
select((.status.allocatable.cpu | cpu_m) == 1000 and
|
||||
(.status.allocatable.memory | memory_mi) == 2560)] | length) == 1 and
|
||||
(.status.allocatable.memory | memory_mi | floor) == 2560)] | length) == 1 and
|
||||
([.items[] | select(any(.status.conditions[]; .type == "Ready" and .status == "True"))] | length) == 3 and
|
||||
([.items[] | select(any(.status.conditions[]; .type == "MemoryPressure" and .status == "True"))] | length) == 0
|
||||
' >/dev/null
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This library is sourced by run-local-acceptance.sh and intentionally uses its
|
||||
# validated runtime globals and lifecycle state.
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
provider_burst_platform_ids=(
|
||||
f1000000-0000-4000-8000-000000000002
|
||||
f1000000-0000-4000-8000-000000000004
|
||||
f1000000-0000-4000-8000-000000000006
|
||||
)
|
||||
|
||||
provider_burst_model_ids=(
|
||||
f2000000-0000-4000-8000-000000000002
|
||||
f2000000-0000-4000-8000-000000000004
|
||||
f2000000-0000-4000-8000-000000000006
|
||||
)
|
||||
|
||||
restore_provider_burst_platforms() {
|
||||
database_query "
|
||||
UPDATE integration_platforms
|
||||
SET status = config->>'acceptanceBurstPreviousStatus',
|
||||
config = config - 'acceptanceBurstPreviousStatus',
|
||||
updated_at = statement_timestamp()
|
||||
WHERE config ? 'acceptanceBurstPreviousStatus';
|
||||
|
||||
UPDATE integration_platforms
|
||||
SET status = 'disabled',
|
||||
updated_at = statement_timestamp()
|
||||
WHERE COALESCE((config->>'acceptanceProviderBurst')::boolean, false);" >/dev/null
|
||||
}
|
||||
|
||||
setup_provider_burst_platforms() {
|
||||
[[ $run_id =~ ^[0-9a-f-]{36}$ ]]
|
||||
[[ $acceptance_video_model =~ ^[A-Za-z0-9._:-]+$ ]]
|
||||
restore_provider_burst_platforms
|
||||
database_query "
|
||||
WITH source AS (
|
||||
SELECT platform.*
|
||||
FROM integration_platforms platform
|
||||
JOIN platform_models model ON model.platform_id = platform.id
|
||||
JOIN base_model_catalog base_model ON base_model.id = model.base_model_id
|
||||
WHERE base_model.invocation_name = '$acceptance_video_model'
|
||||
AND COALESCE((platform.config->>'acceptanceSnapshot')::boolean, false)
|
||||
ORDER BY platform.priority, platform.created_at
|
||||
LIMIT 1
|
||||
), variants(platform_id, platform_key, platform_name, concurrency_limit) AS (
|
||||
VALUES
|
||||
('${provider_burst_platform_ids[0]}'::uuid, 'acceptance-provider-burst-2', 'Acceptance Provider Burst C2', 2),
|
||||
('${provider_burst_platform_ids[1]}'::uuid, 'acceptance-provider-burst-4', 'Acceptance Provider Burst C4', 4),
|
||||
('${provider_burst_platform_ids[2]}'::uuid, 'acceptance-provider-burst-6', 'Acceptance Provider Burst C6', 6)
|
||||
)
|
||||
INSERT INTO integration_platforms (
|
||||
id, provider, platform_key, name, base_url, auth_type, credentials, config,
|
||||
visibility_scope, tenant_id, tenant_key, default_pricing_mode,
|
||||
default_discount_factor, pricing_rule_set_id, retry_policy, rate_limit_policy,
|
||||
priority, dynamic_priority, status, disabled_reason, cooldown_until, internal_name
|
||||
)
|
||||
SELECT variants.platform_id, source.provider, variants.platform_key, variants.platform_name,
|
||||
source.base_url, source.auth_type, source.credentials,
|
||||
source.config || jsonb_build_object(
|
||||
'acceptanceProviderBurst', true,
|
||||
'acceptanceBurstRunId', '$run_id',
|
||||
'acceptanceConcurrencyLimit', variants.concurrency_limit
|
||||
),
|
||||
source.visibility_scope, source.tenant_id, source.tenant_key,
|
||||
source.default_pricing_mode, source.default_discount_factor,
|
||||
source.pricing_rule_set_id, source.retry_policy, '{\"rules\":[]}'::jsonb,
|
||||
100, NULL, 'enabled', NULL, NULL, variants.platform_name
|
||||
FROM source CROSS JOIN variants
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
provider = EXCLUDED.provider,
|
||||
platform_key = EXCLUDED.platform_key,
|
||||
name = EXCLUDED.name,
|
||||
base_url = EXCLUDED.base_url,
|
||||
auth_type = EXCLUDED.auth_type,
|
||||
credentials = EXCLUDED.credentials,
|
||||
config = EXCLUDED.config,
|
||||
visibility_scope = EXCLUDED.visibility_scope,
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
tenant_key = EXCLUDED.tenant_key,
|
||||
default_pricing_mode = EXCLUDED.default_pricing_mode,
|
||||
default_discount_factor = EXCLUDED.default_discount_factor,
|
||||
pricing_rule_set_id = EXCLUDED.pricing_rule_set_id,
|
||||
retry_policy = EXCLUDED.retry_policy,
|
||||
rate_limit_policy = EXCLUDED.rate_limit_policy,
|
||||
priority = EXCLUDED.priority,
|
||||
dynamic_priority = EXCLUDED.dynamic_priority,
|
||||
status = 'enabled',
|
||||
disabled_reason = NULL,
|
||||
cooldown_until = NULL,
|
||||
internal_name = EXCLUDED.internal_name,
|
||||
deleted_at = NULL,
|
||||
updated_at = statement_timestamp();
|
||||
|
||||
WITH source AS (
|
||||
SELECT model.*
|
||||
FROM platform_models model
|
||||
JOIN integration_platforms platform ON platform.id = model.platform_id
|
||||
JOIN base_model_catalog base_model ON base_model.id = model.base_model_id
|
||||
WHERE base_model.invocation_name = '$acceptance_video_model'
|
||||
AND COALESCE((platform.config->>'acceptanceSnapshot')::boolean, false)
|
||||
ORDER BY platform.priority, platform.created_at
|
||||
LIMIT 1
|
||||
), variants(model_id, platform_id, concurrency_limit) AS (
|
||||
VALUES
|
||||
('${provider_burst_model_ids[0]}'::uuid, '${provider_burst_platform_ids[0]}'::uuid, 2),
|
||||
('${provider_burst_model_ids[1]}'::uuid, '${provider_burst_platform_ids[1]}'::uuid, 4),
|
||||
('${provider_burst_model_ids[2]}'::uuid, '${provider_burst_platform_ids[2]}'::uuid, 6)
|
||||
)
|
||||
INSERT INTO platform_models (
|
||||
id, platform_id, base_model_id, model_name, model_alias, model_type,
|
||||
display_name, capability_override, capabilities, pricing_mode,
|
||||
discount_factor, pricing_rule_set_id, billing_config_override, billing_config,
|
||||
permission_config, retry_policy, rate_limit_policy, rate_limit_policy_mode,
|
||||
runtime_policy_set_id, runtime_policy_override, provider_model_name,
|
||||
cooldown_until, enabled
|
||||
)
|
||||
SELECT variants.model_id, variants.platform_id, source.base_model_id,
|
||||
source.model_name, source.model_alias, source.model_type, source.display_name,
|
||||
source.capability_override, source.capabilities, source.pricing_mode,
|
||||
source.discount_factor, source.pricing_rule_set_id,
|
||||
source.billing_config_override, source.billing_config,
|
||||
source.permission_config, source.retry_policy,
|
||||
jsonb_build_object('rules', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'metric', 'concurrent', 'limit', variants.concurrency_limit,
|
||||
'leaseTtlSeconds', 120
|
||||
),
|
||||
jsonb_build_object('metric', 'rpm', 'limit', 600, 'windowSeconds', 60)
|
||||
)),
|
||||
'override', NULL, '{}'::jsonb, source.provider_model_name, NULL, true
|
||||
FROM source CROSS JOIN variants
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
platform_id = EXCLUDED.platform_id,
|
||||
base_model_id = EXCLUDED.base_model_id,
|
||||
model_name = EXCLUDED.model_name,
|
||||
model_alias = EXCLUDED.model_alias,
|
||||
model_type = EXCLUDED.model_type,
|
||||
display_name = EXCLUDED.display_name,
|
||||
capability_override = EXCLUDED.capability_override,
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
discount_factor = EXCLUDED.discount_factor,
|
||||
pricing_rule_set_id = EXCLUDED.pricing_rule_set_id,
|
||||
billing_config_override = EXCLUDED.billing_config_override,
|
||||
billing_config = EXCLUDED.billing_config,
|
||||
permission_config = EXCLUDED.permission_config,
|
||||
retry_policy = EXCLUDED.retry_policy,
|
||||
rate_limit_policy = EXCLUDED.rate_limit_policy,
|
||||
rate_limit_policy_mode = 'override',
|
||||
runtime_policy_set_id = NULL,
|
||||
runtime_policy_override = '{}'::jsonb,
|
||||
provider_model_name = EXCLUDED.provider_model_name,
|
||||
cooldown_until = NULL,
|
||||
enabled = true,
|
||||
updated_at = statement_timestamp();
|
||||
|
||||
WITH selected_platforms AS (
|
||||
SELECT platform.id
|
||||
FROM integration_platforms platform
|
||||
JOIN platform_models model ON model.platform_id = platform.id
|
||||
JOIN base_model_catalog base_model ON base_model.id = model.base_model_id
|
||||
WHERE base_model.invocation_name = '$acceptance_video_model'
|
||||
AND COALESCE((platform.config->>'acceptanceSnapshot')::boolean, false)
|
||||
AND NOT COALESCE((platform.config->>'acceptanceProviderBurst')::boolean, false)
|
||||
)
|
||||
UPDATE integration_platforms platform
|
||||
SET config = jsonb_set(
|
||||
platform.config,
|
||||
'{acceptanceBurstPreviousStatus}',
|
||||
to_jsonb(platform.status),
|
||||
true
|
||||
),
|
||||
status = 'disabled',
|
||||
updated_at = statement_timestamp()
|
||||
FROM selected_platforms
|
||||
WHERE platform.id = selected_platforms.id;
|
||||
|
||||
WITH acceptance_group AS (
|
||||
SELECT api_key.user_group_id AS id
|
||||
FROM gateway_acceptance_runs run
|
||||
JOIN gateway_api_keys api_key ON api_key.id::text = run.api_key_id
|
||||
WHERE run.id = '$run_id'::uuid
|
||||
), resources(resource_type, resource_id) AS (
|
||||
SELECT 'platform'::text, platform.id
|
||||
FROM integration_platforms platform
|
||||
WHERE COALESCE((platform.config->>'acceptanceProviderBurst')::boolean, false)
|
||||
AND platform.config->>'acceptanceBurstRunId' = '$run_id'
|
||||
UNION ALL
|
||||
SELECT 'platform_model', model.id
|
||||
FROM platform_models model
|
||||
JOIN integration_platforms platform ON platform.id = model.platform_id
|
||||
WHERE COALESCE((platform.config->>'acceptanceProviderBurst')::boolean, false)
|
||||
AND platform.config->>'acceptanceBurstRunId' = '$run_id'
|
||||
UNION
|
||||
SELECT 'base_model', model.base_model_id
|
||||
FROM platform_models model
|
||||
JOIN integration_platforms platform ON platform.id = model.platform_id
|
||||
WHERE COALESCE((platform.config->>'acceptanceProviderBurst')::boolean, false)
|
||||
AND platform.config->>'acceptanceBurstRunId' = '$run_id'
|
||||
)
|
||||
INSERT INTO gateway_access_rules (
|
||||
subject_type, subject_id, resource_type, resource_id, effect,
|
||||
priority, min_permission_level, conditions, metadata, status
|
||||
)
|
||||
SELECT 'user_group', acceptance_group.id, resources.resource_type,
|
||||
resources.resource_id, 'allow', 1, 0, '{}'::jsonb,
|
||||
jsonb_build_object('purpose', 'provider_burst_acceptance', 'runId', '$run_id'),
|
||||
'active'
|
||||
FROM acceptance_group CROSS JOIN resources
|
||||
WHERE acceptance_group.id IS NOT NULL
|
||||
ON CONFLICT (subject_type, subject_id, resource_type, resource_id, effect)
|
||||
DO UPDATE SET status = 'active', metadata = EXCLUDED.metadata,
|
||||
updated_at = statement_timestamp();" >/dev/null
|
||||
|
||||
local configured
|
||||
configured=$(database_query "
|
||||
SELECT jsonb_build_object(
|
||||
'platforms', count(*),
|
||||
'limits', jsonb_agg((platform.config->>'acceptanceConcurrencyLimit')::int ORDER BY (platform.config->>'acceptanceConcurrencyLimit')::int),
|
||||
'enabled', count(*) FILTER (WHERE platform.status='enabled' AND model.enabled)
|
||||
)
|
||||
FROM integration_platforms platform
|
||||
JOIN platform_models model ON model.platform_id = platform.id
|
||||
WHERE COALESCE((platform.config->>'acceptanceProviderBurst')::boolean, false)
|
||||
AND platform.config->>'acceptanceBurstRunId' = '$run_id';")
|
||||
jq -e '.platforms == 3 and .enabled == 3 and .limits == [2,4,6]' <<<"$configured" >/dev/null
|
||||
}
|
||||
|
||||
sample_provider_burst() {
|
||||
local output=$1 stop_file=$2
|
||||
: >"$output"
|
||||
while [[ ! -e $stop_file ]]; do
|
||||
database_query "
|
||||
WITH configured AS (
|
||||
SELECT platform.id platform_id, platform.platform_key, model.id platform_model_id,
|
||||
(platform.config->>'acceptanceConcurrencyLimit')::int concurrency_limit
|
||||
FROM integration_platforms platform
|
||||
JOIN platform_models model ON model.platform_id = platform.id
|
||||
WHERE COALESCE((platform.config->>'acceptanceProviderBurst')::boolean, false)
|
||||
AND platform.config->>'acceptanceBurstRunId' = '$run_id'
|
||||
), platform_state AS (
|
||||
SELECT configured.*,
|
||||
(SELECT count(*) FROM gateway_concurrency_leases lease
|
||||
WHERE lease.scope_type='platform_model'
|
||||
AND lease.scope_key='acceptance:$run_id:'||configured.platform_model_id::text
|
||||
AND lease.released_at IS NULL
|
||||
AND lease.expires_at > statement_timestamp()) active_leases,
|
||||
(SELECT count(*) FROM gateway_task_attempts attempt
|
||||
WHERE attempt.platform_model_id=configured.platform_model_id
|
||||
AND attempt.task_id IN (SELECT id FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid)) attempts
|
||||
FROM configured
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'sampledAt', to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),
|
||||
'queuedTasks', (SELECT count(*) FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid AND status='queued'),
|
||||
'runningTasks', (SELECT count(*) FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid AND status='running'),
|
||||
'succeededTasks', (SELECT count(*) FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid AND status='succeeded'),
|
||||
'waitingAdmissions', (SELECT count(*) FROM gateway_task_admissions admission JOIN gateway_tasks task ON task.id=admission.task_id WHERE task.acceptance_run_id='$run_id'::uuid AND admission.status='waiting'),
|
||||
'platforms', COALESCE((SELECT jsonb_agg(jsonb_build_object(
|
||||
'platformId', platform_id,
|
||||
'platformModelId', platform_model_id,
|
||||
'platformKey', platform_key,
|
||||
'limit', concurrency_limit,
|
||||
'activeLeases', active_leases,
|
||||
'attempts', attempts
|
||||
) ORDER BY concurrency_limit) FROM platform_state), '[]'::jsonb)
|
||||
);" >>"$output" || true
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
provider_burst_resource_summary() {
|
||||
local resources=$1
|
||||
jq -Rn '
|
||||
def number_without($suffix): rtrimstr($suffix) | tonumber;
|
||||
def memory_mib:
|
||||
if endswith("Gi") then number_without("Gi") * 1024
|
||||
elif endswith("Mi") then number_without("Mi")
|
||||
elif endswith("Ki") then number_without("Ki") / 1024
|
||||
else tonumber end;
|
||||
[inputs | split(",") | select(.[0] != "timestamp") | {
|
||||
scope:.[1],name:.[2],cpu:.[3],memory:.[4]
|
||||
}]
|
||||
| group_by([.scope,.name])
|
||||
| map(if .[0].scope == "node" then {
|
||||
scope:"node",name:.[0].name,
|
||||
maxCpuPercent:(map(.cpu | number_without("%")) | max),
|
||||
maxMemoryPercent:(map(.memory | number_without("%")) | max)
|
||||
} else {
|
||||
scope:"pod",name:.[0].name,
|
||||
maxCpuMillicores:(map(.cpu | if endswith("m") then number_without("m") else tonumber * 1000 end) | max),
|
||||
maxMemoryMiB:(map(.memory | memory_mib) | max)
|
||||
} end)' <"$resources"
|
||||
}
|
||||
|
||||
build_provider_burst_report() {
|
||||
local output=$1 load_report=$2 samples=$3 worker_samples=$4 resources=$5 requests=$6
|
||||
local platforms queue workers distribution worker_peaks limits concurrency_peaks
|
||||
local leaks duplicates callbacks resource_summary passed=true
|
||||
platforms=$(database_query "
|
||||
WITH configured AS (
|
||||
SELECT platform.id platform_id, platform.platform_key, model.id platform_model_id,
|
||||
(platform.config->>'acceptanceConcurrencyLimit')::int concurrency_limit
|
||||
FROM integration_platforms platform
|
||||
JOIN platform_models model ON model.platform_id=platform.id
|
||||
WHERE COALESCE((platform.config->>'acceptanceProviderBurst')::boolean, false)
|
||||
AND platform.config->>'acceptanceBurstRunId'='$run_id'
|
||||
), events AS (
|
||||
SELECT configured.platform_model_id, lease.acquired_at event_at, lease.lease_value delta
|
||||
FROM configured
|
||||
JOIN gateway_concurrency_leases lease
|
||||
ON lease.scope_type='platform_model'
|
||||
AND lease.scope_key='acceptance:$run_id:'||configured.platform_model_id::text
|
||||
UNION ALL
|
||||
SELECT configured.platform_model_id,
|
||||
LEAST(COALESCE(lease.released_at, lease.expires_at), lease.expires_at),
|
||||
-lease.lease_value
|
||||
FROM configured
|
||||
JOIN gateway_concurrency_leases lease
|
||||
ON lease.scope_type='platform_model'
|
||||
AND lease.scope_key='acceptance:$run_id:'||configured.platform_model_id::text
|
||||
), points AS (
|
||||
SELECT platform_model_id,
|
||||
SUM(delta) OVER (PARTITION BY platform_model_id ORDER BY event_at,delta ROWS UNBOUNDED PRECEDING) active
|
||||
FROM events
|
||||
), peaks AS (
|
||||
SELECT platform_model_id, COALESCE(max(active),0) peak FROM points GROUP BY platform_model_id
|
||||
), attempts AS (
|
||||
SELECT attempt.platform_model_id, count(*) attempts
|
||||
FROM gateway_task_attempts attempt
|
||||
WHERE attempt.task_id IN (SELECT id FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid)
|
||||
GROUP BY attempt.platform_model_id
|
||||
)
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'platformId', configured.platform_id,
|
||||
'platformModelId', configured.platform_model_id,
|
||||
'platformKey', configured.platform_key,
|
||||
'limit', configured.concurrency_limit,
|
||||
'peak', COALESCE(peaks.peak,0),
|
||||
'attempts', COALESCE(attempts.attempts,0)
|
||||
) ORDER BY configured.concurrency_limit), '[]'::jsonb)
|
||||
FROM configured
|
||||
LEFT JOIN peaks ON peaks.platform_model_id=configured.platform_model_id
|
||||
LEFT JOIN attempts ON attempts.platform_model_id=configured.platform_model_id;")
|
||||
queue=$(jq -s '{
|
||||
samples:length,
|
||||
maxQueuedTasks:(map(.queuedTasks)|max),
|
||||
maxRunningTasks:(map(.runningTasks)|max),
|
||||
maxWaitingAdmissions:(map(.waitingAdmissions)|max),
|
||||
queueObserved:any(.queuedTasks > 0),
|
||||
admissionWaitObserved:any(.waitingAdmissions > 0),
|
||||
drainedAtEnd:(.[-1].queuedTasks == 0 and .[-1].runningTasks == 0 and .[-1].waitingAdmissions == 0)
|
||||
}' "$samples")
|
||||
workers=$(database_query "
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'instanceId',instance_id,'podName',pod_name,'site',site,
|
||||
'allocatedCapacity',allocated_capacity,'safeCapacity',safe_capacity,
|
||||
'heavyCapacity',heavy_capacity,'pressureState',pressure_state
|
||||
) ORDER BY site,instance_id),'[]'::jsonb)
|
||||
FROM gateway_worker_instances
|
||||
WHERE status='active' AND heartbeat_at > now() - interval '30 seconds';")
|
||||
distribution=$(database_query "
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object('workerInstanceId',worker_id,'tasks',tasks) ORDER BY worker_id),'[]'::jsonb)
|
||||
FROM (
|
||||
SELECT COALESCE(
|
||||
regexp_replace(job.attempted_by[cardinality(job.attempted_by)], '-exec-[^-]+-[^-]+$', ''),
|
||||
'unclaimed'
|
||||
) worker_id,
|
||||
count(*) tasks
|
||||
FROM gateway_tasks task
|
||||
LEFT JOIN river_job job ON job.id=task.river_job_id
|
||||
WHERE task.acceptance_run_id='$run_id'::uuid
|
||||
GROUP BY worker_id
|
||||
) grouped;")
|
||||
worker_peaks=$(jq -s '[.[].workers[]] | group_by(.instanceId) | map({
|
||||
instanceId:.[0].instanceId,
|
||||
peakActiveTasks:(map(.activeTasks)|max),
|
||||
peakPreparingTasks:(map(.preparingTasks)|max),
|
||||
peakWaitingUpstreamTasks:(map(.waitingUpstreamTasks)|max),
|
||||
peakFinalizingTasks:(map(.finalizingTasks)|max),
|
||||
minSafeCapacity:(map(.safeCapacity)|min),
|
||||
maxSafeCapacity:(map(.safeCapacity)|max),
|
||||
pressureStates:(map(.pressureState)|unique)
|
||||
})' "$worker_samples")
|
||||
limits=$(database_query "
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'scopeType',scope_type,'scopeKey',scope_key,'metric',metric,
|
||||
'limit',limit_value,'used',used_value,'reserved',reserved_value,
|
||||
'windowStart',window_start,'resetAt',reset_at
|
||||
) ORDER BY scope_type,scope_key,metric,window_start),'[]'::jsonb)
|
||||
FROM gateway_rate_limit_counters
|
||||
WHERE scope_key LIKE 'acceptance:$run_id:%';")
|
||||
concurrency_peaks=$(database_query "
|
||||
WITH events AS (
|
||||
SELECT scope_type,scope_key,limit_value,acquired_at event_at,lease_value delta
|
||||
FROM gateway_concurrency_leases WHERE scope_key LIKE 'acceptance:$run_id:%'
|
||||
UNION ALL
|
||||
SELECT scope_type,scope_key,limit_value,
|
||||
LEAST(COALESCE(released_at,expires_at),expires_at),-lease_value
|
||||
FROM gateway_concurrency_leases WHERE scope_key LIKE 'acceptance:$run_id:%'
|
||||
), points AS (
|
||||
SELECT scope_type,scope_key,limit_value,
|
||||
SUM(delta) OVER (PARTITION BY scope_type,scope_key ORDER BY event_at,delta ROWS UNBOUNDED PRECEDING) active
|
||||
FROM events
|
||||
), peaks AS (
|
||||
SELECT scope_type,scope_key,max(limit_value) limit_value,COALESCE(max(active),0) peak
|
||||
FROM points GROUP BY scope_type,scope_key
|
||||
)
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'scopeType',scope_type,'scopeKey',scope_key,'limit',limit_value,'peak',peak
|
||||
) ORDER BY scope_type,scope_key),'[]'::jsonb) FROM peaks;")
|
||||
leaks=$(database_query "
|
||||
SELECT jsonb_build_object(
|
||||
'activeConcurrencyLeases',(SELECT count(*) FROM gateway_concurrency_leases WHERE scope_key LIKE 'acceptance:$run_id:%' AND released_at IS NULL AND expires_at>statement_timestamp()),
|
||||
'activeRateReservations',(SELECT count(*) FROM gateway_rate_limit_reservations WHERE scope_key LIKE 'acceptance:$run_id:%' AND status='reserved'),
|
||||
'activeRiverJobs',(SELECT count(*) FROM river_job job JOIN gateway_tasks task ON task.river_job_id=job.id WHERE task.acceptance_run_id='$run_id'::uuid AND job.state IN ('available','pending','retryable','running','scheduled'))
|
||||
);")
|
||||
duplicates=$(database_query "
|
||||
SELECT jsonb_build_object(
|
||||
'remoteTaskIds',(SELECT count(*) FROM (SELECT remote_task_id FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid AND remote_task_id IS NOT NULL GROUP BY remote_task_id HAVING count(*)>1) duplicate),
|
||||
'billingTransactions',(SELECT count(*) FROM (SELECT reference_id,transaction_type FROM gateway_wallet_transactions WHERE reference_type='gateway_task' AND reference_id IN (SELECT id::text FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid) GROUP BY reference_id,transaction_type HAVING count(*)>1) duplicate)
|
||||
);")
|
||||
callbacks=$(database_query "
|
||||
SELECT jsonb_build_object(
|
||||
'deliveries',count(*) FILTER (WHERE callback.status='delivered'),
|
||||
'pending',count(*) FILTER (WHERE callback.status<>'delivered'),
|
||||
'duplicates',(SELECT count(*) FROM (
|
||||
SELECT callback_inner.task_id,callback_inner.seq,callback_inner.callback_url
|
||||
FROM gateway_task_callback_outbox callback_inner
|
||||
WHERE callback_inner.task_id IN (SELECT id FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid)
|
||||
GROUP BY callback_inner.task_id,callback_inner.seq,callback_inner.callback_url
|
||||
HAVING count(*)>1
|
||||
) duplicate)
|
||||
)
|
||||
FROM gateway_task_callback_outbox callback
|
||||
WHERE callback.task_id IN (SELECT id FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid);")
|
||||
resource_summary=$(provider_burst_resource_summary "$resources")
|
||||
if ! jq -e 'length==3 and all(.[]; .peak == .limit and .attempts > 0)' <<<"$platforms" >/dev/null; then passed=false; fi
|
||||
if ! jq -e '.queueObserved and .admissionWaitObserved and .drainedAtEnd and .maxQueuedTasks > 0 and .maxWaitingAdmissions > 0' <<<"$queue" >/dev/null; then passed=false; fi
|
||||
if ! jq -e --argjson requests "$requests" 'length==3 and all(.[]; .tasks>0 and .workerInstanceId!="unclaimed") and ([.[].tasks]|add)==$requests' <<<"$distribution" >/dev/null; then passed=false; fi
|
||||
if ! jq -e 'length==3 and all(.[]; .peakActiveTasks>0 and (.pressureStates|index("critical")|not))' <<<"$worker_peaks" >/dev/null; then passed=false; fi
|
||||
if ! jq -e 'length>0 and all(.[]; (.used+.reserved)<=.limit)' <<<"$limits" >/dev/null; then passed=false; fi
|
||||
if ! jq -e 'all(.[]; .peak<=.limit)' <<<"$concurrency_peaks" >/dev/null; then passed=false; fi
|
||||
if ! jq -e '.activeConcurrencyLeases==0 and .activeRateReservations==0 and .activeRiverJobs==0' <<<"$leaks" >/dev/null; then passed=false; fi
|
||||
if ! jq -e '.remoteTaskIds==0 and .billingTransactions==0' <<<"$duplicates" >/dev/null; then passed=false; fi
|
||||
if ! jq -e '.deliveries>0 and .pending==0 and .duplicates==0' <<<"$callbacks" >/dev/null; then passed=false; fi
|
||||
if ! jq -e --argjson requests "$requests" '.passed and ([.phases[].completed]|add)==$requests and ([.phases[].failed]|add)==0' "$load_report" >/dev/null; then passed=false; fi
|
||||
jq -n \
|
||||
--arg runId "$run_id" --arg model "$acceptance_video_model" \
|
||||
--argjson passed "$passed" --argjson requests "$requests" \
|
||||
--argjson phases "$(jq '.phases' "$load_report")" \
|
||||
--argjson platforms "$platforms" --argjson queue "$queue" \
|
||||
--argjson workers "$workers" --argjson distribution "$distribution" \
|
||||
--argjson workerPeaks "$worker_peaks" --argjson rateLimits "$limits" \
|
||||
--argjson concurrencyPeaks "$concurrency_peaks" \
|
||||
--argjson resources "$resource_summary" --argjson leaks "$leaks" \
|
||||
--argjson duplicates "$duplicates" --argjson callbacks "$callbacks" \
|
||||
'{
|
||||
schemaVersion:"acceptance-provider-burst-report/v1",
|
||||
runId:$runId,profile:"provider-burst",model:$model,
|
||||
startedAt:(now|todateiso8601),finishedAt:(now|todateiso8601),
|
||||
passed:$passed,secretSafe:true,requests:$requests,phases:$phases,
|
||||
providerRouting:{platforms:$platforms,totalConfiguredConcurrency:([$platforms[].limit]|add)},
|
||||
queue:$queue,
|
||||
cluster:{workers:$workers,workerPeaks:$workerPeaks,taskDistribution:$distribution,
|
||||
rateLimits:$rateLimits,concurrencyPeaks:$concurrencyPeaks,
|
||||
resourcePeaks:$resources,leaks:$leaks,duplicates:$duplicates,callbacks:$callbacks}
|
||||
}' >"$output"
|
||||
chmod 0600 "$output"
|
||||
jq -e '.passed==true and .secretSafe==true' "$output" >/dev/null
|
||||
}
|
||||
|
||||
provider_burst() {
|
||||
local requests=${AI_GATEWAY_LOCAL_PROVIDER_BURST_REQUESTS:-48}
|
||||
local load_report="$report_root/provider-burst-load.json"
|
||||
local samples="$report_root/provider-burst-samples.ndjson"
|
||||
local worker_samples="$report_root/provider-burst-worker-samples.ndjson"
|
||||
local resources="$report_root/provider-burst-resources.csv"
|
||||
local report="$report_root/provider-burst.json"
|
||||
worker_sample_stop="$report_root/provider-burst-samples.stop"
|
||||
[[ $requests =~ ^[0-9]+$ ]] && ((requests >= 24 && requests <= 256))
|
||||
[[ ! -e $load_report && ! -e $samples && ! -e $worker_samples && ! -e $resources && ! -e $report && ! -e $worker_sample_stop ]] ||
|
||||
fail_gate acceptance_report_exists "provider burst evidence already exists"
|
||||
current_phase=provider_burst_platforms
|
||||
setup_provider_burst_platforms
|
||||
provider_burst_configured=true
|
||||
current_phase=provider_burst_workers
|
||||
configure_adaptive_workers
|
||||
sample_provider_burst "$samples" "$worker_sample_stop" &
|
||||
active_provider_sampler_pid=$!
|
||||
sample_worker_runtime "$worker_samples" "$worker_sample_stop" &
|
||||
active_worker_sampler_pid=$!
|
||||
sample_resources "$resources" "$worker_sample_stop" &
|
||||
active_resource_sampler_pid=$!
|
||||
current_phase=provider_burst_load
|
||||
run_load video-throughput "$load_report" -requests "$requests"
|
||||
sleep 2
|
||||
touch "$worker_sample_stop"
|
||||
wait "$active_provider_sampler_pid"
|
||||
wait "$active_worker_sampler_pid"
|
||||
wait "$active_resource_sampler_pid"
|
||||
active_provider_sampler_pid=
|
||||
active_worker_sampler_pid=
|
||||
active_resource_sampler_pid=
|
||||
worker_sample_stop=
|
||||
current_phase=provider_burst_hard_gates
|
||||
verify_hard_gates
|
||||
current_phase=provider_burst_report
|
||||
build_provider_burst_report "$report" "$load_report" "$samples" "$worker_samples" "$resources" "$requests"
|
||||
restore_provider_burst_platforms
|
||||
provider_burst_configured=false
|
||||
echo "local_acceptance_provider_burst=PASS run_id=$run_id requests=$requests report=$report"
|
||||
}
|
||||
@@ -154,7 +154,7 @@ async function loadReports(directory) {
|
||||
const reports = [];
|
||||
for (const name of entries) {
|
||||
const report = await regularJSON(resolve(directory, name));
|
||||
if (report.schemaVersion === 'acceptance-load-report/v1') {
|
||||
if (report.schemaVersion === 'acceptance-load-report/v1' || report.schemaVersion === 'acceptance-load-report/v2') {
|
||||
if (report.secretSafe !== true) throw new Error(`load report ${name} is not secret-safe`);
|
||||
reports.push({
|
||||
file: name,
|
||||
@@ -163,6 +163,8 @@ async function loadReports(directory) {
|
||||
phases: report.phases,
|
||||
failure: report.failure,
|
||||
failureOperation: report.failureOperation,
|
||||
capacity: report.capacity,
|
||||
cluster: report.cluster,
|
||||
startedAt: report.startedAt,
|
||||
finishedAt: report.finishedAt
|
||||
});
|
||||
|
||||
@@ -13,20 +13,32 @@ snapshot="$state_root/snapshot.json"
|
||||
load_binary="$state_root/easyai-ai-gateway-acceptance-load"
|
||||
identity_file="$state_root/control-plane-identity.json"
|
||||
active_load_pid=
|
||||
active_worker_sampler_pid=
|
||||
active_resource_sampler_pid=
|
||||
active_provider_sampler_pid=
|
||||
worker_sample_stop=
|
||||
netem_active=false
|
||||
provider_burst_configured=false
|
||||
current_phase=initialization
|
||||
stable_profile=
|
||||
failure_gate_id=local_execution_incomplete
|
||||
# shellcheck source=scripts/acceptance/local-control-plane.sh
|
||||
source "$script_dir/local-control-plane.sh"
|
||||
# shellcheck source=scripts/acceptance/provider-burst.sh
|
||||
source "$script_dir/provider-burst.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/acceptance/run-local-acceptance.sh quick
|
||||
scripts/acceptance/run-local-acceptance.sh adaptive
|
||||
scripts/acceptance/run-local-acceptance.sh provider-burst
|
||||
scripts/acceptance/run-local-acceptance.sh full --release-manifest dist/releases/<SHA>.json
|
||||
scripts/acceptance/run-local-acceptance.sh artifact-smoke --release-manifest dist/releases/<SHA>.json
|
||||
|
||||
`adaptive` runs a three-Worker emulator-only concurrency ladder without changing
|
||||
provider limits. `provider-burst` binds one video model to three emulator-only
|
||||
platforms with concurrency limits 2/4/6 and validates burst queueing and routing.
|
||||
`full` executes P24/P28/P32 three times, the fault matrix, autoscaling/drain,
|
||||
80% soak, 120% overload, and exact linux/amd64 artifact smoke. The load process
|
||||
runs outside K3s and splits requests 50/50 across both TLS entrances.
|
||||
@@ -51,9 +63,24 @@ cleanup() {
|
||||
kill "$active_load_pid" >/dev/null 2>&1 || true
|
||||
wait "$active_load_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -n $active_worker_sampler_pid ]]; then
|
||||
[[ -z $worker_sample_stop ]] || touch "$worker_sample_stop"
|
||||
wait "$active_worker_sampler_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -n $active_resource_sampler_pid ]]; then
|
||||
[[ -z $worker_sample_stop ]] || touch "$worker_sample_stop"
|
||||
wait "$active_resource_sampler_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -n $active_provider_sampler_pid ]]; then
|
||||
[[ -z $worker_sample_stop ]] || touch "$worker_sample_stop"
|
||||
wait "$active_provider_sampler_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ $netem_active == true ]]; then
|
||||
"$script_dir/network-fault.sh" reset >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ $provider_burst_configured == true && -n ${runtime:-} ]]; then
|
||||
restore_provider_burst_platforms >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ $status -ne 0 && -n ${runtime:-} && -n ${report_root:-} && -f ${runtime:-} && -f $snapshot ]]; then
|
||||
restore_profile_best_effort "${stable_profile:-P24}"
|
||||
if [[ -f ${failure_gate_file:-} && ! -L ${failure_gate_file:-} ]]; then
|
||||
@@ -206,10 +233,281 @@ run_load() {
|
||||
return 1
|
||||
fi
|
||||
((load_status == 0)) || return "$load_status"
|
||||
jq -e '.schemaVersion == "acceptance-load-report/v1" and .secretSafe == true and .passed == true' \
|
||||
jq -e '.schemaVersion == "acceptance-load-report/v2" and .secretSafe == true and .passed == true' \
|
||||
"$report_path" >/dev/null
|
||||
}
|
||||
|
||||
sample_worker_runtime() {
|
||||
local output=$1 stop_file=$2
|
||||
: >"$output"
|
||||
while [[ ! -e $stop_file ]]; do
|
||||
database_query "
|
||||
SELECT jsonb_build_object(
|
||||
'sampledAt', to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'),
|
||||
'workers', COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'instanceId', instance_id,
|
||||
'allocatedCapacity', allocated_capacity,
|
||||
'safeCapacity', safe_capacity,
|
||||
'heavyCapacity', heavy_capacity,
|
||||
'activeTasks', active_tasks,
|
||||
'preparingTasks', preparing_tasks,
|
||||
'waitingUpstreamTasks', waiting_upstream_tasks,
|
||||
'finalizingTasks', finalizing_tasks,
|
||||
'pressureState', pressure_state,
|
||||
'pressureReason', pressure_reason
|
||||
) ORDER BY instance_id), '[]'::jsonb)
|
||||
)
|
||||
FROM gateway_worker_instances
|
||||
WHERE status='active' AND heartbeat_at > now() - interval '30 seconds';" >>"$output" || true
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_adaptive_workloads_settled() {
|
||||
local deadline=$((SECONDS + 180)) stable_samples=0 pod_state
|
||||
local api_total api_ready api_terminating worker_total worker_ready worker_terminating
|
||||
while ((SECONDS < deadline)); do
|
||||
pod_state=$(kubectl --context "$context" -n "$namespace" get pods \
|
||||
-l 'app.kubernetes.io/name in (easyai-api,easyai-worker)' -o json) || {
|
||||
stable_samples=0
|
||||
sleep 2
|
||||
continue
|
||||
}
|
||||
read -r api_total api_ready api_terminating worker_total worker_ready worker_terminating < <(
|
||||
jq -r '
|
||||
def total($name): [.items[] | select(.metadata.labels["app.kubernetes.io/name"] == $name and .metadata.deletionTimestamp == null)] | length;
|
||||
def ready($name): [.items[] | select(
|
||||
.metadata.labels["app.kubernetes.io/name"] == $name and
|
||||
.metadata.deletionTimestamp == null and
|
||||
.status.phase == "Running" and
|
||||
((.status.containerStatuses // []) | length) > 0 and
|
||||
all(.status.containerStatuses[]; .ready == true)
|
||||
)] | length;
|
||||
def terminating($name): [.items[] | select(.metadata.labels["app.kubernetes.io/name"] == $name and .metadata.deletionTimestamp != null)] | length;
|
||||
[total("easyai-api"), ready("easyai-api"), terminating("easyai-api"), total("easyai-worker"), ready("easyai-worker"), terminating("easyai-worker")] | @tsv
|
||||
' <<<"$pod_state"
|
||||
)
|
||||
if [[ $api_total == 2 && $api_ready == 2 && $api_terminating == 0 &&
|
||||
$worker_total == 3 && $worker_ready == 3 && $worker_terminating == 0 ]]; then
|
||||
stable_samples=$((stable_samples + 1))
|
||||
if ((stable_samples >= 3)); then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
stable_samples=0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fail_gate adaptive_workload_settle_timeout \
|
||||
"adaptive workloads did not settle at two Ready APIs and three Ready Workers without terminating Pods"
|
||||
}
|
||||
|
||||
configure_adaptive_workers() {
|
||||
local hard_limit=${AI_GATEWAY_LOCAL_ADAPTIVE_HARD_LIMIT:-32}
|
||||
local global_limit=$((hard_limit * 3))
|
||||
[[ $hard_limit =~ ^[0-9]+$ ]] && ((hard_limit >= 4 && hard_limit <= 256))
|
||||
kubectl --context "$context" -n "$namespace" patch configmap easyai-ai-gateway-config \
|
||||
--type=merge -p "$(jq -cn --arg hard "$hard_limit" --arg global "$global_limit" '{data:{
|
||||
AI_GATEWAY_WORKER_AUTOSCALING_ENABLED:"false",
|
||||
AI_GATEWAY_WORKER_LOAD_MODE:"adaptive",
|
||||
AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT:$hard,
|
||||
AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT:$global,
|
||||
AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT:$global
|
||||
}}')" >/dev/null
|
||||
kubectl --context "$context" -n "$namespace" rollout restart \
|
||||
deployment/easyai-capacity-controller >/dev/null
|
||||
kubectl --context "$context" -n "$namespace" rollout status \
|
||||
deployment/easyai-capacity-controller --timeout=10m
|
||||
kubectl --context "$context" -n "$namespace" scale deployment/easyai-worker-ningbo --replicas=2 >/dev/null
|
||||
kubectl --context "$context" -n "$namespace" scale deployment/easyai-worker-hongkong --replicas=1 >/dev/null
|
||||
for deployment in easyai-worker-ningbo easyai-worker-hongkong; do
|
||||
kubectl --context "$context" -n "$namespace" set env deployment/"$deployment" \
|
||||
AI_GATEWAY_WORKER_LOAD_MODE=adaptive \
|
||||
AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT="$hard_limit" >/dev/null
|
||||
kubectl --context "$context" -n "$namespace" rollout status deployment/"$deployment" --timeout=10m
|
||||
done
|
||||
for deployment in easyai-api-ningbo easyai-api-hongkong; do
|
||||
kubectl --context "$context" -n "$namespace" rollout restart deployment/"$deployment" >/dev/null
|
||||
kubectl --context "$context" -n "$namespace" rollout status deployment/"$deployment" --timeout=10m
|
||||
done
|
||||
wait_for_adaptive_workloads_settled
|
||||
local ready
|
||||
ready=$(kubectl --context "$context" -n "$namespace" get pods -l app.kubernetes.io/name=easyai-worker -o json |
|
||||
jq '[.items[] | select(.status.phase=="Running" and any(.status.containerStatuses[]?; .ready==true))] | length')
|
||||
[[ $ready == 3 ]] || fail_gate adaptive_worker_count "adaptive validation requires exactly three Ready Workers"
|
||||
}
|
||||
|
||||
run_adaptive_level() {
|
||||
local concurrency=$1 passed=true etcd_healthy=true
|
||||
local gemini_report="$report_root/adaptive-load-${concurrency}-gemini.json"
|
||||
local video_report="$report_root/adaptive-load-${concurrency}-video.json"
|
||||
verify_local_control_plane_identity "$context" "$cluster_name" "$identity_file" ||
|
||||
fail_gate local_control_plane_restarted "local K3s server identity changed before adaptive level $concurrency"
|
||||
record_local_control_plane_identity "$context" "$cluster_name" "$identity_file" ||
|
||||
fail_gate local_control_plane_identity "failed to record adaptive level $concurrency control-plane baseline"
|
||||
if ! run_load gemini-multi-image "$gemini_report" -requests "$concurrency"; then
|
||||
passed=false
|
||||
fi
|
||||
if [[ $passed == true ]] && ! run_load video-throughput "$video_report" -requests "$concurrency"; then
|
||||
passed=false
|
||||
fi
|
||||
local critical
|
||||
critical=$(database_query "SELECT count(*) FROM gateway_worker_instances WHERE status='active' AND pressure_state='critical' AND heartbeat_at > now() - interval '30 seconds';")
|
||||
if ((critical > 0)); then
|
||||
passed=false
|
||||
fi
|
||||
if ! verify_local_etcd_runtime_logs "$cluster_name" "$identity_file"; then
|
||||
passed=false
|
||||
etcd_healthy=false
|
||||
record_local_control_plane_identity "$context" "$cluster_name" "$identity_file" || return 1
|
||||
fi
|
||||
jq -n --argjson concurrency "$concurrency" --argjson passed "$passed" \
|
||||
--argjson etcdHealthy "$etcd_healthy" \
|
||||
--arg gemini "$(basename "$gemini_report")" --arg video "$(basename "$video_report")" \
|
||||
'{concurrency:$concurrency,passed:$passed,etcdHealthy:$etcdHealthy,reports:[$gemini,$video]}' \
|
||||
>"$report_root/adaptive-level-${concurrency}.json"
|
||||
chmod 0600 "$report_root/adaptive-level-${concurrency}.json"
|
||||
[[ $passed == true ]]
|
||||
}
|
||||
|
||||
build_adaptive_report() {
|
||||
local output=$1 samples=$2 resources=$3 highest=$4 failed=${5:-0}
|
||||
local levels workers distribution routing limits concurrency_peaks leaks duplicates sample_summary resource_summary phases
|
||||
levels=$(jq -s 'sort_by(.concurrency)' "$report_root"/adaptive-level-*.json)
|
||||
workers=$(database_query "
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'instanceId',instance_id,'podName',pod_name,'site',site,'status',status,
|
||||
'allocatedCapacity',allocated_capacity,'hardCapacityLimit',hard_capacity_limit,
|
||||
'safeCapacity',safe_capacity,'heavyCapacity',heavy_capacity,
|
||||
'activeTasks',active_tasks,'preparingTasks',preparing_tasks,
|
||||
'waitingUpstreamTasks',waiting_upstream_tasks,'finalizingTasks',finalizing_tasks,
|
||||
'pressureState',pressure_state,'pressureReason',pressure_reason,
|
||||
'loadSampledAt',load_sampled_at
|
||||
) ORDER BY site,instance_id),'[]'::jsonb)
|
||||
FROM gateway_worker_instances
|
||||
WHERE heartbeat_at > now() - interval '30 seconds';")
|
||||
distribution=$(database_query "
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object('workerInstanceId',worker_id,'tasks',tasks) ORDER BY worker_id),'[]'::jsonb)
|
||||
FROM (
|
||||
SELECT COALESCE(
|
||||
regexp_replace(job.attempted_by[cardinality(job.attempted_by)], '-exec-[^-]+-[^-]+$', ''),
|
||||
'unclaimed'
|
||||
) worker_id,
|
||||
count(*) tasks
|
||||
FROM gateway_tasks task
|
||||
LEFT JOIN river_job job ON job.id = task.river_job_id
|
||||
WHERE task.acceptance_run_id='$run_id'::uuid
|
||||
GROUP BY worker_id
|
||||
) grouped;")
|
||||
routing=$(database_query "
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'platformId',platform_id,'platformModelId',platform_model_id,'attempts',attempts
|
||||
) ORDER BY platform_id,platform_model_id),'[]'::jsonb)
|
||||
FROM (SELECT platform_id::text,platform_model_id::text,count(*) attempts
|
||||
FROM gateway_task_attempts WHERE task_id IN
|
||||
(SELECT id FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid)
|
||||
GROUP BY platform_id,platform_model_id) grouped;")
|
||||
limits=$(database_query "
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'scopeType',scope_type,'scopeKey',scope_key,'metric',metric,
|
||||
'limit',limit_value,'used',used_value,'reserved',reserved_value,
|
||||
'windowStart',window_start,'resetAt',reset_at
|
||||
) ORDER BY scope_type,scope_key,metric,window_start),'[]'::jsonb)
|
||||
FROM gateway_rate_limit_counters
|
||||
WHERE scope_key LIKE 'acceptance:$run_id:%';")
|
||||
concurrency_peaks=$(database_query "
|
||||
WITH events AS (
|
||||
SELECT scope_type,scope_key,limit_value,acquired_at event_at,lease_value delta FROM gateway_concurrency_leases
|
||||
WHERE scope_key LIKE 'acceptance:$run_id:%'
|
||||
UNION ALL
|
||||
SELECT scope_type,scope_key,limit_value,LEAST(COALESCE(released_at,expires_at),expires_at),-lease_value
|
||||
FROM gateway_concurrency_leases WHERE scope_key LIKE 'acceptance:$run_id:%'
|
||||
), points AS (
|
||||
SELECT scope_type,scope_key,limit_value,
|
||||
SUM(delta) OVER (PARTITION BY scope_type,scope_key ORDER BY event_at,delta ROWS UNBOUNDED PRECEDING) active
|
||||
FROM events
|
||||
), peaks AS (
|
||||
SELECT scope_type,scope_key,MAX(limit_value) limit_value,COALESCE(MAX(active),0) peak
|
||||
FROM points GROUP BY scope_type,scope_key
|
||||
)
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object('scopeType',scope_type,'scopeKey',scope_key,'limit',limit_value,'peak',peak)
|
||||
ORDER BY scope_type,scope_key),'[]'::jsonb) FROM peaks;")
|
||||
leaks=$(database_query "
|
||||
SELECT jsonb_build_object(
|
||||
'activeConcurrencyLeases',(SELECT count(*) FROM gateway_concurrency_leases WHERE scope_key LIKE 'acceptance:$run_id:%' AND released_at IS NULL AND expires_at>now()),
|
||||
'activeRateReservations',(SELECT count(*) FROM gateway_rate_limit_reservations WHERE scope_key LIKE 'acceptance:$run_id:%' AND status='reserved'),
|
||||
'activeRiverJobs',(SELECT count(*) FROM river_job job JOIN gateway_tasks task ON task.river_job_id=job.id WHERE task.acceptance_run_id='$run_id'::uuid AND job.state IN ('available','pending','retryable','running','scheduled'))
|
||||
);")
|
||||
duplicates=$(database_query "
|
||||
SELECT jsonb_build_object(
|
||||
'remoteTaskIds',(SELECT count(*) FROM (SELECT remote_task_id FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid AND remote_task_id IS NOT NULL GROUP BY remote_task_id HAVING count(*)>1) duplicate),
|
||||
'billingTransactions',(SELECT count(*) FROM (SELECT reference_id,transaction_type FROM gateway_wallet_transactions WHERE reference_type='gateway_task' AND reference_id IN (SELECT id::text FROM gateway_tasks WHERE acceptance_run_id='$run_id'::uuid) GROUP BY reference_id,transaction_type HAVING count(*)>1) duplicate)
|
||||
);")
|
||||
sample_summary=$(jq -s '[.[].workers[]] | group_by(.instanceId) | map({
|
||||
instanceId:.[0].instanceId,
|
||||
peakActiveTasks:(map(.activeTasks)|max),
|
||||
peakPreparingTasks:(map(.preparingTasks)|max),
|
||||
peakWaitingUpstreamTasks:(map(.waitingUpstreamTasks)|max),
|
||||
peakFinalizingTasks:(map(.finalizingTasks)|max),
|
||||
minSafeCapacity:(map(.safeCapacity)|min),
|
||||
maxSafeCapacity:(map(.safeCapacity)|max),
|
||||
pressureStates:(map(.pressureState)|unique)
|
||||
})' "$samples")
|
||||
resource_summary=$(jq -Rn '
|
||||
def number_without($suffix): rtrimstr($suffix) | tonumber;
|
||||
def memory_mib:
|
||||
if endswith("Gi") then number_without("Gi") * 1024
|
||||
elif endswith("Mi") then number_without("Mi")
|
||||
elif endswith("Ki") then number_without("Ki") / 1024
|
||||
else tonumber end;
|
||||
[inputs | split(",") | select(.[0] != "timestamp") | {
|
||||
scope:.[1],name:.[2],cpu:.[3],memory:.[4]
|
||||
}]
|
||||
| group_by([.scope,.name])
|
||||
| map(if .[0].scope == "node" then {
|
||||
scope:"node",name:.[0].name,
|
||||
maxCpuPercent:(map(.cpu | number_without("%")) | max),
|
||||
maxMemoryPercent:(map(.memory | number_without("%")) | max)
|
||||
} else {
|
||||
scope:"pod",name:.[0].name,
|
||||
maxCpuMillicores:(map(.cpu | if endswith("m") then number_without("m") else tonumber * 1000 end) | max),
|
||||
maxMemoryMiB:(map(.memory | memory_mib) | max)
|
||||
} end)' <"$resources")
|
||||
phases=$(jq -s '[.[] | .phases[]]' "$report_root"/adaptive-load-*.json)
|
||||
local passed=true bottleneck=configured_probe_ceiling
|
||||
if ((highest < 1)); then passed=false; fi
|
||||
if ((failed > 0)); then bottleneck=worker_or_gateway_limit; fi
|
||||
if jq -e 'any(.[]; .etcdHealthy == false)' <<<"$levels" >/dev/null; then
|
||||
bottleneck=local_control_plane_limit
|
||||
fi
|
||||
if ! jq -e 'length == 3' <<<"$workers" >/dev/null; then passed=false; fi
|
||||
if ! jq -e 'all(.[]; ((.used + .reserved) <= .limit))' <<<"$limits" >/dev/null; then passed=false; fi
|
||||
if ! jq -e 'all(.[]; (.limit == null or .peak <= .limit))' <<<"$concurrency_peaks" >/dev/null; then passed=false; fi
|
||||
if ! jq -e '.activeConcurrencyLeases==0 and .activeRateReservations==0 and .activeRiverJobs==0' <<<"$leaks" >/dev/null; then passed=false; fi
|
||||
if ! jq -e '.remoteTaskIds==0 and .billingTransactions==0' <<<"$duplicates" >/dev/null; then passed=false; fi
|
||||
jq -n \
|
||||
--arg runId "$run_id" --arg startedAt "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
|
||||
--arg bottleneck "$bottleneck" --argjson passed "$passed" \
|
||||
--argjson highest "$highest" --argjson failed "$failed" \
|
||||
--argjson levels "$levels" --argjson phases "$phases" \
|
||||
--argjson workers "$workers" --argjson distribution "$distribution" \
|
||||
--argjson routing "$routing" --argjson limits "$limits" \
|
||||
--argjson concurrencyPeaks "$concurrency_peaks" --argjson workerPeaks "$sample_summary" \
|
||||
--argjson resourcePeaks "$resource_summary" \
|
||||
--argjson leaks "$leaks" --argjson duplicates "$duplicates" \
|
||||
'{
|
||||
schemaVersion:"acceptance-load-report/v2",runId:$runId,profile:"adaptive-capacity",
|
||||
startedAt:$startedAt,finishedAt:(now|todateiso8601),passed:$passed,secretSafe:true,
|
||||
phases:$phases,
|
||||
capacity:{levels:$levels,highestStableConcurrency:$highest,firstFailedConcurrency:$failed,bottleneck:$bottleneck},
|
||||
cluster:{workers:$workers,workerPeaks:$workerPeaks,taskDistribution:$distribution,
|
||||
platformRouting:$routing,rateLimits:$limits,concurrencyPeaks:$concurrencyPeaks,
|
||||
resourcePeaks:$resourcePeaks,leaks:$leaks,duplicates:$duplicates}
|
||||
}' >"$output"
|
||||
chmod 0600 "$output"
|
||||
jq -e '.passed==true and .secretSafe==true' "$output" >/dev/null
|
||||
}
|
||||
|
||||
sample_resources() {
|
||||
local output=$1 stop_file=$2
|
||||
printf 'timestamp,scope,name,cpu,memory\n' >"$output"
|
||||
@@ -567,6 +865,64 @@ quick() {
|
||||
echo "local_acceptance_quick=PASS run_id=$run_id report=$report_root/quick.json"
|
||||
}
|
||||
|
||||
adaptive() {
|
||||
local hard_limit=${AI_GATEWAY_LOCAL_ADAPTIVE_HARD_LIMIT:-32}
|
||||
local max_probe=${AI_GATEWAY_LOCAL_ADAPTIVE_MAX_CONCURRENCY:-$((hard_limit * 3))}
|
||||
local level=4 highest=0 failed=0 low high mid
|
||||
local samples="$report_root/adaptive-worker-samples.ndjson"
|
||||
local resources="$report_root/adaptive-resources.csv"
|
||||
worker_sample_stop="$report_root/adaptive-worker-samples.stop"
|
||||
[[ ! -e $samples && ! -e $resources && ! -e $worker_sample_stop ]] || fail_gate acceptance_report_exists "adaptive evidence already exists"
|
||||
current_phase=adaptive_workers
|
||||
configure_adaptive_workers
|
||||
sample_worker_runtime "$samples" "$worker_sample_stop" &
|
||||
active_worker_sampler_pid=$!
|
||||
sample_resources "$resources" "$worker_sample_stop" &
|
||||
active_resource_sampler_pid=$!
|
||||
while ((level <= max_probe)); do
|
||||
current_phase="adaptive_capacity_$level"
|
||||
if run_adaptive_level "$level"; then
|
||||
highest=$level
|
||||
level=$((level * 2))
|
||||
else
|
||||
failed=$level
|
||||
break
|
||||
fi
|
||||
done
|
||||
if ((failed > 0 && highest > 0)); then
|
||||
low=$((highest + 1))
|
||||
high=$((failed - 1))
|
||||
while ((low <= high)); do
|
||||
mid=$(((low + high) / 2))
|
||||
current_phase="adaptive_capacity_refine_$mid"
|
||||
if run_adaptive_level "$mid"; then
|
||||
highest=$mid
|
||||
low=$((mid + 1))
|
||||
else
|
||||
failed=$mid
|
||||
high=$((mid - 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
touch "$worker_sample_stop"
|
||||
wait "$active_worker_sampler_pid"
|
||||
wait "$active_resource_sampler_pid"
|
||||
active_worker_sampler_pid=
|
||||
active_resource_sampler_pid=
|
||||
worker_sample_stop=
|
||||
local ready_workers registered_workers
|
||||
ready_workers=$(kubectl --context "$context" -n "$namespace" get pods \
|
||||
-l app.kubernetes.io/name=easyai-worker -o json |
|
||||
jq '[.items[] | select(.status.phase=="Running" and any(.status.containerStatuses[]?; .ready==true))] | length')
|
||||
registered_workers=$(database_query "SELECT count(*) FROM gateway_worker_instances WHERE status='active' AND heartbeat_at > now() - interval '30 seconds';")
|
||||
[[ $ready_workers == 3 && $registered_workers == 3 ]] ||
|
||||
fail_gate adaptive_worker_count "adaptive validation requires three Ready and registered Workers throughout the final gate"
|
||||
verify_hard_gates
|
||||
current_phase=adaptive_report
|
||||
build_adaptive_report "$report_root/adaptive-capacity.json" "$samples" "$resources" "$highest" "$failed"
|
||||
echo "local_acceptance_adaptive=PASS run_id=$run_id highest_stable_concurrency=$highest first_failed_concurrency=$failed report=$report_root/adaptive-capacity.json"
|
||||
}
|
||||
|
||||
full() {
|
||||
local manifest=$1 profile repetition
|
||||
quick
|
||||
@@ -610,7 +966,7 @@ full() {
|
||||
command=${1:-}
|
||||
shift || true
|
||||
case $command in
|
||||
quick)
|
||||
quick|adaptive|provider-burst)
|
||||
[[ $# -eq 0 ]] || { usage >&2; exit 64; }
|
||||
;;
|
||||
artifact-smoke|full)
|
||||
@@ -628,6 +984,12 @@ case $command in
|
||||
quick)
|
||||
quick
|
||||
;;
|
||||
adaptive)
|
||||
adaptive
|
||||
;;
|
||||
provider-burst)
|
||||
provider_burst
|
||||
;;
|
||||
artifact-smoke)
|
||||
artifact_smoke "$2"
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user