From c77412e02904d3a2b7727c9fd04be69c4c672140 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 7 May 2026 10:53:18 +0100 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20bump=20BDD=20wait=5Ffor=5Fmessages?= =?UTF-8?q?=20deadline=205s=20=E2=86=92=2010s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 5s budget for the buffered example to complete launch + cert load + TLS handshake + Send + oracle write was flake-prone on the Windows OTel runner under load (mTLS scenario in PR #283 timed out once and passed on retry). 10s gives comfortable margin without changing test intent. Co-Authored-By: Claude Opus 4.7 (1M context) --- Bdd/features/steps/syslog_steps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bdd/features/steps/syslog_steps.py b/Bdd/features/steps/syslog_steps.py index d7c563a9..83d833b3 100644 --- a/Bdd/features/steps/syslog_steps.py +++ b/Bdd/features/steps/syslog_steps.py @@ -355,13 +355,13 @@ def wait_for_messages(context, expected_messages): received_log = context.received_log oracle_format = context.oracle_format expected_total = context.lines_before + expected_messages - deadline = time.monotonic() + 5 + deadline = time.monotonic() + 10 while oracle_record_count(received_log, oracle_format) < expected_total: if time.monotonic() > deadline: actual = oracle_record_count(received_log, oracle_format) - context.lines_before raise AssertionError( f"oracle received {actual} of {expected_messages} " - f"messages within 5 seconds" + f"messages within 10 seconds" ) time.sleep(0.1) From 70af4b879349092f3d9035eaca90a76e54ecd709 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 7 May 2026 10:53:34 +0100 Subject: [PATCH 2/4] chore: make BDD scenarios runnable on Windows-hosted devcontainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two chronic local-only issues blocked TCP-outage scenarios from running under the Windows-hosted Docker Desktop devcontainer; CI (which runs in its own Linux VM with matching UID/GID) was unaffected. Both fixes are no-ops for CI. 1. shutil.copy → shutil.copyfile in syslog_ng_swap_config (steps) and after_scenario (environment). The destination syslog-ng.conf is bind-mounted from the host and not owned by the container's `developer` user; copy()'s chmod step then fails with EPERM and leaves the config truncated. copyfile copies bytes only, so the file's existing mode (set by the host) is left intact. 2. syslog-ng compose entrypoint now keeps a watchdog re-chmodding /var/lib/syslog-ng/syslog-ng.ctl to 0777. syslog-ng RELOAD recreates the socket with default 0755 perms; the behave container connects as `developer` and needs world-writable to call connect(2). Single 100ms-tick background loop, exits with the syslog-ng pid. Closes the open follow-up tracked in MEMORY:project_syslog_ng_conf_corruption. Co-Authored-By: Claude Opus 4.7 (1M context) --- .devcontainer/docker-compose.yml | 10 ++++++++-- Bdd/features/environment.py | 5 +++-- Bdd/features/steps/syslog_steps.py | 11 +++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index d28738f0..cd0ab773 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -43,8 +43,14 @@ services: [ "$$SECONDS" -ge "$$deadline" ] && { echo "Timed out waiting for syslog-ng control socket"; kill "$$pid" 2>/dev/null; exit 1; } sleep 0.1 done - chmod 0777 /var/lib/syslog-ng/syslog-ng.ctl - wait + # Watchdog: syslog-ng RELOAD recreates the ctl socket with default 755 perms; + # the BDD behave container connects as a non-root user, so keep the socket + # world-writable across reloads. + ( while kill -0 "$$pid" 2>/dev/null; do + chmod 0777 /var/lib/syslog-ng/syslog-ng.ctl 2>/dev/null + sleep 0.1 + done ) & + wait $$pid behave: image: ghcr.io/davidcozens/behave:sha-3faff14 diff --git a/Bdd/features/environment.py b/Bdd/features/environment.py index bc0c33ac..d20c6df5 100644 --- a/Bdd/features/environment.py +++ b/Bdd/features/environment.py @@ -133,9 +133,10 @@ def after_scenario(context, scenario): process.wait() del context.interactive_process - # Restore syslog-ng config if it was changed during the scenario + # Restore syslog-ng config if it was changed during the scenario. + # copyfile (data only, no chmod) — see syslog_ng_swap_config rationale. if hasattr(context, "syslog_ng_config_changed") and context.syslog_ng_config_changed: - shutil.copy(SYSLOG_NG_FULL_CONF, SYSLOG_NG_CONF) + shutil.copyfile(SYSLOG_NG_FULL_CONF, SYSLOG_NG_CONF) try: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(SYSLOG_NG_CTL) diff --git a/Bdd/features/steps/syslog_steps.py b/Bdd/features/steps/syslog_steps.py index 83d833b3..3558930c 100644 --- a/Bdd/features/steps/syslog_steps.py +++ b/Bdd/features/steps/syslog_steps.py @@ -468,8 +468,15 @@ def syslog_ng_reload(): def syslog_ng_swap_config(config_path): - """Replace the active syslog-ng config and reload.""" - shutil.copy(config_path, SYSLOG_NG_CONF) + """Replace the active syslog-ng config and reload. + + Uses copyfile (data only, no chmod) rather than copy. The destination + is bind-mounted from the host on developer machines; chmod fails with + 'Operation not permitted' when the host owner is not the container's + `developer` user. The file already exists with the correct mode + so copying just the contents is sufficient. + """ + shutil.copyfile(config_path, SYSLOG_NG_CONF) syslog_ng_reload() From d019cc136873ab4ad1558bb454e5d0bffd87244e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 7 May 2026 10:53:45 +0100 Subject: [PATCH 3/4] feat: S13.21 re-enable @windows_wip store_capacity scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S12.14's eager-drain refactor decoupled buffer drain from sender state on both runners; the four store_capacity discard scenarios that were tagged @windows_wip pending that fix now run reliably end-to-end. Drop the four @windows_wip tags and the long explanatory note; the existing 10/8-message form gives sufficient margin for the discard policy to engage with both runners. Locally verified all 4 scenarios pass on Linux (and the full BDD suite — 21 features, 46 scenarios, 242 steps — remains green). Closes #278 Co-Authored-By: Claude Opus 4.7 (1M context) --- Bdd/features/store_capacity.feature | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Bdd/features/store_capacity.feature b/Bdd/features/store_capacity.feature index f3643d85..dde9c038 100644 --- a/Bdd/features/store_capacity.feature +++ b/Bdd/features/store_capacity.feature @@ -4,16 +4,6 @@ Feature: Store capacity limit and discard policy When the store is full, the discard policy determines whether the oldest or newest messages are dropped. - Note: scenarios are tagged @windows_wip pending S12.14 (architecture - refactor that decouples buffer drain from sender state). The tight-loop - `client sends N messages` step fills the in-memory buffer faster than - the service thread can drain it on Windows, so records never reach the - store and the discard policy can't engage. Linux compose runner happens - to schedule producer and service so the buffer stays small enough that - the assertions pass; that is timing-dependent and will be re-tuned in - the PR #275 finale once S12.14 is on main. - - @windows_wip Scenario: Discard-oldest drops oldest messages when store overflows Given the syslog oracle is running And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy oldest @@ -29,7 +19,6 @@ Feature: Store capacity limit and discard policy And the syslog oracle did not receive sequenceId 2 And the outage messages have contiguous sequenceIds - @windows_wip Scenario: Discard-newest preserves oldest messages when store overflows Given the syslog oracle is running And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy newest @@ -45,7 +34,6 @@ Feature: Store capacity limit and discard policy And the syslog oracle did not receive sequenceId 11 And the outage messages have contiguous sequenceIds - @windows_wip Scenario: Halt stops the application when store overflows Given the syslog oracle is running And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy halt @@ -57,7 +45,6 @@ Feature: Store capacity limit and discard policy And the client sends 8 messages And the client attempts to send it exits with code 2 - @windows_wip Scenario: Halt prevents further service after store overflows Given the syslog oracle is running And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy halt From 129a7ffd76a7308338ebdf1c112065e27d0432c9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 7 May 2026 11:03:27 +0100 Subject: [PATCH 4/4] fix: address CodeRabbit review on PR #284 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - environment.py after_scenario: move shutil.copyfile inside the existing try/except so a copyfile failure is logged via the warning path rather than bypassing the rest of teardown. - syslog_steps.py step_oracle_stops_tcp: set context.syslog_ng_config_changed = True *before* syslog_ng_swap_config() rather than after. Any failure inside swap (or the subsequent wait / probe teardown / sleep) still triggers the after_scenario restore path — otherwise a half-applied swap would leave the on-disk config in UDP-only mode and contaminate the next scenario. Co-Authored-By: Claude Opus 4.7 (1M context) --- Bdd/features/environment.py | 2 +- Bdd/features/steps/syslog_steps.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Bdd/features/environment.py b/Bdd/features/environment.py index d20c6df5..17cd2242 100644 --- a/Bdd/features/environment.py +++ b/Bdd/features/environment.py @@ -136,8 +136,8 @@ def after_scenario(context, scenario): # Restore syslog-ng config if it was changed during the scenario. # copyfile (data only, no chmod) — see syslog_ng_swap_config rationale. if hasattr(context, "syslog_ng_config_changed") and context.syslog_ng_config_changed: - shutil.copyfile(SYSLOG_NG_FULL_CONF, SYSLOG_NG_CONF) try: + shutil.copyfile(SYSLOG_NG_FULL_CONF, SYSLOG_NG_CONF) with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(SYSLOG_NG_CTL) sock.sendall(b"RELOAD\n") diff --git a/Bdd/features/steps/syslog_steps.py b/Bdd/features/steps/syslog_steps.py index 3558930c..e8040b0c 100644 --- a/Bdd/features/steps/syslog_steps.py +++ b/Bdd/features/steps/syslog_steps.py @@ -720,12 +720,17 @@ def step_oracle_stops_tcp(context): probe.settimeout(1) probe.connect(("syslog-ng", 5514)) + # Set the restore flag *before* the swap so any failure inside + # syslog_ng_swap_config (or the subsequent wait/probe-teardown) + # still triggers after_scenario's config restore — otherwise a + # half-applied swap would leave the on-disk config in UDP-only + # mode and contaminate the next scenario. + context.syslog_ng_config_changed = True syslog_ng_swap_config(SYSLOG_NG_UDP_ONLY_CONF) wait_for_tcp_port_closed() wait_for_connection_teardown(probe) # Allow time for the sender's existing connection to receive RST time.sleep(0.5) - context.syslog_ng_config_changed = True else: # Windows OTel: kill the otelcol-contrib process. The TCP listener # disappears as the process exits — the resume step will start a