Mir

Merge lp:~raof/mir/fix-deadlock-in-glib-alarm into lp:mir

Proposed by Chris Halse Rogers on 2015-08-03
Status: Merged
Approved by: Alberto Aguirre on 2015-08-11
Approved revision: 2808
Merged at revision: 2832
Proposed branch: lp:~raof/mir/fix-deadlock-in-glib-alarm
Merge into: lp:mir
Diff against target: 406 lines (+139/-38)
9 files modified
examples/server_example.cpp (+6/-3)
examples/server_example_test_client.cpp (+15/-14)
examples/server_example_test_client.h (+12/-2)
src/include/server/mir/glib_main_loop_sources.h (+4/-2)
src/server/glib_main_loop.cpp (+6/-0)
src/server/glib_main_loop_sources.cpp (+19/-8)
tests/mir_test_framework/stub_input_platform.cpp (+9/-7)
tests/mir_test_framework/stub_input_platform.h (+2/-2)
tests/unit-tests/test_glib_main_loop.cpp (+66/-0)
To merge this branch: bzr merge lp:~raof/mir/fix-deadlock-in-glib-alarm
Reviewer Review Type Date Requested Status
Alberto Aguirre Approve on 2015-08-11
Alan Griffiths Approve on 2015-08-11
PS Jenkins bot continuous-integration Approve on 2015-08-11
Kevin DuBois (community) 2015-08-03 Approve on 2015-08-03
Review via email: mp+266682@code.launchpad.net

Commit Message

Fix a deadlock in GLibMainLoop's Alarm implementation.

There's currently a deadlock in the following case:

Thread 1: enters an alarm callback. (And so has taken TimerContext::mutex)
Thread 2: calls alarm->reschedule_in(), takes AlarmImpl::alarm_mutex, blocks when trying to take TimerContext::mutex
Thread 1: In the alarm callback, calls alarm->reschedule_in(), blocks on AlarmImpl::alarm_mutex.

The fundamental problem here is that we use ~GSourceHandle to guarantee that we've left all callbacks. This is needed for the guarantees provided by ~Alarm (and is nice for ::cancel), but is unnecessary for the reschedule_* calls - barring external synchronisation with the MainLoop, calling reschedule_* when there's an alarm already pending is inherently racing against real-time.

So, split the ensure-callbacks-are-cancelled guarantee out into GSourceHandle::ensure_no_further_dispatch(), and let ~GSourceHandle merely clean up all the relevant resources.

Description of the Change

Oh god.

Fix a deadlock in GLibMainLoop's Alarm implementation.

There's currently a deadlock in the following case:

Thread 1: enters an alarm callback. (And so has taken TimerContext::mutex)
Thread 2: calls alarm->reschedule_in(), takes AlarmImpl::alarm_mutex, blocks when trying to take TimerContext::mutex
Thread 1: In the alarm callback, calls alarm->reschedule_in(), blocks on AlarmImpl::alarm_mutex.

The fundamental problem here is that we use ~GSourceHandle to guarantee that we've left all callbacks. This is needed for the guarantees provided by ~Alarm (and is nice for ::cancel), but is unnecessary for the reschedule_* calls - barring external synchronisation with the MainLoop, calling reschedule_* when there's an alarm already pending is inherently racing against real-time.

So, split the ensure-callbacks-are-cancelled guarantee out into GSourceHandle::ensure_no_further_dispatch(), and let ~GSourceHandle merely clean up all the relevant resources.

For bonus points allows us to switch from a std::recursive_mutex to a std::mutex in TimerContext.

To post a comment you must log in.
Kevin DuBois (kdub) wrote :

lgtm

review: Approve
2800. By Chris Halse Rogers on 2015-08-04

Fix alarm-lifecycle FIXME in example server.

Don't have Alarms that outlive the server.

2801. By Chris Halse Rogers on 2015-08-04

Merge trunk, fixing GSourceHandle conflict

2802. By Chris Halse Rogers on 2015-08-05

Fix race in Alarm::cancel test case

2803. By Chris Halse Rogers on 2015-08-05

Guard access to stub platform pointers.

One more data race down. How many to go?

2804. By Chris Halse Rogers on 2015-08-05

Simplify GLibMainLoopAlarmTest::cancel_blocks_until_definitely_cancelled

At the cost of being slightly less accurate. It should also eliminate false-positives.

Chris Halse Rogers (raof) wrote :

Ok. Failure is:
+ phablet-test-run -x -s 00693fd555c9186a -v mir_demo_server --test-client /usr/bin/mir_demo_client_egltriangle
running mir_demo_server --test-client /usr/bin/mir_demo_client_egltriangle

Surface 0 DPI
Signal 15 received. Good night.
[1438765839.346519] mirserver: Stopping
/bin/bash: line 1: 11723 Segmentation fault (core dumped) mir_demo_server --test-client /usr/bin/mir_demo_client_egltriangle

And is the same for all the mir_demo_server --test-client... runs.

Unfortunately, I can't reproduce this failure crash, either on my development system or on a cross-built arale.

Chris Halse Rogers (raof) wrote :

Maybe it's cosmic rays?

Alberto Aguirre (albaguirre) wrote :

I can replicate the error with a cross-compile build using -Duse_debflags=ON and

"bin/mir_demo_server --test-client /home/phablet/mir/bin/mir_demo_client_egltriangle"

Probably related to the static alarm objects used in server_example_test_client.cpp

Alberto Aguirre (albaguirre) wrote :

Actually even without -Duse_debflags=ON the server still crashes at the end.

Alberto Aguirre (albaguirre) wrote :

And actually just exiting bin/mir_demo_server results in a crash too.

Chris Halse Rogers (raof) wrote :

Ok, I'm pretty sure this only happens on mako(!). I've tried, with a debug build and use_debflags=ON, on desktop and arale (rc-proposed, wily), and on none of those platforms does mir_demo_server crash on shutdown.

Time to see if my mako has awoken from its dead-battery fugue.

Alberto Aguirre (albaguirre) wrote :

This is happening on arale too (image #80 from ubuntu-touch/devel-proposed/meizu.en)

Stack trace:
http://pastebin.ubuntu.com/12022909/

Seems like memory corruption.

Alberto Aguirre (albaguirre) wrote :

Ahh this seems to be the same TLS issue I encountered in lp:1280086.

The relevant bit from screencast.cpp:

"/* On devices with android based openGL drivers, the vendor dispatcher table
 * may be optimized if USE_FAST_TLS_KEY is set, which hardcodes a TLS slot where
 * the vendor opengl function pointers live. Since glibc is not aware of this
 * use, collisions may happen.
"

2805. By Chris Halse Rogers on 2015-08-10

Remove static std::mutexes. Looks like this runs into TLS memory corruption issues on hybris

2806. By Chris Halse Rogers on 2015-08-10

Resolve StubInputPlatform data race with an atomic.

2807. By Chris Halse Rogers on 2015-08-10

Unfix alarm-lifecycle FIXME in example server.

Trying to actually tie the alarms' lifecycles to the server apparently hits
TLS-glibc-hybris-borkage resulting in memory corruption and segfault on exit,
and I'm disinclined to spend *more* time debugging why.

Just leak the alarms, since we never needed to run their destructors in the
first place.

Chris Halse Rogers (raof) wrote :

WOOOOOOO! FINALLY!

I wonder what the triggering condition for std::future clashing with the Android TLS slot is? We use futures elsewhere in the code without issue...

Chris Halse Rogers (raof) wrote :

Oh! Of course it's the TLS mode. The binary can (and almost certainly will) use the local-exec TLS model. The Mir libraries can at most use initial-exec model, and will probably(?) be using the local-dynamic model, and will be getting a non-conflicting slot.

Alan Griffiths (alan-griffiths) wrote :

Admittedly the existing code using "static" to extend the lifetime of these alarms is wrong, but I can't help feeling leaking them is also inelegant. (I've not thought of a better solution yet.)

I'd also be inclined to make leaked_kill_action & leaked_exit_action function scope static to make it clear they are not used elsewhere.

Chris Halse Rogers (raof) wrote :

Well, I *could* resurrect the pass-to-main implementation that ran into TLS problems. We could either try forcing a different TLS model or do what screencast does and just hope adding a couple of unused TLS entries will move stdlib's TLS entries out of the conflicting slot...

-----Original Message-----
From: "Alan Griffiths" <email address hidden>
Sent: ‎10/‎08/‎2015 18:56
To: "Chris Halse Rogers" <email address hidden>
Subject: Re: [Merge] lp:~raof/mir/fix-deadlock-in-glib-alarm into lp:mir

Admittedly the existing code using "static" to extend the lifetime of these alarms is wrong, but I can't help feeling leaking them is also inelegant. (I've not thought of a better solution yet.)

I'd also be inclined to make leaked_kill_action & leaked_exit_action function scope static to make it clear they are not used elsewhere.
--
https://code.launchpad.net/~raof/mir/fix-deadlock-in-glib-alarm/+merge/266682
You are the owner of lp:~raof/mir/fix-deadlock-in-glib-alarm.

Alan Griffiths (alan-griffiths) wrote :

On the basis that it is an improvement.

review: Approve
Alberto Aguirre (albaguirre) wrote :

You can actually leave them as static, I fixed what the old FIXME allured to (https://code.launchpad.net/~albaguirre/mir/fix-1480420/+merge/266614) but I forgot to remove the comment there.
Or also why not just use a condition variable instead of a std::future? TLS won't be an issue then.

review: Needs Information
2808. By Chris Halse Rogers on 2015-08-11

Fix alarm-lifecycle FIXME in a simpler manner.

Looking at it fresh, we don't actually need any of the std::future machinery.
We can just pass things around by reference. So do so.

Alan Griffiths (alan-griffiths) wrote :

OK

review: Approve
Alberto Aguirre (albaguirre) wrote :

LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'examples/server_example.cpp'
2--- examples/server_example.cpp 2015-06-17 05:20:42 +0000
3+++ examples/server_example.cpp 2015-08-11 01:44:53 +0000
4@@ -94,8 +94,8 @@
5 add_launcher_option_to(server);
6 add_timeout_option_to(server);
7
8- std::atomic<bool> test_failed{false};
9- me::add_test_client_option_to(server, test_failed);
10+ me::ClientContext context;
11+ me::add_test_client_option_to(server, context);
12
13 // Create some input filters (we need to keep them or they deactivate)
14 auto const quit_filter = me::make_quit_filter_for(server);
15@@ -108,7 +108,10 @@
16 server.run();
17
18 // Propagate any test failure
19- if (test_failed) return EXIT_FAILURE;
20+ if (context.test_failed)
21+ {
22+ return EXIT_FAILURE;
23+ }
24
25 return server.exited_normally() ? EXIT_SUCCESS : EXIT_FAILURE;
26 }
27
28=== modified file 'examples/server_example_test_client.cpp'
29--- examples/server_example_test_client.cpp 2015-07-20 03:16:27 +0000
30+++ examples/server_example_test_client.cpp 2015-08-11 01:44:53 +0000
31@@ -81,7 +81,7 @@
32 }
33 }
34
35-void me::add_test_client_option_to(mir::Server& server, std::atomic<bool>& test_failed)
36+void me::add_test_client_option_to(mir::Server& server, me::ClientContext& context)
37 {
38 static const char* const test_client_opt = "test-client";
39 static const char* const test_client_descr = "client executable";
40@@ -92,11 +92,14 @@
41 server.add_configuration_option(test_client_opt, test_client_descr, mir::OptionType::string);
42 server.add_configuration_option(test_timeout_opt, test_timeout_descr, 10);
43
44- server.add_init_callback([&]
45+ server.add_init_callback([&server, &context]
46 {
47 const auto options = server.get_options();
48+
49 if (options->is_set(test_client_opt))
50 {
51+ context.test_failed = true;
52+
53 auto const pid = fork();
54
55 if (pid == 0)
56@@ -107,32 +110,30 @@
57 }
58 else if (pid > 0)
59 {
60- //FIXME: These alarm objects outlive the server - their destructor is called after the server is destroyed.
61- //The alarm destructor implementation reference glib objects that are assumed to exist which leads to crashes.
62- //For now, as a workaround, canceling the alarm will release such internal resources
63- static std::unique_ptr<mir::time::Alarm> const kill_action = server.the_main_loop()->create_alarm(
64+ context.client_kill_action = server.the_main_loop()->create_alarm(
65 [pid]
66 {
67- kill_action->cancel();
68 kill(pid, SIGTERM);
69 });
70
71- static std::unique_ptr<mir::time::Alarm> const exit_action = server.the_main_loop()->create_alarm(
72- [pid, &server, &test_failed]
73+ context.server_stop_action = server.the_main_loop()->create_alarm(
74+ [pid, &server, &context]()
75 {
76- if (!exit_success(pid))
77- test_failed = true;
78- exit_action->cancel();
79+ context.test_failed = !exit_success(pid);
80 server.stop();
81 });
82
83- kill_action->reschedule_in(std::chrono::seconds(options->get<int>(test_timeout_opt)));
84- exit_action->reschedule_in(std::chrono::seconds(options->get<int>(test_timeout_opt)+1));
85+ context.client_kill_action->reschedule_in(std::chrono::seconds(options->get<int>(test_timeout_opt)));
86+ context.server_stop_action->reschedule_in(std::chrono::seconds(options->get<int>(test_timeout_opt)+1));
87 }
88 else
89 {
90 BOOST_THROW_EXCEPTION(std::runtime_error("Client failed to launch"));
91 }
92 }
93+ else
94+ {
95+ context.test_failed = false;
96+ }
97 });
98 }
99
100=== modified file 'examples/server_example_test_client.h'
101--- examples/server_example_test_client.h 2014-12-17 17:01:13 +0000
102+++ examples/server_example_test_client.h 2015-08-11 01:44:53 +0000
103@@ -19,7 +19,10 @@
104 #ifndef MIR_EXAMPLE_TEST_CLIENT_H_
105 #define MIR_EXAMPLE_TEST_CLIENT_H_
106
107-#include <atomic>
108+#include <memory>
109+#include <future>
110+
111+#include "mir/main_loop.h"
112
113 namespace mir
114 {
115@@ -27,7 +30,14 @@
116
117 namespace examples
118 {
119-void add_test_client_option_to(mir::Server& server, std::atomic<bool>& test_failed);
120+struct ClientContext
121+{
122+ std::unique_ptr<mir::time::Alarm> client_kill_action;
123+ std::unique_ptr<mir::time::Alarm> server_stop_action;
124+ std::atomic<bool> test_failed;
125+};
126+
127+void add_test_client_option_to(mir::Server& server, ClientContext& context);
128 }
129 }
130
131
132=== modified file 'src/include/server/mir/glib_main_loop_sources.h'
133--- src/include/server/mir/glib_main_loop_sources.h 2015-06-17 05:20:42 +0000
134+++ src/include/server/mir/glib_main_loop_sources.h 2015-08-11 01:44:53 +0000
135@@ -50,16 +50,18 @@
136 {
137 public:
138 GSourceHandle();
139- GSourceHandle(GSource* gsource, std::function<void(GSource*)> const& pre_destruction_hook);
140+ GSourceHandle(GSource* gsource, std::function<void(GSource*)> const& terminate_dispatch);
141 GSourceHandle(GSourceHandle&& other);
142 GSourceHandle& operator=(GSourceHandle other);
143 ~GSourceHandle();
144
145+ void ensure_no_further_dispatch();
146+
147 operator GSource*() const;
148
149 private:
150 GSource* gsource;
151- std::function<void(GSource*)> pre_destruction_hook;
152+ std::function<void(GSource*)> terminate_dispatch;
153 };
154
155 void add_idle_gsource(
156
157=== modified file 'src/server/glib_main_loop.cpp'
158--- src/server/glib_main_loop.cpp 2015-06-17 05:20:42 +0000
159+++ src/server/glib_main_loop.cpp 2015-08-11 01:44:53 +0000
160@@ -47,10 +47,16 @@
161 {
162 }
163
164+ ~AlarmImpl() override
165+ {
166+ gsource.ensure_no_further_dispatch();
167+ }
168+
169 bool cancel() override
170 {
171 std::lock_guard<std::mutex> lock{alarm_mutex};
172
173+ gsource.ensure_no_further_dispatch();
174 gsource = mir::detail::GSourceHandle{};
175 state_ = State::cancelled;
176 return true;
177
178=== modified file 'src/server/glib_main_loop_sources.cpp'
179--- src/server/glib_main_loop_sources.cpp 2015-07-31 18:57:59 +0000
180+++ src/server/glib_main_loop_sources.cpp 2015-08-11 01:44:53 +0000
181@@ -80,24 +80,24 @@
182
183 md::GSourceHandle::GSourceHandle(
184 GSource* gsource,
185- std::function<void(GSource*)> const& pre_destruction_hook)
186+ std::function<void(GSource*)> const& terminate_dispatch)
187 : gsource(gsource),
188- pre_destruction_hook(pre_destruction_hook)
189+ terminate_dispatch(terminate_dispatch)
190 {
191 }
192
193 md::GSourceHandle::GSourceHandle(GSourceHandle&& other)
194 : gsource(std::move(other.gsource)),
195- pre_destruction_hook(std::move(other.pre_destruction_hook))
196+ terminate_dispatch(std::move(other.terminate_dispatch))
197 {
198 other.gsource = nullptr;
199- other.pre_destruction_hook = [](GSource*){};
200+ other.terminate_dispatch = [](GSource*){};
201 }
202
203 md::GSourceHandle& md::GSourceHandle::operator=(GSourceHandle other)
204 {
205 std::swap(other.gsource, gsource);
206- std::swap(other.pre_destruction_hook, pre_destruction_hook);
207+ std::swap(other.terminate_dispatch, terminate_dispatch);
208 return *this;
209 }
210
211@@ -105,8 +105,6 @@
212 {
213 if (gsource)
214 {
215- pre_destruction_hook(gsource);
216-
217 #ifdef GLIB_HAS_FIXED_LP_1401488
218 g_source_destroy(gsource);
219 g_source_unref(gsource);
220@@ -138,6 +136,14 @@
221 }
222 }
223
224+void md::GSourceHandle::ensure_no_further_dispatch()
225+{
226+ if (gsource)
227+ {
228+ terminate_dispatch(gsource);
229+ }
230+}
231+
232 md::GSourceHandle::operator GSource*() const
233 {
234 return gsource;
235@@ -260,8 +266,8 @@
236 std::shared_ptr<LockableCallback> handler;
237 std::function<void()> exception_handler;
238 time::Timestamp target_time;
239+ std::mutex mutex;
240 bool enabled;
241- std::recursive_mutex mutex;
242 };
243
244 struct TimerGSource
245@@ -393,6 +399,11 @@
246
247 struct md::FdSources::FdSource
248 {
249+ ~FdSource()
250+ {
251+ gsource.ensure_no_further_dispatch();
252+ }
253+
254 GSourceHandle gsource;
255 void const* const owner;
256 };
257
258=== modified file 'tests/mir_test_framework/stub_input_platform.cpp'
259--- tests/mir_test_framework/stub_input_platform.cpp 2015-07-30 08:33:38 +0000
260+++ tests/mir_test_framework/stub_input_platform.cpp 2015-08-11 01:44:53 +0000
261@@ -68,14 +68,15 @@
262
263 void mtf::StubInputPlatform::add(std::shared_ptr<mir::input::InputDevice> const& dev)
264 {
265- if (!stub_input_platform)
266+ auto input_platform = stub_input_platform.load();
267+ if (!input_platform)
268 {
269 device_store.push_back(dev);
270 return;
271 }
272
273- stub_input_platform->platform_queue->enqueue(
274- [registry=stub_input_platform->registry,dev]
275+ input_platform->platform_queue->enqueue(
276+ [registry=input_platform->registry,dev]
277 {
278 registry->add_device(dev);
279 });
280@@ -83,15 +84,16 @@
281
282 void mtf::StubInputPlatform::remove(std::shared_ptr<mir::input::InputDevice> const& dev)
283 {
284- if (!stub_input_platform)
285+ auto input_platform = stub_input_platform.load();
286+ if (!input_platform)
287 BOOST_THROW_EXCEPTION(std::runtime_error("No stub input platform available"));
288
289- stub_input_platform->platform_queue->enqueue(
290- [registry=stub_input_platform->registry,dev]
291+ input_platform->platform_queue->enqueue(
292+ [registry=input_platform->registry,dev]
293 {
294 registry->remove_device(dev);
295 });
296 }
297
298-mtf::StubInputPlatform* mtf::StubInputPlatform::stub_input_platform = nullptr;
299+std::atomic<mtf::StubInputPlatform*> mtf::StubInputPlatform::stub_input_platform{nullptr};
300 std::vector<std::weak_ptr<mir::input::InputDevice>> mtf::StubInputPlatform::device_store;
301
302=== modified file 'tests/mir_test_framework/stub_input_platform.h'
303--- tests/mir_test_framework/stub_input_platform.h 2015-04-09 08:57:24 +0000
304+++ tests/mir_test_framework/stub_input_platform.h 2015-08-11 01:44:53 +0000
305@@ -19,7 +19,7 @@
306 #define MIR_TEST_FRAMEWORK_STUB_INPUT_PLATFORM_H_
307
308 #include "mir/input/platform.h"
309-#include <mutex>
310+#include <atomic>
311 #include <memory>
312 #include <vector>
313
314@@ -53,7 +53,7 @@
315 private:
316 std::shared_ptr<mir::dispatch::ActionQueue> const platform_queue;
317 std::shared_ptr<mir::input::InputDeviceRegistry> const registry;
318- static StubInputPlatform* stub_input_platform;
319+ static std::atomic<StubInputPlatform*> stub_input_platform;
320 static std::vector<std::weak_ptr<mir::input::InputDevice>> device_store;
321 };
322
323
324=== modified file 'tests/unit-tests/test_glib_main_loop.cpp'
325--- tests/unit-tests/test_glib_main_loop.cpp 2015-08-06 16:19:28 +0000
326+++ tests/unit-tests/test_glib_main_loop.cpp 2015-08-11 01:44:53 +0000
327@@ -24,6 +24,7 @@
328 #include "mir/test/pipe.h"
329 #include "mir/test/fake_shared.h"
330 #include "mir/test/auto_unblock_thread.h"
331+#include "mir/test/barrier.h"
332 #include "mir/test/doubles/advanceable_clock.h"
333 #include "mir/test/doubles/mock_lockable_callback.h"
334 #include "mir_test_framework/process.h"
335@@ -1095,6 +1096,71 @@
336 EXPECT_THAT(num_triggers, Eq(expected_triggers));
337 }
338
339+TEST_F(GLibMainLoopAlarmTest, rescheduling_alarm_from_within_alarm_callback_doesnt_deadlock_with_external_reschedule)
340+{
341+ using namespace testing;
342+ using namespace std::literals::chrono_literals;
343+
344+ mt::Signal in_alarm;
345+ mt::Signal alarm_rescheduled;
346+
347+ std::shared_ptr<mir::time::Alarm> alarm = ml.create_alarm(
348+ [&]
349+ {
350+ // Ensure that the external thread reschedules us while we're
351+ // in the callback.
352+ in_alarm.raise();
353+ ASSERT_TRUE(alarm_rescheduled.wait_for(5s));
354+
355+ alarm->reschedule_in(0ms);
356+
357+ ml.stop();
358+ });
359+
360+ alarm->reschedule_in(0ms);
361+
362+ mt::AutoJoinThread rescheduler{
363+ [alarm, &in_alarm, &alarm_rescheduled]()
364+ {
365+ ASSERT_TRUE(in_alarm.wait_for(5s));
366+ alarm->reschedule_in(0ms);
367+ alarm_rescheduled.raise();
368+ }};
369+
370+ ml.run();
371+}
372+
373+TEST_F(GLibMainLoopAlarmTest, cancel_blocks_until_definitely_cancelled)
374+{
375+ using namespace testing;
376+ using namespace std::literals::chrono_literals;
377+
378+ auto waiting_in_lock = std::make_shared<mt::Barrier>(2);
379+ auto has_been_called = std::make_shared<mt::Signal>();
380+
381+ std::shared_ptr<mir::time::Alarm> alarm = ml.create_alarm(
382+ [waiting_in_lock, has_been_called]()
383+ {
384+ waiting_in_lock->ready();
385+ std::this_thread::sleep_for(500ms);
386+ has_been_called->raise();
387+ });
388+
389+ alarm->reschedule_in(0ms);
390+
391+ mt::AutoJoinThread canceller{
392+ [waiting_in_lock, has_been_called, alarm, this]()
393+ {
394+ waiting_in_lock->ready();
395+ alarm->cancel();
396+ EXPECT_TRUE(has_been_called->raised());
397+ ml.stop();
398+ }
399+ };
400+
401+ ml.run();
402+}
403+
404 // More targeted regression test for LP: #1381925
405 TEST_F(GLibMainLoopTest, stress_emits_alarm_notification_with_zero_timeout)
406 {

Subscribers

People subscribed via source and target branches