Skip to content

Commit e8be6a0

Browse files
GitHub #461: TD-S056: bound every in-memory cache and every metric label-set cardinality (#124)
1 parent 435f927 commit e8be6a0

4 files changed

Lines changed: 226 additions & 21 deletions

File tree

.github/workflows/server-perf.yml

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,77 @@
11
name: Server Perf
22

33
on:
4+
pull_request:
5+
branches: [main]
6+
paths:
7+
- ".github/workflows/server-perf.yml"
8+
- "Dockerfile"
9+
- "docker-compose.yml"
10+
- "app/Support/HistoryRetentionEnforcer.php"
11+
- "app/Support/BoundedMetricPolicy.php"
12+
- "app/Support/LongPollSignalStore.php"
13+
- "app/Support/ProjectionDriftMetrics.php"
14+
- "app/Support/ServerReadiness.php"
15+
- "app/Support/ServerPollingCache.php"
16+
- "app/Support/TaskQueueAdmission.php"
17+
- "app/Support/WorkflowQueryTaskBroker.php"
18+
- "app/Support/WorkflowTaskFailureMetrics.php"
19+
- "app/Support/WorkflowTaskPoller.php"
20+
- "app/Support/WorkflowTaskPollRequestStore.php"
21+
- "app/Http/Controllers/Api/SystemController.php"
22+
- "config/dw-bounded-growth.php"
23+
- "config/server.php"
24+
- "docs/bounded-growth.md"
25+
- "routes/api.php"
26+
- "scripts/perf/**"
27+
- "tests/Unit/BoundedGrowthPolicyTest.php"
28+
- "tests/Unit/BoundedMetricPolicyTest.php"
29+
- "tests/Unit/ServerPerfHarnessContractTest.php"
30+
- "tests/Feature/SystemMetricsTest.php"
31+
push:
32+
branches: [main]
33+
paths:
34+
- ".github/workflows/server-perf.yml"
35+
- "Dockerfile"
36+
- "docker-compose.yml"
37+
- "app/Support/HistoryRetentionEnforcer.php"
38+
- "app/Support/BoundedMetricPolicy.php"
39+
- "app/Support/LongPollSignalStore.php"
40+
- "app/Support/ProjectionDriftMetrics.php"
41+
- "app/Support/ServerReadiness.php"
42+
- "app/Support/ServerPollingCache.php"
43+
- "app/Support/TaskQueueAdmission.php"
44+
- "app/Support/WorkflowQueryTaskBroker.php"
45+
- "app/Support/WorkflowTaskFailureMetrics.php"
46+
- "app/Support/WorkflowTaskPoller.php"
47+
- "app/Support/WorkflowTaskPollRequestStore.php"
48+
- "app/Http/Controllers/Api/SystemController.php"
49+
- "config/dw-bounded-growth.php"
50+
- "config/server.php"
51+
- "docs/bounded-growth.md"
52+
- "routes/api.php"
53+
- "scripts/perf/**"
54+
- "tests/Unit/BoundedGrowthPolicyTest.php"
55+
- "tests/Unit/BoundedMetricPolicyTest.php"
56+
- "tests/Unit/ServerPerfHarnessContractTest.php"
57+
- "tests/Feature/SystemMetricsTest.php"
58+
schedule:
59+
- cron: "17 7 * * *"
460
workflow_dispatch:
61+
inputs:
62+
duration_seconds:
63+
description: "Soak duration in seconds"
64+
required: false
65+
default: "7200"
66+
concurrency:
67+
description: "Concurrent long-poll workers"
68+
required: false
69+
default: "24"
70+
remote_write:
71+
description: "Enable Prometheus remote_write when variables/secrets are configured"
72+
required: false
73+
type: boolean
74+
default: true
575

676
permissions:
777
contents: read
@@ -13,36 +83,29 @@ concurrency:
1383
jobs:
1484
contract:
1585
name: Bounded-growth contract
16-
if: github.event_name == 'pull_request' || github.event_name == 'push'
1786
runs-on: ubuntu-latest
87+
if: github.event_name == 'pull_request' || github.event_name == 'push'
1888
timeout-minutes: 20
1989

2090
steps:
2191
- name: Checkout server
2292
uses: actions/checkout@v6
2393

2494
- name: Checkout workflow package
25-
uses: actions/checkout@v6
26-
with:
27-
repository: durable-workflow/workflow
28-
ref: v2
29-
path: workflow-package
95+
run: git clone --depth=1 --branch v2 https://github.com/durable-workflow/workflow.git workflow-package
3096

3197
- name: Run bounded-growth contract tests
3298
run: |
33-
docker run --rm \
34-
-u "$(id -u):$(id -g)" \
35-
-v "${PWD}:/app" \
36-
-v "${PWD}/workflow-package:/workflow:ro" \
37-
-w /app \
38-
composer:2 \
39-
sh -lc 'composer install --no-interaction --no-progress --prefer-dist && vendor/bin/phpunit tests/Unit/BoundedGrowthPolicyTest.php tests/Unit/ServerPerfHarnessContractTest.php --colors=never'
99+
# Stream sources into the test container so containerized runners avoid host bind-path assumptions.
100+
tar --exclude=.git --exclude=vendor --exclude=build -cf - . \
101+
| docker run --rm -i -w /app composer:2 \
102+
sh -lc 'tar -xf - -C /app && cp -a /app/workflow-package /workflow && composer install --no-interaction --no-progress --prefer-dist && vendor/bin/phpunit tests/Unit/BoundedGrowthPolicyTest.php tests/Unit/ServerPerfHarnessContractTest.php --colors=never'
40103
41104
smoke:
42105
name: Polling cache bounded-growth smoke
43-
if: github.event_name == 'pull_request' || github.event_name == 'push'
44106
needs: contract
45107
runs-on: ubuntu-latest
108+
if: github.event_name == 'pull_request' || github.event_name == 'push'
46109
timeout-minutes: 45
47110

48111
steps:
@@ -61,13 +124,22 @@ jobs:
61124
DW_PERF_MAX_SERVER_MEMORY_MB: "768"
62125
DW_PERF_MAX_POLLING_KEYS: "512"
63126
DW_PERF_MAX_FINAL_POLLING_KEYS: "0"
127+
DW_PERF_MIN_SAMPLE_COVERAGE: "0.75"
64128
DW_PERF_MAX_SERVER_CACHE_KEYS_BY_POLICY: '{"workflow_task_poll_requests":512,"long_poll_signals":512,"workflow_query_tasks":64,"task_queue_admission_locks":128,"task_queue_dispatch_counters":128,"workflow_task_expired_lease_recovery":128,"history_retention_inline":64,"readiness_probe":16}'
65129
DW_PERF_MAX_FINAL_SERVER_CACHE_KEYS_BY_POLICY: '{"workflow_task_poll_requests":0,"long_poll_signals":0,"workflow_query_tasks":0,"task_queue_admission_locks":0,"task_queue_dispatch_counters":0,"workflow_task_expired_lease_recovery":0,"history_retention_inline":0,"readiness_probe":0}'
66130
RUNNER_ENVIRONMENT: "github-hosted"
67131
run: scripts/perf/run-server-soak.sh
68132

69133
- name: Upload perf artifacts
70-
if: always()
134+
if: always() && github.server_url == 'https://github.com'
135+
uses: actions/upload-artifact@v4
136+
with:
137+
name: server-perf-smoke
138+
path: build/perf/
139+
if-no-files-found: warn
140+
141+
- name: Upload perf artifacts on compatible Actions servers
142+
if: always() && github.server_url != 'https://github.com'
71143
uses: actions/upload-artifact@v3.2.2
72144
with:
73145
name: server-perf-smoke
@@ -76,8 +148,8 @@ jobs:
76148

77149
soak:
78150
name: Self-hosted polling cache soak
79-
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
80151
runs-on: [self-hosted, linux, x64, perf-soak, server-perf]
152+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
81153
timeout-minutes: 390
82154

83155
steps:
@@ -105,7 +177,15 @@ jobs:
105177
run: scripts/perf/run-server-soak.sh
106178

107179
- name: Upload perf artifacts
108-
if: always()
180+
if: always() && github.server_url == 'https://github.com'
181+
uses: actions/upload-artifact@v4
182+
with:
183+
name: server-perf-soak
184+
path: build/perf/
185+
if-no-files-found: warn
186+
187+
- name: Upload perf artifacts on compatible Actions servers
188+
if: always() && github.server_url != 'https://github.com'
109189
uses: actions/upload-artifact@v3.2.2
110190
with:
111191
name: server-perf-soak

scripts/perf/run-server-soak.sh

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ YAML
130130

131131
server_base_url() {
132132
local base_url="http://127.0.0.1:${SERVER_PORT}"
133+
local docker_internal_url="http://host.docker.internal:${SERVER_PORT}"
133134
local docker_host_url
134135
local docker_host_ip
135136
local server_id
@@ -140,6 +141,11 @@ server_base_url() {
140141
return
141142
fi
142143

144+
if curl -fsS --max-time 2 "$docker_internal_url/api/health" >/dev/null 2>&1; then
145+
echo "$docker_internal_url"
146+
return
147+
fi
148+
143149
docker_host_ip="$(ip route 2>/dev/null | awk '/default/ {print $3; exit}')"
144150
if [ -n "$docker_host_ip" ]; then
145151
docker_host_url="http://${docker_host_ip}:${SERVER_PORT}"
@@ -153,8 +159,11 @@ server_base_url() {
153159
server_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$server_id" 2>/dev/null || true)"
154160

155161
if [ -n "$server_ip" ]; then
156-
echo "http://${server_ip}:8080"
157-
return
162+
local server_container_url="http://${server_ip}:8080"
163+
if curl -fsS --max-time 2 "$server_container_url/api/health" >/dev/null 2>&1; then
164+
echo "$server_container_url"
165+
return
166+
fi
158167
fi
159168

160169
echo "$base_url"

scripts/perf/server_soak.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
from typing import Any
2525

2626

27-
CONTROL_PLANE_VERSION = "2"
28-
WORKER_PROTOCOL_VERSION = "1.0"
27+
CONTROL_PLANE_VERSION = os.environ.get("DW_PERF_CONTROL_PLANE_VERSION", "2")
28+
WORKER_PROTOCOL_VERSION = os.environ.get("DW_PERF_WORKER_PROTOCOL_VERSION", "1.2")
2929
ERROR_WRITE_LOCK = threading.Lock()
3030
SERVER_CACHE_KEY_PATTERNS = {
3131
"long_poll_signals": "*server:long-poll-signal:*",

tests/Unit/ServerPerfHarnessContractTest.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,30 @@ public function test_ci_perf_jobs_set_runner_environment_provenance(): void
187187
);
188188
}
189189

190+
public function test_server_perf_jobs_keep_event_split_guards(): void
191+
{
192+
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/server-perf.yml');
193+
$this->assertNotFalse($workflow, '.github/workflows/server-perf.yml must be readable');
194+
195+
$this->assertMatchesRegularExpression(
196+
"/contract:\\s+name:\\s+Bounded-growth contract\\s+runs-on:\\s+ubuntu-latest\\s+if:\\s+github\\.event_name == 'pull_request' \\|\\| github\\.event_name == 'push'/s",
197+
$workflow,
198+
'Contract checks should only run for pull_request/push events, with runs-on before if for runner compatibility.',
199+
);
200+
201+
$this->assertMatchesRegularExpression(
202+
"/smoke:\\s+name:\\s+Polling cache bounded-growth smoke\\s+needs:\\s+contract\\s+runs-on:\\s+ubuntu-latest\\s+if:\\s+github\\.event_name == 'pull_request' \\|\\| github\\.event_name == 'push'/s",
203+
$workflow,
204+
'Short perf smokes should only run for pull_request/push events.',
205+
);
206+
207+
$this->assertMatchesRegularExpression(
208+
"/soak:\\s+name:\\s+Self-hosted polling cache soak\\s+runs-on:\\s+\\[self-hosted, linux, x64, perf-soak, server-perf\\]\\s+if:\\s+github\\.event_name == 'schedule' \\|\\| github\\.event_name == 'workflow_dispatch'/s",
209+
$workflow,
210+
'Trusted long soaks should only run for schedule/workflow_dispatch events.',
211+
);
212+
}
213+
190214
public function test_self_hosted_perf_soak_requires_trusted_evidence_eligibility(): void
191215
{
192216
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/server-perf.yml');
@@ -211,6 +235,98 @@ public function test_self_hosted_perf_soak_requires_trusted_evidence_eligibility
211235
);
212236
}
213237

238+
public function test_server_perf_artifact_uploads_use_github_current_action_with_compatible_fallback(): void
239+
{
240+
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/server-perf.yml');
241+
$this->assertNotFalse($workflow, '.github/workflows/server-perf.yml must be readable');
242+
243+
$this->assertSame(2, substr_count($workflow, 'uses: actions/upload-artifact@v4'));
244+
$this->assertSame(2, substr_count($workflow, 'uses: actions/upload-artifact@v3.2.2'));
245+
$this->assertSame(2, substr_count($workflow, "github.server_url == 'https://github.com'"));
246+
$this->assertSame(2, substr_count($workflow, "github.server_url != 'https://github.com'"));
247+
}
248+
249+
public function test_server_perf_soak_uses_current_worker_protocol_default(): void
250+
{
251+
$source = file_get_contents(dirname(__DIR__, 2).'/scripts/perf/server_soak.py');
252+
$this->assertNotFalse($source, 'scripts/perf/server_soak.py must be readable');
253+
254+
$this->assertStringContainsString(
255+
'WORKER_PROTOCOL_VERSION = os.environ.get("DW_PERF_WORKER_PROTOCOL_VERSION", "1.2")',
256+
$source,
257+
);
258+
$this->assertStringContainsString(
259+
'headers["X-Durable-Workflow-Protocol-Version"] = WORKER_PROTOCOL_VERSION',
260+
$source,
261+
);
262+
$this->assertStringNotContainsString('WORKER_PROTOCOL_VERSION = "1.0"', $source);
263+
}
264+
265+
public function test_short_perf_smoke_keeps_flake_resistant_sample_coverage_floor(): void
266+
{
267+
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/server-perf.yml');
268+
$this->assertNotFalse($workflow, '.github/workflows/server-perf.yml must be readable');
269+
270+
$this->assertMatchesRegularExpression(
271+
'/name:\s+Polling cache bounded-growth smoke(?P<block>.*?)\n\s+soak:/s',
272+
$workflow,
273+
'Server Perf workflow must keep a distinct short smoke job before the long soak job.',
274+
);
275+
preg_match('/name:\s+Polling cache bounded-growth smoke(?P<block>.*?)\n\s+soak:/s', $workflow, $smokeMatch);
276+
277+
$this->assertStringContainsString(
278+
'DW_PERF_MIN_SAMPLE_COVERAGE: "0.75"',
279+
(string) ($smokeMatch['block'] ?? ''),
280+
'Short perf smokes should tolerate one slow compose-backed sample without losing coverage signal.',
281+
);
282+
}
283+
284+
public function test_server_perf_base_url_probe_supports_containerized_actions_runners(): void
285+
{
286+
$source = file_get_contents(dirname(__DIR__, 2).'/scripts/perf/run-server-soak.sh');
287+
$this->assertNotFalse($source, 'scripts/perf/run-server-soak.sh must be readable');
288+
289+
$this->assertStringContainsString('http://host.docker.internal:${SERVER_PORT}', $source);
290+
$this->assertMatchesRegularExpression(
291+
'/host\.docker\.internal.*?docker_host_ip.*?docker inspect/s',
292+
$source,
293+
'Perf smoke should prefer host-published ports before falling back to direct container addresses.',
294+
);
295+
$this->assertMatchesRegularExpression(
296+
'/server_container_url="http:\/\/\$\{server_ip\}:8080".*?curl -fsS --max-time 2 "\$server_container_url\/api\/health"/s',
297+
$source,
298+
'Perf smoke should only select a direct container address after confirming the runner can reach it.',
299+
);
300+
}
301+
302+
public function test_server_perf_workflow_can_produce_trusted_long_soak_evidence(): void
303+
{
304+
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/server-perf.yml');
305+
$this->assertNotFalse($workflow, '.github/workflows/server-perf.yml must be readable');
306+
307+
foreach ([
308+
'schedule:',
309+
'cron: "17 7 * * *"',
310+
'workflow_dispatch:',
311+
'duration_seconds:',
312+
'default: "7200"',
313+
'concurrency:',
314+
'default: "24"',
315+
'remote_write:',
316+
'type: boolean',
317+
"github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'",
318+
'runs-on: [self-hosted, linux, x64, perf-soak, server-perf]',
319+
'DW_PERF_REQUIRE_TRUSTED_EVIDENCE: "true"',
320+
'RUNNER_ENVIRONMENT: "self-hosted"',
321+
] as $needle) {
322+
$this->assertStringContainsString(
323+
$needle,
324+
$workflow,
325+
"Server Perf workflow must retain trusted long-soak trigger support for {$needle}.",
326+
);
327+
}
328+
}
329+
214330
public function test_ci_perf_trigger_paths_cover_bounded_growth_runtime_surfaces(): void
215331
{
216332
$repoRoot = dirname(__DIR__, 2);

0 commit comments

Comments
 (0)