1 | /* GIO - GLib Input, Output and Streaming Library |
2 | * |
3 | * Copyright (C) 2011 Collabora Ltd. |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Lesser General Public |
7 | * License as published by the Free Software Foundation; either |
8 | * version 2.1 of the License, or (at your option) any later version. |
9 | * |
10 | * This library is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | * Lesser General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Lesser General |
16 | * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. |
17 | * |
18 | * Author: Stef Walter <stefw@collabora.co.uk> |
19 | */ |
20 | |
21 | #include <locale.h> |
22 | |
23 | #include <gio/gio.h> |
24 | |
25 | #include "glib/glib-private.h" |
26 | |
27 | /* How long to wait in ms for each iteration */ |
28 | #define WAIT_ITERATION (10) |
29 | |
30 | static gint num_async_operations = 0; |
31 | |
32 | typedef struct |
33 | { |
34 | guint iterations_requested; /* construct-only */ |
35 | guint iterations_done; /* (atomic) */ |
36 | } MockOperationData; |
37 | |
38 | static void |
39 | mock_operation_free (gpointer user_data) |
40 | { |
41 | MockOperationData *data = user_data; |
42 | g_free (mem: data); |
43 | } |
44 | |
45 | static void |
46 | mock_operation_thread (GTask *task, |
47 | gpointer source_object, |
48 | gpointer task_data, |
49 | GCancellable *cancellable) |
50 | { |
51 | MockOperationData *data = task_data; |
52 | guint i; |
53 | |
54 | for (i = 0; i < data->iterations_requested; i++) |
55 | { |
56 | if (g_cancellable_is_cancelled (cancellable)) |
57 | break; |
58 | if (g_test_verbose ()) |
59 | g_test_message (format: "THRD: %u iteration %u" , data->iterations_requested, i); |
60 | g_usleep (WAIT_ITERATION * 1000); |
61 | } |
62 | |
63 | if (g_test_verbose ()) |
64 | g_test_message (format: "THRD: %u stopped at %u" , data->iterations_requested, i); |
65 | g_atomic_int_add (&data->iterations_done, i); |
66 | |
67 | g_task_return_boolean (task, TRUE); |
68 | } |
69 | |
70 | static gboolean |
71 | mock_operation_timeout (gpointer user_data) |
72 | { |
73 | GTask *task; |
74 | MockOperationData *data; |
75 | gboolean done = FALSE; |
76 | guint iterations_done; |
77 | |
78 | task = G_TASK (user_data); |
79 | data = g_task_get_task_data (task); |
80 | iterations_done = g_atomic_int_get (&data->iterations_done); |
81 | |
82 | if (iterations_done >= data->iterations_requested) |
83 | done = TRUE; |
84 | |
85 | if (g_cancellable_is_cancelled (cancellable: g_task_get_cancellable (task))) |
86 | done = TRUE; |
87 | |
88 | if (done) |
89 | { |
90 | if (g_test_verbose ()) |
91 | g_test_message (format: "LOOP: %u stopped at %u" , |
92 | data->iterations_requested, iterations_done); |
93 | g_task_return_boolean (task, TRUE); |
94 | return G_SOURCE_REMOVE; |
95 | } |
96 | else |
97 | { |
98 | g_atomic_int_inc (&data->iterations_done); |
99 | if (g_test_verbose ()) |
100 | g_test_message (format: "LOOP: %u iteration %u" , |
101 | data->iterations_requested, iterations_done + 1); |
102 | return G_SOURCE_CONTINUE; |
103 | } |
104 | } |
105 | |
106 | static void |
107 | mock_operation_async (guint wait_iterations, |
108 | gboolean run_in_thread, |
109 | GCancellable *cancellable, |
110 | GAsyncReadyCallback callback, |
111 | gpointer user_data) |
112 | { |
113 | GTask *task; |
114 | MockOperationData *data; |
115 | |
116 | task = g_task_new (NULL, cancellable, callback, callback_data: user_data); |
117 | data = g_new0 (MockOperationData, 1); |
118 | data->iterations_requested = wait_iterations; |
119 | g_task_set_task_data (task, task_data: data, task_data_destroy: mock_operation_free); |
120 | |
121 | if (run_in_thread) |
122 | { |
123 | g_task_run_in_thread (task, task_func: mock_operation_thread); |
124 | if (g_test_verbose ()) |
125 | g_test_message (format: "THRD: %d started" , wait_iterations); |
126 | } |
127 | else |
128 | { |
129 | g_timeout_add_full (G_PRIORITY_DEFAULT, WAIT_ITERATION, function: mock_operation_timeout, |
130 | g_object_ref (task), notify: g_object_unref); |
131 | if (g_test_verbose ()) |
132 | g_test_message (format: "LOOP: %d started" , wait_iterations); |
133 | } |
134 | |
135 | g_object_unref (object: task); |
136 | } |
137 | |
138 | static guint |
139 | mock_operation_finish (GAsyncResult *result, |
140 | GError **error) |
141 | { |
142 | MockOperationData *data; |
143 | GTask *task; |
144 | |
145 | g_assert_true (g_task_is_valid (result, NULL)); |
146 | |
147 | /* This test expects the return value to be iterations_done even |
148 | * when an error is set. |
149 | */ |
150 | task = G_TASK (result); |
151 | data = g_task_get_task_data (task); |
152 | |
153 | g_task_propagate_boolean (task, error); |
154 | return g_atomic_int_get (&data->iterations_done); |
155 | } |
156 | |
157 | static void |
158 | on_mock_operation_ready (GObject *source, |
159 | GAsyncResult *result, |
160 | gpointer user_data) |
161 | { |
162 | guint iterations_requested; |
163 | guint iterations_done; |
164 | GError *error = NULL; |
165 | |
166 | iterations_requested = GPOINTER_TO_UINT (user_data); |
167 | iterations_done = mock_operation_finish (result, error: &error); |
168 | |
169 | g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED); |
170 | g_error_free (error); |
171 | |
172 | g_assert_cmpint (iterations_requested, >, iterations_done); |
173 | num_async_operations--; |
174 | g_main_context_wakeup (NULL); |
175 | } |
176 | |
177 | static void |
178 | test_cancel_multiple_concurrent (void) |
179 | { |
180 | GCancellable *cancellable; |
181 | guint i, iterations; |
182 | |
183 | if (!g_test_thorough ()) |
184 | { |
185 | g_test_skip (msg: "Not running timing heavy test" ); |
186 | return; |
187 | } |
188 | |
189 | cancellable = g_cancellable_new (); |
190 | |
191 | for (i = 0; i < 45; i++) |
192 | { |
193 | iterations = i + 10; |
194 | mock_operation_async (wait_iterations: iterations, g_random_boolean (), cancellable, |
195 | callback: on_mock_operation_ready, GUINT_TO_POINTER (iterations)); |
196 | num_async_operations++; |
197 | } |
198 | |
199 | /* Wait for the threads to start up */ |
200 | while (num_async_operations != 45) |
201 | g_main_context_iteration (NULL, TRUE); |
202 | g_assert_cmpint (num_async_operations, ==, 45);\ |
203 | |
204 | if (g_test_verbose ()) |
205 | g_test_message (format: "CANCEL: %d operations" , num_async_operations); |
206 | g_cancellable_cancel (cancellable); |
207 | g_assert_true (g_cancellable_is_cancelled (cancellable)); |
208 | |
209 | /* Wait for all operations to be cancelled */ |
210 | while (num_async_operations != 0) |
211 | g_main_context_iteration (NULL, TRUE); |
212 | g_assert_cmpint (num_async_operations, ==, 0); |
213 | |
214 | g_object_unref (object: cancellable); |
215 | } |
216 | |
217 | static void |
218 | test_cancel_null (void) |
219 | { |
220 | g_cancellable_cancel (NULL); |
221 | } |
222 | |
223 | typedef struct |
224 | { |
225 | GCond cond; |
226 | GMutex mutex; |
227 | gboolean thread_ready; |
228 | GAsyncQueue *cancellable_source_queue; /* (owned) (element-type GCancellableSource) */ |
229 | } ThreadedDisposeData; |
230 | |
231 | static gboolean |
232 | cancelled_cb (GCancellable *cancellable, |
233 | gpointer user_data) |
234 | { |
235 | /* Nothing needs to be done here. */ |
236 | return G_SOURCE_CONTINUE; |
237 | } |
238 | |
239 | static gpointer |
240 | threaded_dispose_thread_cb (gpointer user_data) |
241 | { |
242 | ThreadedDisposeData *data = user_data; |
243 | GSource *cancellable_source; |
244 | |
245 | g_mutex_lock (mutex: &data->mutex); |
246 | data->thread_ready = TRUE; |
247 | g_cond_broadcast (cond: &data->cond); |
248 | g_mutex_unlock (mutex: &data->mutex); |
249 | |
250 | while ((cancellable_source = g_async_queue_pop (queue: data->cancellable_source_queue)) != (gpointer) 1) |
251 | { |
252 | /* Race with cancellation of the cancellable. */ |
253 | g_source_unref (source: cancellable_source); |
254 | } |
255 | |
256 | return NULL; |
257 | } |
258 | |
259 | static void |
260 | test_cancellable_source_threaded_dispose (void) |
261 | { |
262 | #ifdef _GLIB_ADDRESS_SANITIZER |
263 | g_test_incomplete ("FIXME: Leaks lots of GCancellableSource objects, see glib#2309" ); |
264 | (void) cancelled_cb; |
265 | (void) threaded_dispose_thread_cb; |
266 | #else |
267 | ThreadedDisposeData data; |
268 | GThread *thread = NULL; |
269 | guint i; |
270 | GPtrArray *cancellables_pending_unref = g_ptr_array_new_with_free_func (element_free_func: g_object_unref); |
271 | |
272 | g_test_summary (summary: "Test a thread race between disposing of a GCancellableSource " |
273 | "(in one thread) and cancelling the GCancellable it refers " |
274 | "to (in another thread)" ); |
275 | g_test_bug (bug_uri_snippet: "https://gitlab.gnome.org/GNOME/glib/issues/1841" ); |
276 | |
277 | /* Create a new thread and wait until it’s ready to execute. Each iteration of |
278 | * the test will pass it a new #GCancellableSource. */ |
279 | g_cond_init (cond: &data.cond); |
280 | g_mutex_init (mutex: &data.mutex); |
281 | data.cancellable_source_queue = g_async_queue_new_full (item_free_func: (GDestroyNotify) g_source_unref); |
282 | data.thread_ready = FALSE; |
283 | |
284 | g_mutex_lock (mutex: &data.mutex); |
285 | thread = g_thread_new (name: "/cancellable-source/threaded-dispose" , |
286 | func: threaded_dispose_thread_cb, data: &data); |
287 | |
288 | while (!data.thread_ready) |
289 | g_cond_wait (cond: &data.cond, mutex: &data.mutex); |
290 | g_mutex_unlock (mutex: &data.mutex); |
291 | |
292 | for (i = 0; i < 100000; i++) |
293 | { |
294 | GCancellable *cancellable = NULL; |
295 | GSource *cancellable_source = NULL; |
296 | |
297 | /* Create a cancellable and a cancellable source for it. For this test, |
298 | * there’s no need to attach the source to a #GMainContext. */ |
299 | cancellable = g_cancellable_new (); |
300 | cancellable_source = g_cancellable_source_new (cancellable); |
301 | g_source_set_callback (source: cancellable_source, G_SOURCE_FUNC (cancelled_cb), NULL, NULL); |
302 | |
303 | /* Send it to the thread and wait until it’s ready to execute before |
304 | * cancelling our cancellable. */ |
305 | g_async_queue_push (queue: data.cancellable_source_queue, g_steal_pointer (&cancellable_source)); |
306 | |
307 | /* Race with disposal of the cancellable source. */ |
308 | g_cancellable_cancel (cancellable); |
309 | |
310 | /* This thread can’t drop its reference to the #GCancellable here, as it |
311 | * might not be the final reference (depending on how the race is |
312 | * resolved: #GCancellableSource holds a strong ref on the #GCancellable), |
313 | * and at this point we can’t guarantee to support disposing of a |
314 | * #GCancellable in a different thread from where it’s created, especially |
315 | * when signal handlers are connected to it. |
316 | * |
317 | * So this is a workaround for a disposal-in-another-thread bug for |
318 | * #GCancellable, but there’s no hope of debugging and resolving it with |
319 | * this test setup, and the bug is orthogonal to what’s being tested here |
320 | * (a race between #GCancellable and #GCancellableSource). */ |
321 | g_ptr_array_add (array: cancellables_pending_unref, g_steal_pointer (&cancellable)); |
322 | } |
323 | |
324 | /* Indicate that the test has finished. Can’t use %NULL as #GAsyncQueue |
325 | * doesn’t allow that.*/ |
326 | g_async_queue_push (queue: data.cancellable_source_queue, data: (gpointer) 1); |
327 | |
328 | g_thread_join (g_steal_pointer (&thread)); |
329 | |
330 | g_assert (g_async_queue_length (data.cancellable_source_queue) == 0); |
331 | g_async_queue_unref (queue: data.cancellable_source_queue); |
332 | g_mutex_clear (mutex: &data.mutex); |
333 | g_cond_clear (cond: &data.cond); |
334 | |
335 | g_ptr_array_unref (array: cancellables_pending_unref); |
336 | #endif |
337 | } |
338 | |
339 | int |
340 | main (int argc, char *argv[]) |
341 | { |
342 | g_test_init (argc: &argc, argv: &argv, NULL); |
343 | |
344 | g_test_add_func (testpath: "/cancellable/multiple-concurrent" , test_func: test_cancel_multiple_concurrent); |
345 | g_test_add_func (testpath: "/cancellable/null" , test_func: test_cancel_null); |
346 | g_test_add_func (testpath: "/cancellable-source/threaded-dispose" , test_func: test_cancellable_source_threaded_dispose); |
347 | |
348 | return g_test_run (); |
349 | } |
350 | |