Skip to content

Commit 6472d18

Browse files
committed
FEATURE-ci-darwin-header-deadline: Prepare native archive deadline diagnostic checkpoint
Add temporary bounded compile-time traces for the actual watchdog, timer completion, external dispatch and socket close path. Manually run only macOS against the exact failing c0 selected archive plus an explicitly hashed diagnostic overlay. Reuse strict artifact verification and extraction, keep read-only permissions, and preserve push/PR matrix commands and original test budgets/assertions. Keep the task In Progress for native evidence and later TDD repair/removal. Enabled/disabled syntax, trace contract, workflow syntax and changed-file cpplint passed; actual selected archive overlay and extracted-source syntax preflight passed. No full baseline, repair, push or merge.
1 parent c0cfdab commit 6472d18

11 files changed

Lines changed: 344 additions & 1 deletion

‎.github/workflows/v3-package-validation.yml‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ permissions:
99
contents: read
1010
jobs:
1111
linux:
12+
if: github.event_name != 'workflow_dispatch'
1213
runs-on: ubuntu-24.04
1314
timeout-minutes: 90
1415
steps:
@@ -44,6 +45,9 @@ jobs:
4445
retention-days: 14
4546
if-no-files-found: error
4647
macos:
48+
permissions:
49+
contents: read
50+
actions: read
4751
runs-on: macos-15
4852
timeout-minutes: 90
4953
steps:
@@ -67,12 +71,125 @@ jobs:
6771
- name: Bootstrap
6872
run: ./bootstrap
6973
- name: Installed matrix
74+
if: github.event_name != 'workflow_dispatch'
7075
run: |
7176
prefix=()
7277
if ! python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",443)); s.close()'; then
7378
prefix=(sudo env "PATH=$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory "GIT_CONFIG_VALUE_0=$PWD")
7479
fi
7580
"${prefix[@]}" python3 scripts/check-v3-installed-consumer.py --output "$RUNNER_TEMP/package-evidence/matrix" --provider-prefix "$RUNNER_TEMP/package-evidence/prerequisites/provider" --pkg-config-dir "$(brew --prefix)/share/pkgconfig" --client-python "$RUNNER_TEMP/package-evidence/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --toolchain-prefix "$V3_PACKAGE_TOOLCHAIN" --jobs 2
81+
# Branch-only checkpoint: remove before merging to v3. This is not matrix proof.
82+
- name: Diagnose installed shared TLS-off external HTTP1 on Darwin
83+
if: github.event_name == 'workflow_dispatch'
84+
env:
85+
GH_TOKEN: ${{ github.token }}
86+
run: |
87+
python3 - <<'PY_DIAGNOSTIC'
88+
import hashlib, importlib.util, json, os, platform, shlex, shutil, subprocess, sys
89+
from pathlib import Path
90+
checkout = Path.cwd().resolve()
91+
sys.path.insert(0, str(checkout / 'scripts/lib'))
92+
import package_matrix as matrix
93+
from package_commands import Commands, runtime_environment
94+
from package_integration import compile_probe
95+
import release_artifact as identity
96+
from release_checks import extract_archive
97+
from release_ci import verify_download
98+
spec = importlib.util.spec_from_file_location('audit', checkout / 'scripts/audit-v3-installed-package.py')
99+
audit = importlib.util.module_from_spec(spec)
100+
spec.loader.exec_module(audit)
101+
assert platform.system() == 'Darwin'
102+
output = Path(os.environ['RUNNER_TEMP']) / 'package-evidence/header-deadline-diagnostic'
103+
output.mkdir()
104+
build, prefix, integration = [output / name for name in ('build', 'prefix', 'integration-shared')]
105+
for directory in (build, prefix, integration):
106+
directory.mkdir()
107+
command = Commands(output)
108+
provider = output.parent / 'prerequisites/provider'
109+
compiler, cc = os.environ['V3_CXX'], os.environ['V3_CC']
110+
toolchain = Path(os.environ['V3_PACKAGE_TOOLCHAIN'])
111+
env = audit.clean_environment()
112+
for key in ('GH_TOKEN', 'GITHUB_TOKEN', 'V3_RELEASE_IDENTITY'):
113+
env.pop(key, None)
114+
env.update(CXX=compiler, CC=cc, CXXFLAGS='-O0 -g',
115+
CPPFLAGS='-DHTTPSERVER_DIAG_HEADER_DEADLINE',
116+
LDFLAGS='-Wl,-rpath,' + str(provider / 'lib'))
117+
receipt = dict(kind='diagnostic-only; not complete package validation', tls='no', linkage='shared',
118+
revision=command(['git', 'rev-parse', 'HEAD'], checkout, env).strip(),
119+
compiler=command([compiler, '--version'], checkout, env),
120+
provider_header_sha256=hashlib.sha256((provider / 'include/openssl/opensslv.h').read_bytes()).hexdigest())
121+
try:
122+
expected = dict(repository=os.environ['GITHUB_REPOSITORY'], run_id='39969790346', run_attempt='1',
123+
workflow_head='c0cfdabc8040273d60c610269a93d1c687ed8517', artifact_id='12508482722')
124+
endpoint = 'repos/' + expected['repository'] + '/actions/artifacts/' + expected['artifact_id']
125+
metadata = json.loads(subprocess.check_output(['gh', 'api', endpoint], text=True))
126+
run = json.loads(subprocess.check_output(['gh', 'api', 'repos/' + expected['repository'] +
127+
'/actions/runs/' + expected['run_id'] + '/attempts/' + expected['run_attempt']], text=True))
128+
(output / 'selected-artifact-metadata.json').write_text(json.dumps(metadata, indent=2) + '\n')
129+
(output / 'selected-run-metadata.json').write_text(json.dumps(run, indent=2) + '\n')
130+
downloaded = output / 'selected-archive.zip'
131+
with downloaded.open('xb') as destination:
132+
subprocess.run(['gh', 'api', endpoint + '/zip'], stdout=destination, check=True)
133+
zip_digest = 'sha256:a08eb79186b13cf12ac8c639f424e13a7de1ee09df5547aab2b4200a951a5f05'
134+
verify_download(downloaded, metadata, run, expected, zip_digest, output / 'selected')
135+
selected = json.loads((output / 'selected/selected.json').read_text())
136+
archive = output / 'selected/libhttpserver-3.0.0.tar.gz'
137+
assert selected['commit'] == selected['source']['head'] == expected['workflow_head']
138+
assert selected['archive'] == archive.name and archive.stat().st_size == 3053890
139+
assert identity.sha256(archive) == selected['sha256'] == '43124552f20bd00fceddc714856d605ca9caf017a849a991c34f16f437fcab83'
140+
source = extract_archive(archive, output / 'source')
141+
originals = identity.source_files(source)
142+
assert originals and all(selected['files_sha256'].get(name) == digest for name, digest in originals.items())
143+
(output / 'original-source-hashes.json').write_text(json.dumps(originals, indent=2) + '\n')
144+
overlay = {}
145+
names = ['src/detail/' + name + '.cpp' for name in ('connection_engine', 'connection_engine_websocket',
146+
'io_external_adapter', 'io_operation', 'io_poll_backend', 'io_poll_sys', 'io_timer_sweep')]
147+
names += ['test/integ/external_http1_e2e_test.cpp', 'src/httpserver/detail/deadline_trace.hpp']
148+
for name in names:
149+
original = originals.get(name)
150+
if name.endswith('/deadline_trace.hpp'):
151+
assert original is None and name not in selected['source']['files_sha256']
152+
else:
153+
assert original == selected['source']['files_sha256'][name]
154+
overlay[name] = dict(original_sha256=original, diagnostic_sha256=identity.sha256(checkout / name))
155+
shutil.copyfile(checkout / name, source / name)
156+
modified = identity.source_files(source)
157+
assert set(modified) - set(originals) == {'src/httpserver/detail/deadline_trace.hpp'}
158+
assert {name for name in originals if modified[name] != originals[name]} == set(names[:-1])
159+
(output / 'diagnostic-source-hashes.json').write_text(json.dumps(modified, indent=2) + '\n')
160+
receipt['source_archive'] = dict(**expected, artifact_zip_sha256=zip_digest,
161+
archive_sha256=selected['sha256'], source_root=str(source), overlay=overlay,
162+
provenance='verified original archive plus explicit diagnostic overlay; not immutable release proof')
163+
command(['bash', source / 'configure', '--enable-v3-tls=no', '--disable-examples', '--prefix=' + str(prefix)], build, env)
164+
command(['make', '-j2'], build, env)
165+
command(['make', 'install'], build, env)
166+
runtime = runtime_environment({key: value for key, value in audit.clean_environment().items()
167+
if key not in ('GH_TOKEN', 'GITHUB_TOKEN', 'V3_RELEASE_IDENTITY')}, prefix, provider, [], toolchain)
168+
runtime.update(PKG_CONFIG_PATH='', PKG_CONFIG_LIBDIR=str(prefix / 'lib/pkgconfig'))
169+
os.environ.update({key: runtime[key] for key in ('LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH', 'V3_PACKAGE_TOOLCHAIN')})
170+
cflags = shlex.split(command(['pkg-config', '--cflags', 'libhttpserver'], integration, runtime))
171+
libraries = shlex.split(command(['pkg-config', '--libs', 'libhttpserver'], integration, runtime))
172+
libraries += ['-pthread', '-Wl,-rpath,' + str(prefix / 'lib'), '-Wl,-rpath,' + str(provider / 'lib')]
173+
flags = cflags + ['-DHTTPSERVER_COMPILATION', '-DHTTPSERVER_DIAG_HEADER_DEADLINE',
174+
'-I' + str(source / 'src'), '-I' + str(source / 'test')]
175+
definition = matrix.load()['suites']['external_http1_e2e']
176+
units = [source / 'test' / name for name in definition['sources']]
177+
executable = integration / 'external_http1_e2e'
178+
receipt['compile'] = compile_probe(compiler, units, flags, libraries, executable, command, runtime, source, build, prefix, True)
179+
graph = audit.dependency_graph([str(executable)])
180+
matrix.check_linkage(graph, 'shared', prefix)
181+
receipt['dependencies'] = graph
182+
result = command([str(executable)], source / 'test', runtime, timeout=180, allow_failure=True)
183+
print(result, end='', flush=True)
184+
receipt['exit'] = command.records[-1]['exit']
185+
receipt['suite'] = matrix.suite_result(result, receipt['exit'])
186+
receipt['status'] = 'diagnostic suite passed; full matrix not run'
187+
except Exception as error:
188+
receipt.update(status='diagnostic failed', error=str(error))
189+
raise
190+
finally:
191+
(output / 'diagnostic.json').write_text(json.dumps(receipt, indent=2) + '\n')
192+
PY_DIAGNOSTIC
76193
- name: Make privileged proof readable by artifact upload
77194
if: always()
78195
run: |
@@ -89,6 +206,7 @@ jobs:
89206
retention-days: 14
90207
if-no-files-found: error
91208
mingw64:
209+
if: github.event_name != 'workflow_dispatch'
92210
runs-on: windows-2022
93211
timeout-minutes: 90
94212
defaults:
@@ -148,6 +266,7 @@ jobs:
148266
retention-days: 14
149267
if-no-files-found: error
150268
freebsd:
269+
if: github.event_name != 'workflow_dispatch'
151270
runs-on: ubuntu-24.04
152271
timeout-minutes: 180
153272
steps:
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# FEATURE-ci-darwin-header-deadline: Diagnose native external header deadline
2+
3+
Status: In Progress — diagnostic checkpoint only; awaiting native Darwin evidence.
4+
5+
Base: `v3` at `c0cfdabc8040273d60c610269a93d1c687ed8517`.
6+
7+
Plan: `.groundwork-plans/FEATURE-ci-darwin-header-deadline-plan.md` in the primary checkout.
8+
Journal: `.debug/ci-darwin-native-69af4dxe/journal.md` in the primary checkout.
9+
Prior closed diagnostic: `.debug/ci-darwin-header-deadline-ybz8bi7l/journal.md`.
10+
11+
## Goal and evidence
12+
13+
Determine why the installed shared TLS-off Darwin `external_http1_e2e`
14+
`published_header_deadline_drives_empty_dispatch` case fails to observe peer close,
15+
then repair only the proven cause. User authorized focused native diagnostics on
16+
2026-10-23. Preserve the 300 ms header deadline, 10 s host failure deadline and all
17+
existing assertions.
18+
19+
The current base reproduces the failure in archive run `39969790346`, Darwin job
20+
`120469466623`, artifact `12509855171`: four tests, 52 checks, two failures,
21+
10,006.1 ms for the deadline case. Assertions 217 (`receive_close`) and 221
22+
(`!failed`) fail; assertion 219 (`empty_dispatches > 0`) passes. Other three cases
23+
and the earlier native HTTP1 suite pass. The underlying cause remains unknown.
24+
25+
New native comparison before publishing the checkpoint: current c0 package job
26+
`120468733495` passes the entire TLS-off lane, including this external deadline
27+
case, then fails later in TLS-on public TLS with Python
28+
`SSLCertVerificationError` (CA certificate missing key usage). Archive job
29+
`120469466623` fails the header deadline at the same source revision, selected
30+
LLVM toolchain and `-O0 -g`. Checkout versus extracted archive/execution context
31+
is now an observed distinction; it is not an established cause. The CA issue is
32+
queued separately and is outside this task.
33+
34+
## Action items
35+
36+
- [x] Preserve and read prior/current evidence without repeating the closed Linux probe.
37+
- [x] Create isolated task branch/worktree from the exact base.
38+
- [x] Add compile-time optional, bounded traces to actual watchdog, timer,
39+
dispatch, registration retirement and POSIX native close paths.
40+
- [x] Prepare a branch-only manual Darwin diagnostic in the existing
41+
`v3-package-validation.yml`; preserve push/PR matrix commands and skip other
42+
jobs explicitly on manual dispatch.
43+
- [x] Check enabled/disabled trace compilation, bounded output, errno preservation,
44+
ordinary-build argument elision, workflow syntax and changed-file lint.
45+
- [ ] Execute the native diagnostic and retain actual installed-library provenance.
46+
- [ ] Establish cause from native evidence, then write and observe a failing
47+
regression before applying a narrow repair.
48+
- [ ] Remove all temporary trace code/header and workflow edits before final merge.
49+
- [ ] Run task-relevant final checks and Groundwork validation/finalization.
50+
51+
## Checkpoint scope and checks
52+
53+
`HTTPSERVER_DIAG_HEADER_DEADLINE` enables temporary stderr records, capped at 64
54+
per call site. Correlation uses connection IDs, operation pointers/sequences,
55+
socket descriptors and registration generations. Time points are converted with
56+
`duration_cast<nanoseconds>`; header durations are explicit milliseconds. No
57+
request bytes, credentials or peer content are logged. Ordinary builds erase
58+
trace argument evaluation.
59+
60+
Manual dispatch runs macOS only, preserving its selected compiler/runtime,
61+
provider setup and artifact upload. It builds and installs TLS-off libraries at
62+
`-O0 -g`, then uses existing `Commands`, `runtime_environment`, `compile_probe`,
63+
dependency inspection, shared-linkage checks and suite receipt parsing to run
64+
the existing four-case external HTTP1 integration suite against the installed
65+
shared library built from the actual failing c0 selected archive. The macOS job
66+
uses only `contents: read` and `actions: read` with the existing GitHub token to
67+
download artifact `12508482722` from run `39969790346` attempt 1. Existing
68+
`verify_download` validates artifact/run/repository/source binding and ZIP digest
69+
`a08eb79186b13cf12ac8c639f424e13a7de1ee09df5547aab2b4200a951a5f05`.
70+
The archive is exactly 3,053,890 bytes with SHA-256
71+
`43124552f20bd00fceddc714856d605ca9caf017a849a991c34f16f437fcab83`.
72+
Existing strict `extract_archive` extracts it without a checkout `.git` directory.
73+
All original packaged file hashes must match the selected descriptor. Before
74+
copying the seven traced private implementations and existing integration test,
75+
each original hash must also match the c0 source snapshot. The private trace
76+
header is the sole added file; every other source remains byte-identical to the
77+
archive. Original/modified source manifests and per-file overlay hashes are
78+
retained. The original archive is preserved separately; the overlaid source is
79+
explicitly a diagnostic modified snapshot, not immutable release proof, and does
80+
not set `V3_RELEASE_IDENTITY`. Build and test source paths/cwd use this extracted
81+
tree. No build command receives the download token.
82+
83+
Compile/dependency hashes, commands, runtime selection and the failing suite log
84+
are retained in `package-evidence/header-deadline-diagnostic`. This partial run is
85+
explicitly diagnostic evidence and does not satisfy the package matrix gate.
86+
87+
Local checks passed: eight affected translation units with traces disabled and
88+
enabled (`g++ -fsyntax-only`, format errors enabled), trace contract checks,
89+
workflow YAML/Python parsing and shell syntax, and cpplint on all changed C++
90+
files. The first syntax invocation lacked generated `config.h`; bootstrap plus
91+
an out-of-tree TLS-off configure corrected that execution setup, and both
92+
configured invocations passed. No full baseline or old Linux publication/wait
93+
probe was run. The follow-up archive-wiring preflight uses the actual downloaded
94+
selected artifact locally: exact ZIP/tar and original hashes pass, the literal
95+
workflow extraction/overlay block passes, all eight original overlay files match
96+
the exact base, and the private trace header is present. All eight affected
97+
translation units also pass diagnostic-enabled syntax/format checks using only
98+
the extracted source/include paths. Workflow syntax passes again; only workflow provenance and this task record change in the follow-up.
99+
Darwin execution and the original required gate remain incomplete.
100+
101+
The caller reviews/publishes only this task branch and dispatches the existing
102+
workflow, then resumes this same worktree with actual native evidence. No root
103+
cause, behavior repair, validation completion or merge is claimed here. Tracing
104+
can perturb scheduling, so a passing diagnostic alone would not prove a fix.
105+
106+
## Scope boundaries
107+
108+
The older Linux parity uncertainty remains closed. This task does not authorize
109+
parity repairs, broad platform reruns, timeout changes, assertion relaxation,
110+
dependency/policy changes or a new shared verification framework. Preserve the
111+
unrelated bauth worktree. Nonlocal final platform checks remain assigned to CI
112+
and the v3 PR under the repository working agreement.

‎src/detail/connection_engine.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
#include <httpserver/detail/diagnostics_access.hpp>
4242
#include <httpserver/http/outcome.hpp>
4343

44+
#include <httpserver/detail/deadline_trace.hpp>
45+
4446
namespace httpserver {
4547

4648
namespace detail {
@@ -715,6 +717,9 @@ bool connection_engine::watchdog_due(
715717
std::chrono::steady_clock::time_point deadline) {
716718
std::lock_guard<std::mutex> lock(mu_);
717719
const watchdog_plan plan = plan_watchdog_locked();
720+
V3_DEADLINE_TRACE("due conn=%llu planned_ns=%lld current_ns=%lld now_ns=%lld exit=%d defer=%d equal=%d\n",
721+
static_cast<deadline_trace_id>(id_), deadline_trace_ns(deadline), deadline_trace_ns(plan.deadline),
722+
deadline_trace_ns(std::chrono::steady_clock::now()), plan.exit, plan.defer, plan.deadline == deadline);
718723
return !plan.exit && !plan.defer && plan.deadline == deadline
719724
&& std::chrono::steady_clock::now() >= deadline;
720725
}
@@ -740,6 +745,8 @@ void connection_engine::note_transport_activity(bool written) {
740745
}
741746

742747
void connection_engine::enforce_timeout(std::chrono::steady_clock::time_point deadline) noexcept {
748+
V3_DEADLINE_TRACE("enforce conn=%llu deadline_ns=%lld now_ns=%lld\n",
749+
static_cast<deadline_trace_id>(id_), deadline_trace_ns(deadline), deadline_trace_ns(std::chrono::steady_clock::now()));
743750
bool global_expiry = false, close_expiry = false;
744751
{
745752
std::lock_guard<std::mutex> lock(mu_);
@@ -776,13 +783,19 @@ task<void> connection_engine::watchdog_loop(
776783
std::lock_guard<std::mutex> lock(self->mu_);
777784
plan = self->plan_watchdog_locked();
778785
}
786+
V3_DEADLINE_TRACE("plan conn=%llu deadline_ns=%lld now_ns=%lld exit=%d defer=%d\n",
787+
static_cast<deadline_trace_id>(self->id_), deadline_trace_ns(plan.deadline),
788+
deadline_trace_ns(std::chrono::steady_clock::now()), plan.exit, plan.defer);
779789
if (plan.exit) co_return;
780790
const auto deadline = plan.defer
781791
? std::chrono::steady_clock::now() + k_watchdog_tick
782792
: plan.deadline;
783793
timer_operation op(self->owner_, self->id_, deadline);
784794
if (!self->arm_watchdog(op, plan)) continue;
785795
const io_result r = co_await std::move(op);
796+
V3_DEADLINE_TRACE("watchdog_resume conn=%llu seq=%llu result=%d now_ns=%lld\n",
797+
static_cast<deadline_trace_id>(self->id_), static_cast<deadline_trace_id>(op.sequence()),
798+
static_cast<int>(r.code), deadline_trace_ns(std::chrono::steady_clock::now()));
786799
{
787800
std::lock_guard<std::mutex> lock(self->mu_);
788801
self->pending_timer_.reset();

0 commit comments

Comments
 (0)