From 2c8b89a8ec1e605f67234e93c525e2c061d7cc74 Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 16:35:03 +0900 Subject: [PATCH 1/8] fix(windows_event_log source): prevent permanent freeze from signal-event lost wakeup The Windows Event Log service signals pull-mode subscriptions via SetEvent on a manual-reset event each time a new matching event is recorded. Because SetEvent on an already-signaled handle is a no-op, resetting the handle AFTER draining EvtNext creates a race: a signal arriving between the last EvtNext and ResetEvent is silently lost and the source freezes until the next event arrives. Reset before the drain so signals raised mid-drain are preserved; re-SetEvent on early break (budget exhausted / bookmark failure) so channels are not starved. Fixes vectordotdev/vector#25194 --- ...25194_windows_event_log_lost_wakeup.fix.md | 3 + src/sources/windows_event_log/subscription.rs | 119 ++++++++++++++++-- 2 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 changelog.d/25194_windows_event_log_lost_wakeup.fix.md diff --git a/changelog.d/25194_windows_event_log_lost_wakeup.fix.md b/changelog.d/25194_windows_event_log_lost_wakeup.fix.md new file mode 100644 index 0000000000000..acf560f54635d --- /dev/null +++ b/changelog.d/25194_windows_event_log_lost_wakeup.fix.md @@ -0,0 +1,3 @@ +The `windows_event_log` source no longer freezes after periods of inactivity. A race between the signal-handle reset and incoming event notifications could cause new events to be silently dropped from the wakeup path; the signal is now reset before draining so notifications arriving mid-drain are preserved. + +authors: tot19 diff --git a/src/sources/windows_event_log/subscription.rs b/src/sources/windows_event_log/subscription.rs index dc92713f691d9..8ace32b9b1b0c 100644 --- a/src/sources/windows_event_log/subscription.rs +++ b/src/sources/windows_event_log/subscription.rs @@ -18,9 +18,9 @@ use windows::Win32::System::EventLog::{ EvtSubscribeStartAfterBookmark, EvtSubscribeStartAtOldestRecord, EvtSubscribeStrict, EvtSubscribeToFutureEvents, }; -#[cfg(test)] -use windows::Win32::System::Threading::SetEvent; -use windows::Win32::System::Threading::{CreateEventW, ResetEvent, WaitForMultipleObjects}; +use windows::Win32::System::Threading::{ + CreateEventW, ResetEvent, SetEvent, WaitForMultipleObjects, +}; use windows::core::HSTRING; use super::{ @@ -479,9 +479,25 @@ impl EventLogSubscription { let mut bookmark_failed = false; let mut channel_count = 0usize; - // Drain loop: keep calling EvtNext until ERROR_NO_MORE_ITEMS or channel budget. - // Only reset the signal once the channel is fully drained; if we hit the - // budget limit the signal stays set so WaitForMultipleObjects returns immediately. + // Reset the signal BEFORE draining to avoid a lost-wakeup race + // (see vectordotdev/vector#25194). The Windows Event Log service + // signals this manual-reset event via SetEvent each time a new + // matching event is recorded; SetEvent on an already-signaled + // event is a no-op, so if we reset AFTER draining, any signal + // that arrives between our last EvtNext and ResetEvent is lost + // — the subscription then hangs until the next event arrives. + // Resetting first means any signal raised during the drain is + // preserved, causing the next WaitForMultipleObjects to return + // immediately. + // + // If we exit the drain loop early (channel budget exhausted or + // bookmark update failed mid-batch), we re-SetEvent at the end + // of this iteration so the next pull_events call revisits this + // channel without waiting for a fresh OS signal. + unsafe { + let _ = ResetEvent(channel_sub.signal_event); + } + 'drain: loop { if channel_count >= channel_limit { break; @@ -697,15 +713,20 @@ impl EventLogSubscription { } if channel_drained && !bookmark_failed { - unsafe { - let _ = ResetEvent(channel_sub.signal_event); - } - // Update channel record count gauge for lag detection. super::render::update_channel_records( &channel_sub.channel, &channel_sub.channel_records_gauge, ); + } else { + // Drain exited early (budget exhausted or bookmark_failed + // mid-batch). Re-arm the signal so the next pull_events + // revisits this channel immediately without waiting for a + // fresh OS notification. Pairs with the pre-drain ResetEvent + // above. + unsafe { + let _ = SetEvent(channel_sub.signal_event); + } } } @@ -1272,4 +1293,82 @@ mod tests { // that the subscription is functional. let _events = subscription.pull_events(100).unwrap_or_default(); } + + /// Regression test for vectordotdev/vector#25194. + /// + /// The Windows Event Log service signals the pull-mode wait handle via + /// `SetEvent` each time a new matching event is recorded. Because the + /// handle is manual-reset, `SetEvent` on an already-signaled handle is + /// a no-op. If `pull_events` resets the signal *after* draining events + /// via `EvtNext`, any signal that fires between the last `EvtNext` and + /// the `ResetEvent` call is silently lost — the subscription then + /// permanently hangs until a subsequent event arrives. + /// + /// The fix is to reset the signal *before* the drain loop, so signals + /// raised during the drain are preserved and the next wait returns + /// immediately. This test pins that invariant by simulating the race + /// with a `std::sync::Barrier` for deterministic ordering — it fails + /// with `WAIT_TIMEOUT` under the old "reset after drain" order. + #[test] + fn test_signal_not_lost_when_set_during_drain() { + use std::sync::{Arc as StdArc, Barrier}; + use windows::Win32::System::Threading::WaitForSingleObject; + + // Manual-reset event, initially non-signaled (we SetEvent first to + // mirror the post-initial-drain state of a real subscription). + let signal = unsafe { CreateEventW(None, true, false, None).unwrap() }; + let signal_raw = signal.0 as isize; + + // Prime: simulate the OS having signaled (events arrived). + unsafe { + let _ = SetEvent(signal); + } + + // Barrier pair: the helper thread waits until the main thread has + // simulated entering the drain (by resetting), then calls SetEvent + // to simulate a new event notification arriving during the drain. + // A second barrier pair lets the main thread wait until the helper + // has fired SetEvent before we check the signal state. + let barrier_enter = StdArc::new(Barrier::new(2)); + let barrier_exit = StdArc::new(Barrier::new(2)); + let barrier_enter_helper = StdArc::clone(&barrier_enter); + let barrier_exit_helper = StdArc::clone(&barrier_exit); + + let helper = std::thread::spawn(move || { + barrier_enter_helper.wait(); + unsafe { + let h = HANDLE(signal_raw as *mut std::ffi::c_void); + let _ = SetEvent(h); + } + barrier_exit_helper.wait(); + }); + + // Main thread: simulate the pull_events sequence with the fix + // (ResetEvent is the FIRST signal-handling step). + unsafe { + let _ = ResetEvent(signal); + } + // Release helper — it now fires SetEvent, simulating the OS + // signaling a new event "during" the drain window. + barrier_enter.wait(); + // Wait until helper has definitely fired SetEvent. + barrier_exit.wait(); + helper.join().unwrap(); + + // With the fix, the SetEvent above is preserved in the signal and + // this wait returns WAIT_OBJECT_0 immediately. With the old + // post-drain ResetEvent order, the SetEvent would have been + // clobbered by the reset and this wait would return WAIT_TIMEOUT. + let wait_result = unsafe { WaitForSingleObject(signal, 500) }; + assert_eq!( + wait_result, WAIT_OBJECT_0, + "signal set during the drain window was lost — this is the \ + lost-wakeup race from vectordotdev/vector#25194. \ + pull_events must call ResetEvent BEFORE draining, not after." + ); + + unsafe { + let _ = CloseHandle(signal); + } + } } From ab329bcc237811ab9335d0e3a2f159dd5c2d975d Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 16:45:37 +0900 Subject: [PATCH 2/8] fix(sources): re-arm windows_event_log signal on EvtNext error and harden regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on #25195: - Re-SetEvent on the EvtNext error-return path. Without this, a transient EvtNext failure leaves the pre-drain-reset signal cleared with events possibly still un-drained — the same class of lost-wakeup the PR fixes for the success path. - Replace the primitive-level Barrier test with an integration-style test that drives real pull_events against a real EvtSubscribe handle. A new cfg(test) DRAIN_STEP_HOOK fires SetEvent on the subscription's signal handle inside the drain loop, simulating the OS signaling a fresh event mid-drain. After pull_events returns, wait_for_events_blocking must return EventsAvailable; under the old post-drain ResetEvent order it would time out. This test would now actually catch a reversion. --- src/sources/windows_event_log/subscription.rs | 162 ++++++++++++------ 1 file changed, 106 insertions(+), 56 deletions(-) diff --git a/src/sources/windows_event_log/subscription.rs b/src/sources/windows_event_log/subscription.rs index 8ace32b9b1b0c..f53347a53fffa 100644 --- a/src/sources/windows_event_log/subscription.rs +++ b/src/sources/windows_event_log/subscription.rs @@ -30,6 +30,16 @@ use super::{ use crate::internal_events::WindowsEventLogBookmarkError; +/// Test-only hook called inside the `pull_events` drain loop after each +/// `EvtNext` invocation. Used by the lost-wakeup regression test +/// (see `test_pull_events_preserves_setevent_during_drain`) to race a +/// `SetEvent` against the drain without relying on thread-timing. +/// No-op and zero-cost in non-test builds. +#[cfg(test)] +static DRAIN_STEP_HOOK: std::sync::Mutex< + Option>, +> = std::sync::Mutex::new(None); + /// Maximum number of entries in the EvtFormatMessage result cache. pub const FORMAT_CACHE_CAPACITY: usize = 10_000; /// Maximum number of cached publisher metadata handles. @@ -517,6 +527,17 @@ impl EventLogSubscription { ) }; + // Test-only hook: lets the lost-wakeup regression test race + // a SetEvent against the drain without thread-timing. No-op + // and zero-cost in non-test builds. + #[cfg(test)] + { + let hook = DRAIN_STEP_HOOK.lock().unwrap().clone(); + if let Some(h) = hook { + h(channel_sub.signal_event); + } + } + if let Err(err) = result { let code = (err.code().0 as u32) & 0xFFFF; if code == ERROR_NO_MORE_ITEMS { @@ -558,6 +579,17 @@ impl EventLogSubscription { } } } + // Re-arm the signal before returning. We reset it pre-drain + // but are bailing out without confirming the drain completed, + // so if events were left un-drained the next pull_events must + // still revisit this channel without waiting for a fresh OS + // signal. This mirrors the `else` branch below that handles + // budget-exhaustion and bookmark-failure early breaks, and + // avoids the same lost-wakeup symptom (vectordotdev/vector#25194) + // on transient EvtNext failures. + unsafe { + let _ = SetEvent(channel_sub.signal_event); + } return Err(WindowsEventLogError::PullEventsError { channel: channel_sub.channel.clone(), source: err, @@ -1306,69 +1338,87 @@ mod tests { /// /// The fix is to reset the signal *before* the drain loop, so signals /// raised during the drain are preserved and the next wait returns - /// immediately. This test pins that invariant by simulating the race - /// with a `std::sync::Barrier` for deterministic ordering — it fails - /// with `WAIT_TIMEOUT` under the old "reset after drain" order. - #[test] - fn test_signal_not_lost_when_set_during_drain() { - use std::sync::{Arc as StdArc, Barrier}; - use windows::Win32::System::Threading::WaitForSingleObject; + /// immediately. + /// + /// This test pins that invariant by driving the real `pull_events` + /// against a real `EvtSubscribe` handle. It installs a + /// `DRAIN_STEP_HOOK` that runs inside the drain loop after each + /// `EvtNext` and fires `SetEvent` on the subscription's signal + /// handle — simulating the OS signaling a new event arrival during + /// the drain window. After `pull_events` returns, the signal must + /// still be set (the subsequent `wait_for_events_blocking` must + /// return `EventsAvailable`, not `Timeout`). Under the old + /// post-drain `ResetEvent` order, the hook's `SetEvent` would be + /// clobbered by the reset and the wait would time out — which is + /// exactly what #25194 reports. + #[tokio::test] + async fn test_pull_events_preserves_setevent_during_drain() { + use std::sync::Arc as StdArc; - // Manual-reset event, initially non-signaled (we SetEvent first to - // mirror the post-initial-drain state of a real subscription). - let signal = unsafe { CreateEventW(None, true, false, None).unwrap() }; - let signal_raw = signal.0 as isize; + let mut config = WindowsEventLogConfig::default(); + config.channels = vec!["Application".to_string()]; + config.read_existing_events = true; + config.event_timeout_ms = 1000; - // Prime: simulate the OS having signaled (events arrived). - unsafe { - let _ = SetEvent(signal); - } + let (checkpointer, _temp_dir) = create_test_checkpointer().await; - // Barrier pair: the helper thread waits until the main thread has - // simulated entering the drain (by resetting), then calls SetEvent - // to simulate a new event notification arriving during the drain. - // A second barrier pair lets the main thread wait until the helper - // has fired SetEvent before we check the signal state. - let barrier_enter = StdArc::new(Barrier::new(2)); - let barrier_exit = StdArc::new(Barrier::new(2)); - let barrier_enter_helper = StdArc::clone(&barrier_enter); - let barrier_exit_helper = StdArc::clone(&barrier_exit); - - let helper = std::thread::spawn(move || { - barrier_enter_helper.wait(); - unsafe { - let h = HANDLE(signal_raw as *mut std::ffi::c_void); - let _ = SetEvent(h); - } - barrier_exit_helper.wait(); - }); + let mut subscription = EventLogSubscription::new(&config, checkpointer, false) + .await + .expect("Subscription creation should succeed"); - // Main thread: simulate the pull_events sequence with the fix - // (ResetEvent is the FIRST signal-handling step). - unsafe { - let _ = ResetEvent(signal); + // Install the drain-loop hook: every EvtNext call inside + // pull_events fires SetEvent on the subscription's signal + // handle. This simulates the OS signaling a fresh event + // mid-drain, which is exactly the race window #25194 exposes. + // The hook only needs to fire once to prove the invariant; we + // use an AtomicBool to keep it deterministic. + let fired = StdArc::new(std::sync::atomic::AtomicBool::new(false)); + { + let fired = StdArc::clone(&fired); + let hook: StdArc = + StdArc::new(move |signal: HANDLE| { + if !fired.swap(true, std::sync::atomic::Ordering::SeqCst) { + unsafe { + let _ = SetEvent(signal); + } + } + }); + *DRAIN_STEP_HOOK.lock().unwrap() = Some(hook); } - // Release helper — it now fires SetEvent, simulating the OS - // signaling a new event "during" the drain window. - barrier_enter.wait(); - // Wait until helper has definitely fired SetEvent. - barrier_exit.wait(); - helper.join().unwrap(); - - // With the fix, the SetEvent above is preserved in the signal and - // this wait returns WAIT_OBJECT_0 immediately. With the old - // post-drain ResetEvent order, the SetEvent would have been - // clobbered by the reset and this wait would return WAIT_TIMEOUT. - let wait_result = unsafe { WaitForSingleObject(signal, 500) }; - assert_eq!( - wait_result, WAIT_OBJECT_0, - "signal set during the drain window was lost — this is the \ - lost-wakeup race from vectordotdev/vector#25194. \ - pull_events must call ResetEvent BEFORE draining, not after." + + // Drive pull_events. Regardless of how many events are + // actually in the Application channel, the drain loop runs at + // least one EvtNext and the hook fires exactly once. + let _ = subscription.pull_events(100).unwrap_or_default(); + + // Clear the hook so other tests aren't affected. + *DRAIN_STEP_HOOK.lock().unwrap() = None; + + assert!( + fired.load(std::sync::atomic::Ordering::SeqCst), + "drain-loop hook never ran — pull_events must call EvtNext \ + at least once even on an empty channel" ); - unsafe { - let _ = CloseHandle(signal); + // With the fix, the SetEvent fired by the hook during the + // drain is preserved; the next wait returns EventsAvailable + // immediately. Under the old post-drain ResetEvent order, it + // would time out — that is the #25194 freeze. + let (_subscription, result) = tokio::task::spawn_blocking(move || { + let r = subscription.wait_for_events_blocking(500); + (subscription, r) + }) + .await + .unwrap(); + + match result { + WaitResult::EventsAvailable => {} + WaitResult::Timeout => panic!( + "signal set during the drain window was lost — this is the \ + lost-wakeup race from vectordotdev/vector#25194. \ + pull_events must call ResetEvent BEFORE draining, not after." + ), + WaitResult::Shutdown => panic!("unexpected shutdown"), } } } From 8fe90838bfc53b90fc88e1fbea8ca36f00088619 Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 16:49:25 +0900 Subject: [PATCH 3/8] chore(sources): harden windows_event_log lost-wakeup regression test Two small hardenings to make the test deterministically catch a reversion of the pull_events signal-reset ordering: - Use pull_events(usize::MAX) instead of pull_events(100) so the drain exits via ERROR_NO_MORE_ITEMS (channel_drained = true). On hosts with >100 Application events the old budget would exit via budget exhaustion, skipping the post-drain ResetEvent in the pre-fix code and letting the test false-pass against the buggy ordering. - Wrap hook cleanup in a drop-guard so a panic mid-test doesn't leak the global DRAIN_STEP_HOOK into other tests sharing the process. --- src/sources/windows_event_log/subscription.rs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/sources/windows_event_log/subscription.rs b/src/sources/windows_event_log/subscription.rs index f53347a53fffa..4467bc5884bc2 100644 --- a/src/sources/windows_event_log/subscription.rs +++ b/src/sources/windows_event_log/subscription.rs @@ -1386,13 +1386,23 @@ mod tests { *DRAIN_STEP_HOOK.lock().unwrap() = Some(hook); } - // Drive pull_events. Regardless of how many events are - // actually in the Application channel, the drain loop runs at - // least one EvtNext and the hook fires exactly once. - let _ = subscription.pull_events(100).unwrap_or_default(); - - // Clear the hook so other tests aren't affected. - *DRAIN_STEP_HOOK.lock().unwrap() = None; + // Drop-guard: clear the hook even if the test panics, so it + // doesn't contaminate other tests in the same process. + struct HookGuard; + impl Drop for HookGuard { + fn drop(&mut self) { + *DRAIN_STEP_HOOK.lock().unwrap() = None; + } + } + let _guard = HookGuard; + + // Drive pull_events with a very large budget so the drain + // exits via ERROR_NO_MORE_ITEMS (channel_drained = true), + // which is the path that ran the post-drain ResetEvent in the + // old buggy code. Exiting via budget exhaustion would skip + // that reset and cause this test to false-pass against the + // pre-fix code. + let _ = subscription.pull_events(usize::MAX).unwrap_or_default(); assert!( fired.load(std::sync::atomic::Ordering::SeqCst), From ae716caf8c28752e683feaea0db51319ded47e07 Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 17:03:05 +0900 Subject: [PATCH 4/8] fix(sources): key windows_event_log drain-loop test hook to its subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Codex review feedback on #25195. DRAIN_STEP_HOOK is a process-global and cargo runs tests in parallel, so a concurrent test's pull_events could trigger this test's one-shot hook first, flip `fired`, and SetEvent on the wrong signal handle — intermittently timing out test_pull_events_preserves_setevent_during_drain and perturbing the sibling pull_events tests. Capture the subscription's own signal handle up front and make the hook no-op when invoked for any other handle. Add a cfg(test)-only first_channel_signal_raw accessor for the test to read it. --- src/sources/windows_event_log/subscription.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/sources/windows_event_log/subscription.rs b/src/sources/windows_event_log/subscription.rs index 4467bc5884bc2..ce302100ecb8c 100644 --- a/src/sources/windows_event_log/subscription.rs +++ b/src/sources/windows_event_log/subscription.rs @@ -869,6 +869,15 @@ impl EventLogSubscription { self.shutdown_event.0 } + /// Test-only accessor for the first channel's signal event handle. Used + /// by the lost-wakeup regression test to scope its drain-loop hook to + /// exactly this subscription, so it does not fire on concurrent + /// `pull_events` calls from other tests in the same process. + #[cfg(test)] + pub(super) fn first_channel_signal_raw(&self) -> isize { + self.channels[0].signal_event.0 as isize + } + /// Returns a reference to the rate limiter, if configured. pub const fn rate_limiter( &self, @@ -1366,17 +1375,29 @@ mod tests { .await .expect("Subscription creation should succeed"); + // Capture THIS subscription's signal handle so the hook can scope + // itself to this test. DRAIN_STEP_HOOK is a process-global, and + // cargo runs tests in parallel by default; without handle-keying, + // a concurrent test's pull_events could trigger our one-shot + // hook first, flip `fired`, and SetEvent on the wrong handle. + let target_signal_raw = subscription.first_channel_signal_raw(); + // Install the drain-loop hook: every EvtNext call inside // pull_events fires SetEvent on the subscription's signal // handle. This simulates the OS signaling a fresh event // mid-drain, which is exactly the race window #25194 exposes. // The hook only needs to fire once to prove the invariant; we - // use an AtomicBool to keep it deterministic. + // use an AtomicBool to keep it deterministic. The hook is keyed + // to `target_signal_raw` so concurrent pull_events calls from + // other tests no-op here. let fired = StdArc::new(std::sync::atomic::AtomicBool::new(false)); { let fired = StdArc::clone(&fired); let hook: StdArc = StdArc::new(move |signal: HANDLE| { + if signal.0 as isize != target_signal_raw { + return; + } if !fired.swap(true, std::sync::atomic::Ordering::SeqCst) { unsafe { let _ = SetEvent(signal); From 0f017a5c42d089bb846015a92887a97ec8ace769 Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 17:07:11 +0900 Subject: [PATCH 5/8] style(sources): cargo fmt windows_event_log subscription.rs Fixes check-fmt CI failure on #25195. --- src/sources/windows_event_log/subscription.rs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/sources/windows_event_log/subscription.rs b/src/sources/windows_event_log/subscription.rs index ce302100ecb8c..8a83351ef146a 100644 --- a/src/sources/windows_event_log/subscription.rs +++ b/src/sources/windows_event_log/subscription.rs @@ -36,9 +36,8 @@ use crate::internal_events::WindowsEventLogBookmarkError; /// `SetEvent` against the drain without relying on thread-timing. /// No-op and zero-cost in non-test builds. #[cfg(test)] -static DRAIN_STEP_HOOK: std::sync::Mutex< - Option>, -> = std::sync::Mutex::new(None); +static DRAIN_STEP_HOOK: std::sync::Mutex>> = + std::sync::Mutex::new(None); /// Maximum number of entries in the EvtFormatMessage result cache. pub const FORMAT_CACHE_CAPACITY: usize = 10_000; @@ -1393,17 +1392,16 @@ mod tests { let fired = StdArc::new(std::sync::atomic::AtomicBool::new(false)); { let fired = StdArc::clone(&fired); - let hook: StdArc = - StdArc::new(move |signal: HANDLE| { - if signal.0 as isize != target_signal_raw { - return; - } - if !fired.swap(true, std::sync::atomic::Ordering::SeqCst) { - unsafe { - let _ = SetEvent(signal); - } + let hook: StdArc = StdArc::new(move |signal: HANDLE| { + if signal.0 as isize != target_signal_raw { + return; + } + if !fired.swap(true, std::sync::atomic::Ordering::SeqCst) { + unsafe { + let _ = SetEvent(signal); } - }); + } + }); *DRAIN_STEP_HOOK.lock().unwrap() = Some(hook); } From b32552bdd290afe74d4e5d38242dbb9d8c373cfc Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 16:53:55 +0900 Subject: [PATCH 6/8] ci: use windows-latest runner for Windows CI workflows Swap windows-2025-8core (vectordotdev private runner, unavailable on forks) for windows-latest so the Windows integration workflow can run against this PR's branch on the tot19/vector fork. Matches the runner choice used successfully on the windows-latest-runner branch while bringing up this source originally. --- .github/workflows/integration_windows.yml | 2 +- .github/workflows/unit_windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration_windows.yml b/.github/workflows/integration_windows.yml index 39289d786fd24..218b659691ff7 100644 --- a/.github/workflows/integration_windows.yml +++ b/.github/workflows/integration_windows.yml @@ -32,7 +32,7 @@ jobs: run-test-integration-windows: needs: changes if: needs.changes.outputs.windows == 'true' - runs-on: windows-2025-8core + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout branch diff --git a/.github/workflows/unit_windows.yml b/.github/workflows/unit_windows.yml index a4553b4a0b751..6290109925537 100644 --- a/.github/workflows/unit_windows.yml +++ b/.github/workflows/unit_windows.yml @@ -13,7 +13,7 @@ permissions: jobs: test-windows: - runs-on: windows-2025-8core + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout branch From e27da1af8b25e88625e88192622433bdc27b7fce Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 16:54:48 +0900 Subject: [PATCH 7/8] ci: serialize windows_event_log integration tests with --test-threads=1 Matches the serialization that was applied on the original windows-latest-runner branch. Even with per-test provider names isolating eventcreate + XPath filters, the Windows Event Log service has shared per-channel signal handles and bookmarks that can cause cross-test interference when nextest parallelizes across cores. Running with --test-threads=1 is what actually went green in the prior runner bring-up, so mirror it here to keep the integration workflow deterministic on windows-latest. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 34da1302487d9..bf7eb2264f1b4 100644 --- a/Makefile +++ b/Makefile @@ -392,7 +392,7 @@ test-integration: test-integration-datadog-traces test-integration-shutdown .PHONY: test-integration-windows-event-log test-integration-windows-event-log: ## Runs Windows Event Log integration tests (Windows only) ifeq ($(OS),Windows_NT) - ${MAYBE_ENVIRONMENT_EXEC} cargo test -p vector --no-default-features --features sources-windows_event_log-integration-tests windows_event_log::integration_tests + ${MAYBE_ENVIRONMENT_EXEC} cargo test -p vector --no-default-features --features sources-windows_event_log-integration-tests windows_event_log::integration_tests -- --test-threads=1 else @echo "Skipping windows-event-log integration tests (Windows only)" endif From b93f333db3d2378ced01b87a713bf227dac01da5 Mon Sep 17 00:00:00 2001 From: Emory Otott Date: Wed, 15 Apr 2026 17:12:31 +0900 Subject: [PATCH 8/8] ci: add pull_request + workflow_dispatch triggers to Unit - Windows So Windows unit tests (including the windows_event_log source's #[cfg(windows)] regression test for #25194) actually execute on PRs and can be manually triggered on the fork. Scoped via paths filter to files that could change Windows-only code. --- .github/workflows/unit_windows.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/unit_windows.yml b/.github/workflows/unit_windows.yml index 6290109925537..112ad148476ec 100644 --- a/.github/workflows/unit_windows.yml +++ b/.github/workflows/unit_windows.yml @@ -7,6 +7,15 @@ on: description: "Git ref to checkout" required: false type: string + workflow_dispatch: + pull_request: + paths: + - "src/sources/windows_event_log/**" + - "src/internal_events/windows_event_log.rs" + - "src/internal_events/windows.rs" + - "src/vector_windows.rs" + - "src/service.rs" + - ".github/workflows/unit_windows.yml" permissions: contents: read