cool.io 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ /* A private libev copy permits failure injection without production test hooks. */
2
+ #include <ruby.h>
3
+ #include "ev_wrap.h"
4
+ #include <sys/event.h>
5
+ #include <errno.h>
6
+ #include <stdlib.h>
7
+ #include <stdio.h>
8
+ #include <string.h>
9
+ #include <unistd.h>
10
+ #include <sys/wait.h>
11
+ #include <sys/mman.h>
12
+
13
+ static int fail_kqueue, kqueue_calls, fail_calloc, changed, timed_out;
14
+ static int invalid_closes;
15
+ static int event_reads, flood_events;
16
+ static void *flood_watcher;
17
+ static const char *registration_change;
18
+
19
+ static int
20
+ test_kevent (int fd, const struct kevent *changes, int count,
21
+ struct kevent *events, int size, const struct timespec *timeout)
22
+ {
23
+ int result;
24
+ if (!count && size)
25
+ {
26
+ ++event_reads;
27
+ if (flood_events && event_reads <= 2)
28
+ {
29
+ int i;
30
+ for (i = 0; i < size; ++i)
31
+ EV_SET (&events [i], 0, EVFILT_VNODE, 0, NOTE_WRITE, 0, flood_watcher);
32
+ return size;
33
+ }
34
+ }
35
+ result = kevent (fd, changes, count, events, size, timeout);
36
+ if (count && result == 0 && registration_change)
37
+ {
38
+ FILE *file = fopen (registration_change, "w");
39
+ if (file)
40
+ { fputs ("registration race", file); fclose (file); }
41
+ registration_change = 0;
42
+ }
43
+ return result;
44
+ }
45
+
46
+ static int
47
+ test_close (int fd)
48
+ {
49
+ if (fd < 0)
50
+ ++invalid_closes;
51
+ return close (fd);
52
+ }
53
+
54
+ static int
55
+ test_kqueue (void)
56
+ {
57
+ ++kqueue_calls;
58
+ if (fail_kqueue)
59
+ {
60
+ errno = EMFILE;
61
+ return -1;
62
+ }
63
+ return kqueue ();
64
+ }
65
+
66
+ static void *
67
+ test_calloc (size_t count, size_t size)
68
+ {
69
+ if (fail_calloc)
70
+ {
71
+ fail_calloc = 0;
72
+ errno = ENOMEM;
73
+ return 0;
74
+ }
75
+ return calloc (count, size);
76
+ }
77
+
78
+ /* sys/event.h uses EV_ERROR for a different flag than libev. */
79
+ #undef EV_ERROR
80
+ #define kevent(...) test_kevent (__VA_ARGS__)
81
+ #define kqueue test_kqueue
82
+ #define calloc test_calloc
83
+ #define close test_close
84
+ #include "libev.c"
85
+ #undef kevent
86
+ #undef kqueue
87
+ #undef calloc
88
+ #undef close
89
+
90
+ static void
91
+ changed_cb (EV_P_ ev_stat *w, int revents)
92
+ {
93
+ changed = 1;
94
+ ev_break (EV_A_ EVBREAK_ALL);
95
+ }
96
+
97
+ static void
98
+ timeout_cb (EV_P_ ev_timer *w, int revents)
99
+ {
100
+ timed_out = 1;
101
+ ev_break (EV_A_ EVBREAK_ALL);
102
+ }
103
+
104
+ static int
105
+ write_file (const char *path, const char *contents)
106
+ {
107
+ FILE *file = fopen (path, "w");
108
+ if (!file)
109
+ return 0;
110
+ fputs (contents, file);
111
+ return fclose (file) == 0;
112
+ }
113
+
114
+ static int
115
+ await_change (struct ev_loop *loop)
116
+ {
117
+ ev_timer deadline;
118
+ changed = timed_out = 0;
119
+ ev_timer_init (&deadline, timeout_cb, 1., 0.);
120
+ ev_timer_start (loop, &deadline);
121
+ ev_run (loop, 0);
122
+ ev_timer_stop (loop, &deadline);
123
+ return changed && !timed_out;
124
+ }
125
+
126
+ #if EV_USE_KQUEUE_STAT
127
+ static int
128
+ check_reused_fd (struct ev_loop *loop, ev_stat *watcher, int recreate)
129
+ {
130
+ int stale_fd = loop->kq_stat->fd;
131
+ int status;
132
+ pid_t pid = fork ();
133
+ if (pid < 0)
134
+ return 0;
135
+ if (!pid)
136
+ {
137
+ struct stat st;
138
+ int fd = open ("/dev/null", O_RDONLY);
139
+ if (fd < 0 || dup2 (fd, stale_fd) < 0)
140
+ _exit (1);
141
+ if (fd != stale_fd)
142
+ close (fd);
143
+
144
+ /* The child reuses the descriptor number of its parent's kqueue. */
145
+ if (recreate)
146
+ kq_stat_fork (loop);
147
+ ev_stat_stop (loop, watcher);
148
+ ev_loop_destroy (loop);
149
+ _exit (fstat (stale_fd, &st) == 0 && S_ISCHR (st.st_mode) ? 0 : 1);
150
+ }
151
+ while (waitpid (pid, &status, 0) < 0)
152
+ if (errno != EINTR)
153
+ return 0;
154
+ return WIFEXITED (status) && WEXITSTATUS (status) == 0;
155
+ }
156
+ #endif
157
+
158
+ static VALUE
159
+ check (VALUE self, VALUE mode, VALUE path)
160
+ {
161
+ const char *scenario = StringValueCStr (mode);
162
+ const char *filename = StringValueCStr (path);
163
+ const char *error = 0;
164
+ struct ev_loop *loop;
165
+ ev_stat watcher;
166
+ int disabled = !strcmp (scenario, "disabled");
167
+ int init_failure = !strcmp (scenario, "init_failure");
168
+ int fork_failure = !strcmp (scenario, "fork_failure");
169
+ int slot_failure = !strcmp (scenario, "slot_failure");
170
+
171
+ fail_kqueue = kqueue_calls = fail_calloc = invalid_closes = 0;
172
+ event_reads = flood_events = 0;
173
+ registration_change = 0;
174
+ if (!write_file (filename, "a"))
175
+ rb_sys_fail (filename);
176
+ /* A poll backend isolates the stat kqueue from the loop's I/O backend. */
177
+ loop = ev_loop_new (EVFLAG_NOENV | EVBACKEND_POLL | (disabled ? EVFLAG_NOINOTIFY : 0));
178
+ if (!loop)
179
+ rb_raise (rb_eRuntimeError, "ev_loop_new failed");
180
+
181
+ #if EV_USE_KQUEUE_STAT
182
+ if (slot_failure)
183
+ {
184
+ if (!kq_stat_init (loop))
185
+ { error = "could not initialize stat kqueue"; goto destroy; }
186
+ fail_calloc = 1;
187
+ }
188
+ #endif
189
+ fail_kqueue = init_failure;
190
+ ev_stat_init (&watcher, changed_cb, filename, 0.1);
191
+ ev_stat_start (loop, &watcher);
192
+
193
+ #if EV_USE_KQUEUE_STAT
194
+ if (!strcmp (scenario, "bounded_batch"))
195
+ {
196
+ flood_watcher = &watcher;
197
+ flood_events = 1;
198
+ kq_stat_cb (loop, &loop->kq_stat->io, EV_READ);
199
+ flood_events = 0;
200
+ if (event_reads != 1)
201
+ error = "notification callback drained more than one batch";
202
+ goto stop;
203
+ }
204
+ if (!strcmp (scenario, "io_error") || !strcmp (scenario, "io_error_valid_fd"))
205
+ {
206
+ int refs = loop->activecnt;
207
+ int fd = loop->kq_stat->fd;
208
+ fd_reify (loop);
209
+ if (!strcmp (scenario, "io_error"))
210
+ close (fd);
211
+ fd_kill (loop, fd);
212
+ ev_invoke_pending (loop);
213
+ if (fcntl (fd, F_GETFD) != -1 || errno != EBADF
214
+ || loop->activecnt != refs || loop->kq_stat->fd != -1
215
+ || ev_is_active (&loop->kq_stat->io) || loop->kq_stat->slots [watcher.wd]->fd != -1)
216
+ { error = "I/O failure did not restore polling references"; goto stop; }
217
+ }
218
+ if (!strcmp (scenario, "registration_race"))
219
+ {
220
+ kq_stat_close_slot (loop->kq_stat, loop->kq_stat->slots [watcher.wd]);
221
+ registration_change = filename;
222
+ changed = 0;
223
+ stat_timer_cb (loop, &watcher.timer, EV_TIMER);
224
+ ev_invoke_pending (loop);
225
+ if (!changed || registration_change)
226
+ error = "retry missed a change during registration";
227
+ goto stop;
228
+ }
229
+ if (!strcmp (scenario, "gc_cleanup"))
230
+ {
231
+ size_t size = (size_t)getpagesize ();
232
+ ev_stat *collected = (ev_stat *)mmap (0, size, PROT_READ | PROT_WRITE,
233
+ MAP_PRIVATE | MAP_ANON, -1, 0);
234
+ if (collected == MAP_FAILED)
235
+ { error = "could not allocate watcher storage"; goto stop; }
236
+ ev_stat_stop (loop, &watcher);
237
+ ev_stat_init (collected, changed_cb, filename, 0.1);
238
+ ev_stat_start (loop, collected);
239
+ /* Ruby GC may free watcher storage before freeing its loop. Make any
240
+ * access to that storage fail deterministically during loop destruction. */
241
+ if (mprotect (collected, size, PROT_NONE) < 0)
242
+ {
243
+ ev_stat_stop (loop, collected);
244
+ munmap (collected, size);
245
+ error = "could not protect watcher storage";
246
+ goto stop;
247
+ }
248
+ ev_loop_destroy (loop);
249
+ munmap (collected, size);
250
+ return Qtrue;
251
+ }
252
+ if (!strcmp (scenario, "fork_fd_reuse") || !strcmp (scenario, "destroy_fd_reuse"))
253
+ {
254
+ if (!check_reused_fd (loop, &watcher, !strcmp (scenario, "fork_fd_reuse")))
255
+ error = "child closed a descriptor that reused the parent's kqueue number";
256
+ goto stop;
257
+ }
258
+ if (disabled && (kqueue_calls || loop->kq_stat || !loop->kq_stat_disabled))
259
+ { error = "NOINOTIFY did not disable stat kqueue"; goto stop; }
260
+ if (init_failure && (kqueue_calls != 1 || loop->kq_stat || !ev_is_active (&watcher.timer)))
261
+ { error = "injected EMFILE did not leave a polling watcher"; goto stop; }
262
+ if (slot_failure && (fail_calloc || watcher.wd >= 0 || loop->kq_stat->slotcnt))
263
+ { error = "injected allocation failure did not leave an unregistered watcher"; goto stop; }
264
+ if (fork_failure)
265
+ {
266
+ int refs = loop->activecnt;
267
+ fail_kqueue = 1;
268
+ /* Inject failures directly; the watcher spec covers a real fork. */
269
+ kq_stat_fork (loop);
270
+ kq_stat_fork (loop); /* repeated failure must not leak loop references */
271
+ kq_stat_poll (loop, &watcher, EV_TIMER);
272
+ if (loop->kq_stat->fd >= 0 || ev_is_active (&loop->kq_stat->io) || loop->activecnt != refs)
273
+ { error = "failed fork recreation left an active kqueue or leaked references"; goto stop; }
274
+ }
275
+ #endif
276
+
277
+ if (!write_file (filename, "final") || !await_change (loop))
278
+ { error = "polling did not deliver the change"; goto stop; }
279
+
280
+ #if EV_USE_KQUEUE_STAT
281
+ if (disabled)
282
+ {
283
+ if (kqueue_calls || loop->kq_stat)
284
+ { error = "polling ignored NOINOTIFY"; goto stop; }
285
+ }
286
+ else
287
+ {
288
+ fail_kqueue = 0;
289
+ /* Retry deterministically; await_change above exercises real timer ticks. */
290
+ kq_stat_poll (loop, &watcher, EV_TIMER);
291
+ if (!loop->kq_stat || loop->kq_stat->fd < 0 || !ev_is_active (&loop->kq_stat->io) || watcher.wd < 0)
292
+ { error = "notifications did not recover after the injected failure"; goto stop; }
293
+ /* A long interval proves the recovered registration delivers native events. */
294
+ watcher.timer.repeat = 60.;
295
+ ev_timer_again (loop, &watcher.timer);
296
+ if (!write_file (filename, "longer") || !await_change (loop))
297
+ { error = "recovered native registration did not deliver the change"; goto stop; }
298
+ }
299
+ #else
300
+ if (kqueue_calls)
301
+ { error = "disabled build attempted kqueue initialization"; goto stop; }
302
+ #endif
303
+
304
+ stop:
305
+ fail_kqueue = fail_calloc = 0;
306
+ ev_stat_stop (loop, &watcher);
307
+ #if EV_USE_KQUEUE_STAT
308
+ if (!error && loop->kq_stat)
309
+ {
310
+ int calls = kqueue_calls;
311
+ /* Stale calls must not recreate a registration for a stopped watcher. */
312
+ kq_stat_poll (loop, &watcher, EV_TIMER);
313
+ watcher.prev.st_ino = 0;
314
+ kq_stat_refresh (loop, &watcher);
315
+ if (loop->kq_stat->slotcnt || watcher.wd != -1 || kqueue_calls != calls)
316
+ error = "stopped watcher was re-registered";
317
+ }
318
+ destroy:
319
+ #endif
320
+ ev_loop_destroy (loop);
321
+ if (!error && invalid_closes)
322
+ error = "attempted to close an invalid descriptor";
323
+ if (error)
324
+ rb_raise (rb_eRuntimeError, "%s", error);
325
+ return Qtrue;
326
+ }
327
+
328
+ RUBY_FUNC_EXPORTED void
329
+ Init_coolio_kqueue_native (void)
330
+ {
331
+ VALUE module = rb_define_module ("CoolioKqueueNativeSpec");
332
+ rb_define_singleton_method (module, "check", check, 2);
333
+ }
@@ -91,11 +91,7 @@ test_socket (int af, int type, int protocol)
91
91
  #define CreateFileW test_create_file
92
92
  #define GetFileInformationByHandle test_file_information
93
93
  #define FindFirstFileExW test_find_file
94
- #define EV_STANDALONE 1
95
- #define EV_USE_REALTIME 0
96
- #define EV_USE_MONOTONIC 0
97
- #define EV_WIN32_STAT_CODEPAGE CP_UTF8
98
- #include "ev.c"
94
+ #include "libev.c"
99
95
  #undef socket
100
96
  #undef GetQueuedCompletionStatus
101
97
  #undef CreateFileW
@@ -1284,6 +1280,76 @@ alias_recovery (VALUE self, VALUE path)
1284
1280
  retried ? Qtrue : Qfalse, cached ? Qtrue : Qfalse);
1285
1281
  }
1286
1282
 
1283
+ static VALUE
1284
+ imported_listen_failures (VALUE self, VALUE tries)
1285
+ {
1286
+ enum { limit = 64 };
1287
+ WSADATA data;
1288
+ WSAPROTOCOL_INFOW info;
1289
+ struct sockaddr_in addr;
1290
+ SOCKET sockets[limit * 2], base, imported;
1291
+ const char *failure = 0;
1292
+ int i, error = 0, opened = 0, failures = 0, count = NUM2INT (tries);
1293
+ if (count < 0 || count > limit)
1294
+ {
1295
+ rb_raise (rb_eArgError, "tries must be between 0 and %d", limit);
1296
+ }
1297
+ if (WSAStartup (MAKEWORD (2, 2), &data))
1298
+ {
1299
+ rb_raise (rb_eRuntimeError, "WSAStartup failed");
1300
+ }
1301
+ memset (&addr, 0, sizeof (addr));
1302
+ addr.sin_family = AF_INET;
1303
+ addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK);
1304
+ for (i = 0; i < count; ++i)
1305
+ {
1306
+ base = WSASocketW (AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, WSA_FLAG_OVERLAPPED);
1307
+ if (base == INVALID_SOCKET)
1308
+ {
1309
+ failure = "WSASocketW failed";
1310
+ error = WSAGetLastError ();
1311
+ break;
1312
+ }
1313
+ sockets[opened++] = base;
1314
+ if (bind (base, (struct sockaddr *)&addr, sizeof (addr))
1315
+ || WSADuplicateSocketW (base, GetCurrentProcessId (), &info))
1316
+ {
1317
+ failure = "could not prepare a socket to import";
1318
+ error = WSAGetLastError ();
1319
+ break;
1320
+ }
1321
+ imported = WSASocketW (FROM_PROTOCOL_INFO, FROM_PROTOCOL_INFO, FROM_PROTOCOL_INFO,
1322
+ &info, 0, WSA_FLAG_OVERLAPPED);
1323
+ if (imported == INVALID_SOCKET)
1324
+ {
1325
+ failure = "WSASocketW could not import the socket";
1326
+ error = WSAGetLastError ();
1327
+ break;
1328
+ }
1329
+ sockets[opened++] = imported;
1330
+ if (listen (imported, 1) == SOCKET_ERROR)
1331
+ {
1332
+ error = WSAGetLastError ();
1333
+ if (error != WSAEISCONN)
1334
+ {
1335
+ failure = "listen failed";
1336
+ break;
1337
+ }
1338
+ ++failures;
1339
+ }
1340
+ }
1341
+ while (opened)
1342
+ {
1343
+ closesocket (sockets[--opened]);
1344
+ }
1345
+ WSACleanup ();
1346
+ if (failure)
1347
+ {
1348
+ rb_raise (rb_eRuntimeError, "%s: %d", failure, error);
1349
+ }
1350
+ return INT2NUM (failures);
1351
+ }
1352
+
1287
1353
  RUBY_FUNC_EXPORTED void
1288
1354
  Init_coolio_stat_native (void)
1289
1355
  {
@@ -1302,4 +1368,5 @@ Init_coolio_stat_native (void)
1302
1368
  rb_define_singleton_method (module, "children_cwd", children_cwd, 3);
1303
1369
  rb_define_singleton_method (module, "replaced_dir", replaced_dir, 2);
1304
1370
  rb_define_singleton_method (module, "alias_recovery", alias_recovery, 1);
1371
+ rb_define_singleton_method (module, "imported_listen_failures", imported_listen_failures, 1);
1305
1372
  }
@@ -0,0 +1,103 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ require 'rbconfig'
5
+ require 'timeout'
6
+
7
+ describe 'kqueue stat notification internals', :if => RUBY_PLATFORM =~ /darwin/ do
8
+ def check_native(scenario)
9
+ log = File.join(@build, 'native-output')
10
+ path = File.join(@build, 'watched')
11
+ script = 'CoolioKqueueNativeSpec.check(ARGV[0], ARGV[1])'
12
+ pid = Process.spawn(RbConfig.ruby, '-I', @build, '-rcoolio_kqueue_native',
13
+ '-e', script, scenario, path, out: log, err: [:child, :out])
14
+ begin
15
+ _, status = Timeout.timeout(10) { Process.wait2(pid) }
16
+ rescue Timeout::Error
17
+ Process.kill('KILL', pid)
18
+ Process.wait(pid)
19
+ raise "native stat test timed out: #{scenario}\n#{File.read(log)}"
20
+ end
21
+ expect(status.success?).to eq(true), File.read(log)
22
+ end
23
+
24
+ [
25
+ ['enabled', ['-DEV_USE_KQUEUE=1']],
26
+ ['EV_USE_KQUEUE_STAT=0', ['-DEV_USE_KQUEUE=1', '-DEV_USE_KQUEUE_STAT=0']],
27
+ ['EV_USE_KQUEUE=0', ['-DEV_USE_KQUEUE=0']]
28
+ ].each do |configuration, definitions|
29
+ context configuration do
30
+ before(:context) do
31
+ @build = Dir.mktmpdir('coolio-kqueue-native-')
32
+ source = File.expand_path('fixtures/kqueue_stat_native.c', __dir__)
33
+ coolio = File.expand_path('../ext/cool.io', __dir__)
34
+ FileUtils.cp(source, @build)
35
+ File.write(File.join(@build, 'extconf.rb'), <<~RUBY)
36
+ require 'mkmf'
37
+ $INCFLAGS << ' -I"#{coolio}"'
38
+ $defs.concat(#{definitions.inspect})
39
+ create_makefile('coolio_kqueue_native')
40
+ RUBY
41
+ log = IO.popen([RbConfig.ruby, 'extconf.rb'], chdir: @build, err: [:child, :out], &:read)
42
+ raise log unless $?.success?
43
+ make = ENV['MAKE'] || RbConfig::CONFIG['MAKE'] || 'make'
44
+ log = IO.popen([make], chdir: @build, err: [:child, :out], &:read)
45
+ raise log unless $?.success?
46
+ end
47
+
48
+ after(:context) do
49
+ FileUtils.remove_entry(@build) if @build && File.directory?(@build)
50
+ end
51
+
52
+ if configuration == 'enabled'
53
+ it 'returns after one notification batch under sustained load' do
54
+ check_native('bounded_batch')
55
+ end
56
+
57
+ it 'balances loop references after the internal I/O watcher fails' do
58
+ check_native('io_error')
59
+ end
60
+
61
+ it 'closes a valid kqueue descriptor when its I/O watcher fails' do
62
+ check_native('io_error_valid_fd')
63
+ end
64
+
65
+ it 'takes the stat snapshot after retrying registration' do
66
+ check_native('registration_race')
67
+ end
68
+
69
+ it 'keeps polling through EMFILE and retries native initialization' do
70
+ check_native('init_failure')
71
+ end
72
+
73
+ it 'recovers from repeated post-fork kqueue failures without leaking references' do
74
+ check_native('fork_failure')
75
+ end
76
+
77
+ it 'retries slot allocation only for active watchers' do
78
+ check_native('slot_failure')
79
+ end
80
+
81
+ it 'does not create a kqueue when NOINOTIFY is set' do
82
+ check_native('disabled')
83
+ end
84
+
85
+ it 'preserves reused descriptors when recreating the kqueue after fork' do
86
+ check_native('fork_fd_reuse')
87
+ end
88
+
89
+ it 'preserves reused descriptors when destroying an inherited loop' do
90
+ check_native('destroy_fd_reuse')
91
+ end
92
+
93
+ it 'destroys loop resources without accessing collected watcher storage' do
94
+ check_native('gc_cleanup')
95
+ end
96
+ else
97
+ it 'builds and delivers changes through polling without stat kqueue support' do
98
+ check_native('disabled_build')
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end