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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yaml +1 -1
- data/.github/workflows/valgrind.yaml +1 -1
- data/.gitignore +0 -3
- data/CHANGES.md +81 -0
- data/Rakefile +4 -29
- data/cool.io.gemspec +7 -1
- data/examples/dslified_echo_client.rb +1 -0
- data/examples/dslified_echo_server.rb +1 -0
- data/examples/echo_client.rb +3 -3
- data/ext/cool.io/buffer.c +0 -23
- data/ext/cool.io/cool.io.h +0 -16
- data/ext/cool.io/ev_wrap.h +7 -0
- data/ext/cool.io/extconf.rb +0 -24
- data/ext/cool.io/iowatcher.c +0 -14
- data/ext/cool.io/stat_watcher.c +14 -8
- data/ext/cool.io/watcher.c +1 -1
- data/ext/libev/ev.c +39 -22
- data/ext/libev/ev_kqueue_stat.c +414 -0
- data/ext/libev/ev_vars.h +4 -0
- data/ext/libev/ev_wrap.h +4 -0
- data/lib/cool.io/version.rb +1 -1
- data/spec/fixtures/kqueue_stat_native.c +333 -0
- data/spec/fixtures/windows_stat_native.c +72 -5
- data/spec/kqueue_stat_native_spec.rb +103 -0
- data/spec/kqueue_stat_watcher_spec.rb +339 -0
- data/spec/stat_watcher_spec.rb +4 -5
- data/spec/timer_watcher_spec.rb +10 -1
- data/spec/windows_stat_native_spec.rb +21 -2
- metadata +14 -19
- data/appveyor.yml +0 -18
- data/examples/google.rb +0 -9
|
@@ -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
|
-
#
|
|
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
|