Skip to content

Commit 394990c

Browse files
committed
more tests
1 parent 3116e13 commit 394990c

2 files changed

Lines changed: 188 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Copyright 2022 The Photon Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#include <atomic>
18+
#include <thread>
19+
#include <vector>
20+
21+
#include <photon/common/alog.h>
22+
#include <photon/common/lockfree_queue.h>
23+
#include <photon/photon.h>
24+
#include <photon/thread/thread.h>
25+
26+
#include "../../../test/gtest.h"
27+
28+
// Regression test for the RingChannel notification refactor.
29+
//
30+
// Failure mode being guarded:
31+
// When N producers fire a burst, the previous design issued one
32+
// `queue_sem.signal(1)` per push every time the consumer happened to be in
33+
// its `idler` window. The semaphore counter accumulated linearly with the
34+
// burst size, so after the burst the consumer would loop forever between
35+
// 1ms busy-yield and a `wait()` that returned immediately, burning a full
36+
// core for as long as the accumulated counter took to drain.
37+
//
38+
// Verification strategy (deterministic, no wall-clock assumptions):
39+
// - Drive a real `RingChannel` directly, with `kProducers` std::thread
40+
// producers and a single std::thread consumer that owns its own photon
41+
// vCPU.
42+
// - Synchronize using a producer-side `received` counter and a sentinel
43+
// value to terminate the consumer. All "wait" operations are
44+
// event-driven (`std::thread::join`, atomic counter spin), never
45+
// wall-clock sleeps.
46+
// - After the consumer thread has fully joined, all RingChannel state is
47+
// quiescent and visible to the test thread. Assert the *invariant*
48+
// `notification_pending() <= idler_peak`. With a single consumer,
49+
// `idler_peak == 1`, so `pending` must remain ≤1 regardless of burst
50+
// size. Without the fix, `pending` (== `queue_sem.m_count`) would carry
51+
// leftover tokens proportional to how often the consumer dipped into
52+
// the idler window during the burst.
53+
TEST(ring_channel, burst_drain_no_signal_accumulation) {
54+
using QT = LockfreeMPMCRingQueue<int, 4096>;
55+
photon::common::RingChannel<QT> ch(1024, 1024);
56+
57+
constexpr int kProducers = 4;
58+
constexpr int kPerProducer = 25000;
59+
constexpr int kBurst = kProducers * kPerProducer;
60+
constexpr int kSentinel = 0; // sentinel value to terminate consumer
61+
62+
std::atomic<int> received{0};
63+
64+
// Consumer owns its own photon vCPU, so it can be scheduled
65+
// independently of the producers and of this test thread.
66+
std::thread consumer([&] {
67+
photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_NONE);
68+
DEFER(photon::fini());
69+
for (;;) {
70+
int x = ch.recv();
71+
if (x == kSentinel) break;
72+
received.fetch_add(1, std::memory_order_acq_rel);
73+
}
74+
});
75+
76+
std::vector<std::thread> producers;
77+
producers.reserve(kProducers);
78+
for (int p = 0; p < kProducers; ++p) {
79+
producers.emplace_back([&] {
80+
for (int i = 0; i < kPerProducer; ++i) {
81+
// Producers are plain std::threads; ThreadPause yields the
82+
// OS thread on the (extremely unlikely) full-queue spin.
83+
ch.template send<ThreadPause>(/*non-zero*/ 1);
84+
}
85+
});
86+
}
87+
for (auto& t : producers) t.join();
88+
89+
// Deterministic synchronization point: spin until the consumer has
90+
// drained exactly the burst. No wall-clock sleeps.
91+
while (received.load(std::memory_order_acquire) < kBurst) {
92+
std::this_thread::yield();
93+
}
94+
95+
// Terminate consumer cleanly, then join. After join() returns, no other
96+
// thread can touch `ch`, so we can sample its state safely.
97+
ch.template send<ThreadPause>(kSentinel);
98+
consumer.join();
99+
100+
auto pending = ch.notification_pending();
101+
LOG_INFO("after burst: received=`, notification_pending=`",
102+
received.load(), pending);
103+
104+
// Strong invariant under the fix: in-flight wake-up tokens are capped
105+
// by the historical peak of `idler`. With a single consumer that peak
106+
// is 1, so `pending` must remain ≤1 regardless of burst size. Without
107+
// the fix `pending` would scale with kBurst.
108+
EXPECT_LE(pending, 1u);
109+
EXPECT_EQ(kBurst, received.load());
110+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2022 The Photon Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#include <atomic>
18+
19+
#include <photon/common/alog.h>
20+
#include <photon/photon.h>
21+
#include <photon/thread/thread.h>
22+
#include <photon/thread/workerpool.h>
23+
24+
#include "../../test/gtest.h"
25+
26+
// Regression test for the RingChannel notification refactor's multi-consumer
27+
// behavior: when N idle workers share a RingChannel, a burst of N tasks must
28+
// fan out to all of them concurrently. A binary (single-token) notification
29+
// scheme would serialize the burst onto a single worker.
30+
//
31+
// Verification strategy (deterministic, no wall-clock assumptions):
32+
// - Each task signals an `arrived` photon::semaphore on entry and then
33+
// blocks on `release` until the harness allows it to finish.
34+
// - The harness calls `arrived.wait(N, deadline_us)`. If fan-out works,
35+
// all N tasks reach the wait point; if the notification path serializes,
36+
// only one worker can run at a time (the others are stuck holding their
37+
// vCPU on `release.wait`), so `arrived` will never reach N and the wait
38+
// will return non-zero on timeout. The deadline is generous (5s) to
39+
// tolerate CI noise without weakening the assertion.
40+
TEST(workpool, fanout_wakeup) {
41+
// WorkPool::async_call uses PhotonPause and signals a photon::semaphore;
42+
// the caller side must already be a photon vcpu.
43+
photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_NONE);
44+
DEFER(photon::fini());
45+
46+
constexpr int N = 4;
47+
photon::WorkPool pool(N, photon::INIT_EVENT_DEFAULT,
48+
photon::INIT_IO_NONE, -1);
49+
50+
photon::semaphore arrived(0);
51+
photon::semaphore release(0);
52+
std::atomic<int> done{0};
53+
54+
for (int i = 0; i < N; ++i) {
55+
pool.async_call(new auto([&] {
56+
// Mark this worker as woken-and-running. Hold the vCPU until the
57+
// harness lets every other worker also reach this point.
58+
arrived.signal(1);
59+
release.wait(1);
60+
done.fetch_add(1, std::memory_order_release);
61+
}));
62+
}
63+
64+
// Deterministic fan-out check. With the fix, all N tasks must reach
65+
// `arrived.signal(1)` concurrently. Without it, only one worker would
66+
// ever be released by the channel, so `arrived` saturates at 1 and the
67+
// wait times out.
68+
int r = arrived.wait(N, /*timeout_us=*/5UL * 1000 * 1000);
69+
EXPECT_EQ(0, r);
70+
LOG_INFO("fan-out check: arrived.wait(`) returned ` (vcpu_num=`)",
71+
N, r, N);
72+
73+
// Release everyone so the WorkPool can be cleanly destroyed.
74+
release.signal(N);
75+
while (done.load(std::memory_order_acquire) < N) {
76+
photon::thread_yield();
77+
}
78+
}

0 commit comments

Comments
 (0)