farce 0.3.0 → 0.4.1

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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +117 -1489
  4. data/docs/compatibility.md +74 -0
  5. data/docs/concurrency.md +151 -0
  6. data/docs/data-structures.md +313 -0
  7. data/docs/installation.md +63 -0
  8. data/docs/integrations.md +124 -0
  9. data/docs/introduction.md +266 -0
  10. data/docs/scheduling.md +176 -0
  11. data/docs/sharing.md +211 -0
  12. data/docs/shims.md +144 -0
  13. data/docs/utilities.md +93 -0
  14. data/docs/variants-and-scopes.md +92 -0
  15. data/ext/farce/LIBURING-LICENSE +20 -0
  16. data/ext/farce/atom.c +23 -66
  17. data/ext/farce/counter.c +1 -1
  18. data/ext/farce/depend +15 -7
  19. data/ext/farce/drivers.c +44 -2
  20. data/ext/farce/exchanger.c +1 -1
  21. data/ext/farce/extconf.rb +45 -2
  22. data/ext/farce/farce.c +8 -0
  23. data/ext/farce/flag.c +1 -1
  24. data/ext/farce/futex.c +76 -0
  25. data/ext/farce/futex.h +31 -0
  26. data/ext/farce/lock.c +16 -62
  27. data/ext/farce/map.c +10 -54
  28. data/ext/farce/native_wait.c +139 -0
  29. data/ext/farce/native_wait.h +37 -0
  30. data/ext/farce/priority_queue.c +16 -58
  31. data/ext/farce/queue.c +1 -1
  32. data/ext/farce/reactor.c +100 -11
  33. data/ext/farce/reactor.h +6 -2
  34. data/ext/farce/signal.c +17 -64
  35. data/ext/farce/tree_map.c +15 -54
  36. data/ext/farce/unshareable.c +1 -1
  37. data/ext/farce/unsupported.c +13 -1
  38. data/ext/farce/vector.c +13 -60
  39. data/ext/farce/weak_map.c +1 -1
  40. data/lib/farce/abstract/flag.rb +6 -0
  41. data/lib/farce/backports.rb +274 -0
  42. data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
  43. data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
  44. data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
  45. data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
  46. data/lib/farce/engine/ruby/3.4/port.rb +13 -12
  47. data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
  48. data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
  49. data/lib/farce/engine/ruby/4.0/port.rb +1 -2
  50. data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
  51. data/lib/farce/engine/ruby.rb +7 -0
  52. data/lib/farce/engine/shared/null_port.rb +18 -0
  53. data/lib/farce/engine/shared/port.rb +41 -3
  54. data/lib/farce/engine/shared/queue.rb +4 -4
  55. data/lib/farce/engine/shared/ractor_methods.rb +2 -2
  56. data/lib/farce/engine/shared/ractor_selector.rb +73 -0
  57. data/lib/farce/extension.rb +150 -0
  58. data/lib/farce/integrations/ractor_tmvar.rb +4 -1
  59. data/lib/farce/internal/fake_ractor.rb +10 -2
  60. data/lib/farce/internal/scheduler_lifecycle.rb +7 -4
  61. data/lib/farce/null_port.rb +12 -0
  62. data/lib/farce/system.rb +10 -2
  63. data/lib/farce/version.rb +1 -1
  64. data/lib/farce.rb +1 -1
  65. metadata +27 -1
data/ext/farce/map.c CHANGED
@@ -1,16 +1,14 @@
1
1
  #include "containers.h"
2
+ #include "native_wait.h"
2
3
  #include "transaction.h"
3
4
  #include "ruby/fiber/scheduler.h"
4
- #include "ruby/io.h"
5
5
 
6
6
  #include <errno.h>
7
- #include <fcntl.h>
8
7
  #include <math.h>
9
8
  #include <stdint.h>
10
9
  #include <stdatomic.h>
11
10
  #include <stdlib.h>
12
11
  #include <time.h>
13
- #include <unistd.h>
14
12
 
15
13
  #define MAP_INITIAL_CAPACITY 16
16
14
 
@@ -42,9 +40,7 @@ typedef struct {
42
40
  } map_slot_t;
43
41
 
44
42
  typedef struct map_waiter {
45
- int read_fd;
46
- int write_fd;
47
- bool notified;
43
+ containers_waiter_t notification;
48
44
  struct map_waiter *next;
49
45
  } map_waiter_t;
50
46
 
@@ -141,46 +137,12 @@ map_parse_timeout(VALUE timeout)
141
137
  return parsed;
142
138
  }
143
139
 
144
- static void
145
- map_set_fd_flags(int fd)
146
- {
147
- #ifdef _WIN32
148
- (void)fd;
149
- #else
150
- int flags = fcntl(fd, F_GETFL, 0);
151
- if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
152
- flags = fcntl(fd, F_GETFD, 0);
153
- if (flags >= 0) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
154
- #endif
155
- }
156
-
157
- static void
158
- map_waiter_initialize(map_waiter_t *waiter)
159
- {
160
- int descriptors[2];
161
- if (pipe(descriptors) != 0) rb_sys_fail("pipe");
162
- waiter->read_fd = descriptors[0];
163
- waiter->write_fd = descriptors[1];
164
- waiter->notified = false;
165
- waiter->next = NULL;
166
- map_set_fd_flags(waiter->read_fd);
167
- map_set_fd_flags(waiter->write_fd);
168
- }
169
-
170
140
  static void
171
141
  map_notify_waiters_locked(map_t *map)
172
142
  {
173
- unsigned char byte = 1;
174
143
  map->generation++;
175
144
  for (map_waiter_t *waiter = map->waiters; waiter; waiter = waiter->next) {
176
- if (waiter->notified) continue;
177
- ssize_t result;
178
- do {
179
- result = write(waiter->write_fd, &byte, 1);
180
- } while (result < 0 && errno == EINTR);
181
- if (result == 1 || (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
182
- waiter->notified = true;
183
- }
145
+ (void)containers_waiter_notify(&waiter->notification);
184
146
  }
185
147
  }
186
148
 
@@ -260,7 +222,7 @@ map_memsize(const void *pointer)
260
222
  }
261
223
 
262
224
  static const rb_data_type_t map_type = {
263
- .wrap_struct_name = "Ractor::Containers::Map",
225
+ .wrap_struct_name = "Farce::Internal::Map",
264
226
  .function = {
265
227
  .dmark = map_mark,
266
228
  .dfree = map_free,
@@ -430,13 +392,10 @@ static VALUE
430
392
  map_wait_body(VALUE opaque)
431
393
  {
432
394
  map_wait_context_t *context = (map_wait_context_t *)opaque;
433
- VALUE wait_timeout = Qnil;
434
- if (context->timeout->finite) {
435
- double remaining = context->timeout->deadline - map_monotonic_now();
436
- if (remaining <= 0) return Qfalse;
437
- wait_timeout = DBL2NUM(remaining);
438
- }
439
- return containers_wait_for_readable(context->waiter->read_fd, wait_timeout) ? Qtrue : Qfalse;
395
+ return containers_waiter_wait(
396
+ &context->waiter->notification, &context->map->lock,
397
+ context->timeout->finite, context->timeout->deadline, NULL
398
+ ) ? Qtrue : Qfalse;
440
399
  }
441
400
 
442
401
  static VALUE
@@ -451,8 +410,7 @@ map_wait_cleanup(VALUE opaque)
451
410
  pthread_mutex_unlock(&context->map->lock);
452
411
  context->registered = false;
453
412
  }
454
- close(context->waiter->read_fd);
455
- close(context->waiter->write_fd);
413
+ containers_waiter_close(&context->waiter->notification);
456
414
  return Qnil;
457
415
  }
458
416
 
@@ -460,13 +418,11 @@ static bool
460
418
  map_wait_once(map_t *map, uint64_t generation, map_timeout_t *timeout)
461
419
  {
462
420
  map_waiter_t waiter;
463
- map_waiter_initialize(&waiter);
421
+ containers_waiter_init(&waiter.notification);
464
422
 
465
423
  pthread_mutex_lock(&map->lock);
466
424
  if (map->generation != generation) {
467
425
  pthread_mutex_unlock(&map->lock);
468
- close(waiter.read_fd);
469
- close(waiter.write_fd);
470
426
  return true;
471
427
  }
472
428
  waiter.next = map->waiters;
@@ -0,0 +1,139 @@
1
+ #include "native_wait.h"
2
+ #include "ruby/fiber/scheduler.h"
3
+
4
+ #include <errno.h>
5
+ #include <fcntl.h>
6
+ #include <time.h>
7
+ #include <unistd.h>
8
+
9
+ static double
10
+ waiter_monotonic_now(void)
11
+ {
12
+ struct timespec now;
13
+ #ifdef CLOCK_MONOTONIC
14
+ if (clock_gettime(CLOCK_MONOTONIC, &now) == 0) {
15
+ return (double)now.tv_sec + (double)now.tv_nsec / 1000000000.0;
16
+ }
17
+ #endif
18
+ struct timeval fallback;
19
+ gettimeofday(&fallback, NULL);
20
+ return (double)fallback.tv_sec + (double)fallback.tv_usec / 1000000.0;
21
+ }
22
+
23
+ static void
24
+ waiter_set_fd_flags(int fd)
25
+ {
26
+ #ifndef _WIN32
27
+ int flags = fcntl(fd, F_GETFL, 0);
28
+ if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
29
+ flags = fcntl(fd, F_GETFD, 0);
30
+ if (flags >= 0) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
31
+ #endif
32
+ }
33
+
34
+ void
35
+ containers_waiter_init(containers_waiter_t *waiter)
36
+ {
37
+ containers_futex_init(&waiter->futex);
38
+ waiter->read_fd = waiter->write_fd = -1;
39
+ waiter->mode = CONTAINERS_WAITER_PENDING;
40
+ waiter->notified = false;
41
+ }
42
+
43
+ bool
44
+ containers_waiter_notify(containers_waiter_t *waiter)
45
+ {
46
+ if (waiter->notified) return true;
47
+ if (waiter->mode == CONTAINERS_WAITER_FUTEX) {
48
+ waiter->notified = true;
49
+ containers_futex_notify(&waiter->futex);
50
+ return true;
51
+ }
52
+
53
+ /* A pending waiter has not selected its backend yet. Latch its wake so
54
+ * setup can return immediately or a future kernel wait sees a mismatch. */
55
+ atomic_store_explicit(&waiter->futex.value, 1, memory_order_release);
56
+ if (waiter->mode == CONTAINERS_WAITER_PENDING) {
57
+ waiter->notified = true;
58
+ return true;
59
+ }
60
+
61
+ unsigned char byte = 1;
62
+ ssize_t result;
63
+ do {
64
+ result = write(waiter->write_fd, &byte, 1);
65
+ } while (result < 0 && errno == EINTR);
66
+ if (result == 1 || (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
67
+ waiter->notified = true;
68
+ }
69
+ return waiter->notified;
70
+ }
71
+
72
+ bool
73
+ containers_waiter_wait(
74
+ containers_waiter_t *waiter,
75
+ pthread_mutex_t *guard,
76
+ bool finite,
77
+ double deadline,
78
+ int *pipe_error
79
+ )
80
+ {
81
+ if (pipe_error) *pipe_error = 0;
82
+ if (atomic_load_explicit(&waiter->futex.value, memory_order_acquire) == 1) return true;
83
+ if (finite && deadline <= waiter_monotonic_now()) return false;
84
+
85
+ /* Selection can enter Ruby or probe the kernel. The registration stays
86
+ * pending, with no native container mutex held, throughout this work. */
87
+ VALUE scheduler = rb_fiber_scheduler_current();
88
+ /* Blocking futex waits failed the cross-core thread handoff performance
89
+ * gate. Keep Ruby's managed IO path for threads and use futexes only when
90
+ * the owning event loop can complete them asynchronously. */
91
+ bool use_futex = !NIL_P(scheduler) && containers_futex_scheduler_supported(scheduler);
92
+
93
+ int descriptors[2] = {-1, -1};
94
+ if (!use_futex) {
95
+ if (pipe(descriptors) != 0) {
96
+ int error = errno;
97
+ if (atomic_load_explicit(&waiter->futex.value, memory_order_acquire) == 1) return true;
98
+ if (pipe_error) {
99
+ *pipe_error = error;
100
+ return false;
101
+ }
102
+ rb_syserr_fail(error, "pipe");
103
+ }
104
+ waiter_set_fd_flags(descriptors[0]);
105
+ waiter_set_fd_flags(descriptors[1]);
106
+ }
107
+
108
+ /* No Ruby calls occur between creating descriptors and recording ownership.
109
+ * A notifier either latches a pending wake or observes the complete backend. */
110
+ if (guard) pthread_mutex_lock(guard);
111
+ waiter->read_fd = descriptors[0];
112
+ waiter->write_fd = descriptors[1];
113
+ waiter->mode = use_futex ? CONTAINERS_WAITER_FUTEX : CONTAINERS_WAITER_IO;
114
+ bool notified = waiter->notified;
115
+ if (guard) pthread_mutex_unlock(guard);
116
+ if (notified) return true;
117
+
118
+ VALUE timeout = Qnil;
119
+ if (finite) {
120
+ double remaining = deadline - waiter_monotonic_now();
121
+ if (remaining <= 0) {
122
+ return atomic_load_explicit(&waiter->futex.value, memory_order_acquire) == 1;
123
+ }
124
+ timeout = DBL2NUM(remaining);
125
+ }
126
+
127
+ if (use_futex) {
128
+ return containers_futex_scheduler_wait(scheduler, &waiter->futex, timeout);
129
+ }
130
+ return containers_wait_for_readable(waiter->read_fd, timeout);
131
+ }
132
+
133
+ void
134
+ containers_waiter_close(containers_waiter_t *waiter)
135
+ {
136
+ if (waiter->read_fd >= 0) close(waiter->read_fd);
137
+ if (waiter->write_fd >= 0) close(waiter->write_fd);
138
+ waiter->read_fd = waiter->write_fd = -1;
139
+ }
@@ -0,0 +1,37 @@
1
+ #ifndef FARCE_NATIVE_WAIT_H
2
+ #define FARCE_NATIVE_WAIT_H
3
+
4
+ #include "containers.h"
5
+ #include "futex.h"
6
+
7
+ typedef enum {
8
+ CONTAINERS_WAITER_PENDING,
9
+ CONTAINERS_WAITER_FUTEX,
10
+ CONTAINERS_WAITER_IO
11
+ } containers_waiter_mode_t;
12
+
13
+ typedef struct {
14
+ containers_futex_t futex;
15
+ int read_fd, write_fd;
16
+ containers_waiter_mode_t mode;
17
+ bool notified;
18
+ } containers_waiter_t;
19
+
20
+ /* Initialize before linking under the container's guard. Notification is
21
+ * native-only and uses that same guard. A NULL guard denotes owner-local state. */
22
+ void containers_waiter_init(containers_waiter_t *waiter);
23
+ bool containers_waiter_notify(containers_waiter_t *waiter);
24
+ /* Run outside the guard and inside the caller's cleanup ensure. The absolute
25
+ * monotonic deadline includes backend setup. Optional pipe_error lets callers
26
+ * retain their own descriptor-pressure recovery; other errors raise normally. */
27
+ bool containers_waiter_wait(
28
+ containers_waiter_t *waiter,
29
+ pthread_mutex_t *guard,
30
+ bool finite,
31
+ double deadline,
32
+ int *pipe_error
33
+ );
34
+ /* Unlink under the guard first. Waiting has already drained native requests. */
35
+ void containers_waiter_close(containers_waiter_t *waiter);
36
+
37
+ #endif
@@ -1,4 +1,5 @@
1
1
  #include "containers.h"
2
+ #include "native_wait.h"
2
3
  #include "unshared_wait.h"
3
4
  #include <ruby/atomic.h>
4
5
  #include <ruby/fiber/scheduler.h>
@@ -348,9 +349,7 @@ priority_queue_bucket_unlink(
348
349
  }
349
350
 
350
351
  typedef struct priority_queue_lock_waiter {
351
- int read_fd;
352
- int write_fd;
353
- bool notified;
352
+ containers_waiter_t notification;
354
353
  struct priority_queue_lock_waiter *next;
355
354
  } priority_queue_lock_waiter_t;
356
355
 
@@ -753,19 +752,6 @@ priority_queue_get(VALUE self)
753
752
  return queue;
754
753
  }
755
754
 
756
- static void
757
- priority_queue_lock_set_fd_flags(int fd)
758
- {
759
- #ifdef _WIN32
760
- (void)fd;
761
- #else
762
- int flags = fcntl(fd, F_GETFL, 0);
763
- if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
764
- flags = fcntl(fd, F_GETFD, 0);
765
- if (flags >= 0) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
766
- #endif
767
- }
768
-
769
755
  /* Called with queue->lock held. A logical unlock only needs to make one
770
756
  * waiter runnable. The selected waiter remains linked until ensure cleanup. */
771
757
  static void
@@ -777,20 +763,9 @@ priority_queue_lock_notify_one_locked(priority_queue_t *queue)
777
763
  * next waiter. Reentrant operations can register for a later unlock. */
778
764
  farce_unshared_wait_notify_all(&queue->unshared_lock_waiters);
779
765
  }
780
- unsigned char byte = 1;
781
- priority_queue_lock_waiter_t *waiter;
782
-
783
- for (waiter = queue->waiters; waiter; waiter = waiter->next) {
784
- ssize_t result;
785
- if (waiter->notified) continue;
786
- do {
787
- result = write(waiter->write_fd, &byte, 1);
788
- } while (result < 0 && errno == EINTR);
789
- if (result == 1 ||
790
- (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
791
- waiter->notified = true;
792
- return;
793
- }
766
+ for (priority_queue_lock_waiter_t *waiter = queue->waiters; waiter; waiter = waiter->next) {
767
+ if (waiter->notification.notified) continue;
768
+ if (containers_waiter_notify(&waiter->notification)) return;
794
769
  }
795
770
  }
796
771
 
@@ -799,18 +774,16 @@ typedef struct {
799
774
  priority_queue_lock_waiter_t waiter;
800
775
  } priority_queue_lock_wait_context_t;
801
776
 
802
- static bool
803
- priority_queue_lock_wait_for_descriptor(int fd)
804
- {
805
- return containers_wait_for_readable(fd, Qnil);
806
- }
807
-
808
777
  static VALUE
809
778
  priority_queue_lock_wait_body(VALUE opaque)
810
779
  {
811
780
  priority_queue_lock_wait_context_t *context =
812
781
  (priority_queue_lock_wait_context_t *)opaque;
813
- bool awakened = priority_queue_lock_wait_for_descriptor(context->waiter.read_fd);
782
+ bool awakened = containers_waiter_wait(
783
+ &context->waiter.notification,
784
+ context->queue->shared ? &context->queue->lock : NULL,
785
+ false, 0, NULL
786
+ );
814
787
  return awakened ? Qtrue : Qfalse;
815
788
  }
816
789
 
@@ -835,10 +808,10 @@ priority_queue_lock_wait_cleanup(VALUE opaque)
835
808
  }
836
809
  /* A notification only makes this waiter runnable; it does not reserve
837
810
  * ownership. Pass the baton whenever a notified waiter leaves while the
838
- * queue is still ownerless. This covers both interruption inside rb_io_wait
811
+ * queue is still ownerless. This covers both interruption while waiting
839
812
  * and cancellation after a normal wake but before the acquisition loop can
840
813
  * claim ownership, so later waiters cannot be stranded. */
841
- if (context->waiter.notified &&
814
+ if (context->waiter.notification.notified &&
842
815
  NIL_P(context->queue->owner_fiber) &&
843
816
  !priority_queue_publication_busy(
844
817
  RUBY_ATOMIC_LOAD(context->queue->publication_state)
@@ -846,8 +819,7 @@ priority_queue_lock_wait_cleanup(VALUE opaque)
846
819
  priority_queue_lock_notify_one_locked(context->queue);
847
820
  }
848
821
  priority_queue_mutex_unlock(context->queue);
849
- close(context->waiter.read_fd);
850
- close(context->waiter.write_fd);
822
+ containers_waiter_close(&context->waiter.notification);
851
823
  return Qnil;
852
824
  }
853
825
 
@@ -859,25 +831,11 @@ priority_queue_lock_wait_once(priority_queue_t *queue)
859
831
  (void)farce_unshared_wait(&queue->unshared_lock_waiters, queue->unshared_self, false, 0);
860
832
  return;
861
833
  }
862
- int descriptors[2];
863
- priority_queue_lock_wait_context_t context;
864
-
865
- if (pipe(descriptors) != 0) {
866
- priority_queue_mutex_unlock(queue);
867
- rb_sys_fail("pipe");
868
- }
869
- priority_queue_lock_set_fd_flags(descriptors[0]);
870
- priority_queue_lock_set_fd_flags(descriptors[1]);
871
-
872
- context = (priority_queue_lock_wait_context_t){
834
+ priority_queue_lock_wait_context_t context = {
873
835
  .queue = queue,
874
- .waiter = {
875
- .read_fd = descriptors[0],
876
- .write_fd = descriptors[1],
877
- .notified = false,
878
- .next = NULL,
879
- },
836
+ .waiter = {.next = NULL},
880
837
  };
838
+ containers_waiter_init(&context.waiter.notification);
881
839
  if (queue->last_waiter) queue->last_waiter->next = &context.waiter;
882
840
  else queue->waiters = &context.waiter;
883
841
  queue->last_waiter = &context.waiter;
data/ext/farce/queue.c CHANGED
@@ -224,7 +224,7 @@ queue_memsize(const void *pointer)
224
224
  }
225
225
 
226
226
  static const rb_data_type_t queue_type = {
227
- .wrap_struct_name = "Ractor::Containers::Queue",
227
+ .wrap_struct_name = "Farce::Internal::Queue",
228
228
  .function = {
229
229
  .dmark = queue_mark,
230
230
  .dfree = queue_free,
data/ext/farce/reactor.c CHANGED
@@ -345,6 +345,9 @@ void fs_watch_io(fs_state *s, fs_wait *w, int fd, int events, int group, long in
345
345
  }
346
346
  }
347
347
 
348
+ // Cancel the outstanding futex operation before its caller releases the word.
349
+ static void fs_cancel_futex(VALUE self, fs_wait *w);
350
+
348
351
  // Finish a wait and remove its watches and ready-queue entry.
349
352
  // A pending receive must stop using the buffer before cleanup can continue.
350
353
  // Clear Ruby references and keep the fiber's slot available for its next wait.
@@ -352,6 +355,12 @@ VALUE fs_retire(VALUE self, VALUE token) {
352
355
  fs_state *s = fs_get(self); fs_wait *w = fs_lookup(s, token);
353
356
  if (!w) return Qnil;
354
357
  if (w->receive_pending) fs_cancel_receive(self, w);
358
+ if (w->futex_pending) {
359
+ // Logical cancellation must not discard the lookup required by the CQE.
360
+ // Suppress resumption first, then consume the original completion.
361
+ w->status = 2;
362
+ fs_cancel_futex(self, w);
363
+ }
355
364
  ready_remove(s, w);
356
365
  fs_watch *watch = w->watches;
357
366
  while (watch) {
@@ -381,6 +390,56 @@ VALUE fs_retire(VALUE self, VALUE token) {
381
390
  return Qnil;
382
391
  }
383
392
 
393
+ // Select asynchronous futex parking only for an installed, live native ring.
394
+ // External schedulers require a later integration instead of a duck-typed hook.
395
+ bool containers_futex_scheduler_supported(VALUE scheduler) {
396
+ #ifdef FARCE_HAVE_URING_FUTEX
397
+ if (NIL_P(scheduler) || !rb_typeddata_is_kind_of(scheduler, &fs_type)) return false;
398
+ fs_state *s = fs_get(scheduler);
399
+ return s->driver == FS_URING_DRIVER && s->ring_live && s->futex_supported &&
400
+ !s->closed && !s->stopping && s->owner == rb_thread_current() &&
401
+ s->active == rb_fiber_current() && rb_fiber_scheduler_get() == scheduler &&
402
+ containers_futex_available();
403
+ #else
404
+ return false;
405
+ #endif
406
+ }
407
+
408
+ typedef struct {
409
+ VALUE self, token, timeout;
410
+ containers_futex_t *word;
411
+ } fs_futex_wait;
412
+
413
+ // Submit and park using one suspension. The surrounding ensure retains the word
414
+ // until the kernel request has completed, even if timer setup or parking raises.
415
+ static VALUE futex_wait_body(VALUE data) {
416
+ fs_futex_wait *wait = (fs_futex_wait *)data;
417
+ fs_state *s = fs_get(wait->self);
418
+ fs_wait *w = fs_lookup(s, wait->token);
419
+ int error = fs_driver_futex(s, w, wait->word);
420
+ if (error) rb_syserr_fail(error, "scheduler futex submission");
421
+ return rb_funcall(wait->self, rb_intern("wait_with_timeout"), 3,
422
+ wait->token, wait->timeout, Qfalse);
423
+ }
424
+
425
+ static VALUE futex_wait_cleanup(VALUE data) {
426
+ fs_futex_wait *wait = (fs_futex_wait *)data;
427
+ return fs_retire(wait->self, wait->token);
428
+ }
429
+
430
+ bool containers_futex_scheduler_wait(VALUE scheduler, containers_futex_t *word, VALUE timeout) {
431
+ if (!containers_futex_scheduler_supported(scheduler)) return true;
432
+ fs_futex_wait wait = {scheduler, fs_arm(scheduler), timeout, word};
433
+ return RTEST(rb_ensure(futex_wait_body, (VALUE)&wait, futex_wait_cleanup, (VALUE)&wait));
434
+ }
435
+
436
+ // Expose the backend capability privately so native-path validation cannot pass
437
+ // by silently exercising a descriptor fallback.
438
+ static VALUE futex_supported(VALUE self) {
439
+ fs_state *s = fs_get(self);
440
+ return !s->closed && !s->stopping && s->futex_supported && containers_futex_available() ? Qtrue : Qfalse;
441
+ }
442
+
384
443
  // Switch from the root to a ready fiber.
385
444
  // If a newly started child finishes, return control to its waiting parent
386
445
  // before returning to the root dispatcher.
@@ -600,6 +659,27 @@ static int snapshot_fallback(st_data_t key, st_data_t value, st_data_t arg) {
600
659
  // Wake-pipe events ask the dispatcher to return to Ruby policy code.
601
660
  static void accept_event(VALUE self, fs_event *e) {
602
661
  fs_state *s = fs_get(self);
662
+ if (e->generation & FS_FUTEX) {
663
+ fs_wait *w = fs_lookup(s, ULL2NUM(e->generation & ~FS_OPERATION));
664
+ if (!w) return;
665
+ w->futex_pending = 0;
666
+ int error = e->error < 0 ? -e->error : 0;
667
+ if (error == ENOSYS || error == EOPNOTSUPP) s->futex_supported = 0;
668
+ if (!w->status) {
669
+ if (error == ENOSYS || error == EOPNOTSUPP) {
670
+ // Retry with the now-disabled capability, rather than reporting
671
+ // a timeout to consumers that have not reached their deadline.
672
+ fs_resume(self, w->token, Qtrue, Qnil);
673
+ } else {
674
+ // Early notification is reported as EAGAIN. Other interruptions
675
+ // also return to the container's condition/deadline check.
676
+ if (error != EAGAIN && error != EINTR && error != ECANCELED)
677
+ w->native_error = error;
678
+ fs_resume(self, w->token, Qtrue, Qnil);
679
+ }
680
+ }
681
+ return;
682
+ }
603
683
  if (e->generation & FS_RECEIVE) {
604
684
  fs_wait *w = fs_lookup(s, ULL2NUM(e->generation & ~FS_RECEIVE));
605
685
  if (!w) return;
@@ -660,36 +740,44 @@ static void poll_ready(VALUE self) {
660
740
  if (s->poll_error || s->selected_pending) s->policy_pending = 1;
661
741
  }
662
742
 
663
- // Cancel an io_uring receive and wait until the original operation has finished.
664
- // Keep the caller's buffer locked throughout, because a cancellation reply alone
665
- // does not prove the kernel has stopped writing to it.
666
- void fs_cancel_receive(VALUE self, fs_wait *w) {
743
+ // Cancel an operation and consume its original CQE before releasing storage.
744
+ // Cancellation acknowledgements use zero userdata, so a delayed acknowledgement
745
+ // cannot refer to an operation record or a subsequently reused suspension.
746
+ static void cancel_operation(VALUE self, fs_wait *w, uint64_t kind, int *pending) {
667
747
  #ifdef FARCE_URING
668
748
  fs_state *s = fs_get(self);
669
749
  struct io_uring_sqe *entry = io_uring_get_sqe(&s->ring);
670
750
  if (!entry) {
671
751
  int result;
672
752
  do { result = io_uring_submit(&s->ring); } while (result == -EINTR);
673
- if (result < 0) rb_bug("cannot submit pending scheduler receive");
753
+ if (result < 0) rb_bug("cannot submit pending scheduler operation");
674
754
  entry = io_uring_get_sqe(&s->ring);
675
755
  if (!entry) rb_bug("cannot reserve scheduler cancellation");
676
756
  }
677
- io_uring_prep_cancel64(entry, FS_RECEIVE | NUM2ULL(w->token), 0);
757
+ io_uring_prep_cancel64(entry, kind | NUM2ULL(w->token), 0);
678
758
  entry->user_data = 0;
679
- // Wait for the original receive CQE, not just the cancellation CQE. No Ruby
680
- // fiber runs here, and the caller's buffer remains locked throughout.
681
- while (w->receive_pending) {
759
+ // Do not run fibers here. The caller's buffer or futex word stays alive
760
+ // until the original completion, rather than just the cancellation reply.
761
+ while (*pending) {
682
762
  int result = io_uring_submit_and_wait(&s->ring, 1);
683
- if (result < 0 && result != -EINTR) rb_bug("cannot acknowledge scheduler receive cancellation");
763
+ if (result < 0 && result != -EINTR) rb_bug("cannot acknowledge scheduler cancellation");
684
764
  s->timeout = 0;
685
765
  fs_driver_poll(s);
686
- if (s->poll_error && s->poll_error != EINTR) rb_bug("cannot drain scheduler receive cancellation");
766
+ if (s->poll_error && s->poll_error != EINTR) rb_bug("cannot drain scheduler cancellation");
687
767
  for (int i = 0; i < s->event_count; i++) accept_event(self, &s->events[i]);
688
768
  rearm_events(s);
689
769
  }
690
770
  #endif
691
771
  }
692
772
 
773
+ void fs_cancel_receive(VALUE self, fs_wait *w) {
774
+ cancel_operation(self, w, FS_RECEIVE, &w->receive_pending);
775
+ }
776
+
777
+ static void fs_cancel_futex(VALUE self, fs_wait *w) {
778
+ cancel_operation(self, w, FS_FUTEX, &w->futex_pending);
779
+ }
780
+
693
781
  // Call a helper for every active wait, skipping empty slots and idle fibers.
694
782
  static void each_wait(fs_state *s, int (*callback)(st_data_t, st_data_t, st_data_t), st_data_t arg) {
695
783
  for (uint32_t i = 0; i < s->slot_count; i++) {
@@ -836,6 +924,7 @@ void farce_init_fiber_scheduler(VALUE internal) {
836
924
  rb_define_private_method(klass, "retire_wait", fs_retire, 1);
837
925
  rb_define_private_method(klass, "register_select", register_select, 3);
838
926
  rb_define_private_method(klass, "ready?", ready, 0);
927
+ rb_define_private_method(klass, "futex_supported?", futex_supported, 0);
839
928
  rb_define_private_method(klass, "pending?", pending, 0);
840
929
  rb_define_private_method(klass, "pending_count", pending_count, 0);
841
930
  rb_define_private_method(klass, "wakeup", wakeup, 0);
data/ext/farce/reactor.h CHANGED
@@ -1,6 +1,7 @@
1
1
  #ifndef FARCE_FIBER_REACTOR_H
2
2
  #define FARCE_FIBER_REACTOR_H
3
3
  #include "containers.h"
4
+ #include "futex.h"
4
5
  #include <ruby/io.h>
5
6
  #include <ruby/io/buffer.h>
6
7
  #include <ruby/thread.h>
@@ -21,6 +22,8 @@
21
22
  #define FS_WRITE 4
22
23
  #define FS_EVENTS 1024
23
24
  #define FS_RECEIVE (UINT64_C(1) << 63)
25
+ #define FS_FUTEX (UINT64_C(1) << 62)
26
+ #define FS_OPERATION (FS_RECEIVE | FS_FUTEX)
24
27
  #define FS_SLOT_BITS 20
25
28
  #define FS_SLOT_MASK ((1U << FS_SLOT_BITS) - 1)
26
29
 
@@ -45,7 +48,7 @@ struct fs_reg {
45
48
  struct fs_wait {
46
49
  VALUE token, fiber, value, error, groups, ios;
47
50
  uint32_t slot;
48
- int status, events, selected, queued, native_error, receive_pending;
51
+ int status, events, selected, queued, native_error, receive_pending, futex_pending;
49
52
  fs_wait *ready_next, *ready_prev;
50
53
  fs_watch *watches;
51
54
  fs_watch first_watch;
@@ -62,7 +65,7 @@ typedef struct {
62
65
  size_t pending;
63
66
  uint64_t serial, generation, suspensions;
64
67
  unsigned immediate;
65
- int selected_pending, receive_disabled;
68
+ int selected_pending, receive_disabled, futex_supported;
66
69
  int driver, descriptor, wake[2], closed;
67
70
  pthread_mutex_t wake_mutex;
68
71
  #ifdef FARCE_URING
@@ -85,6 +88,7 @@ static inline fs_state *fs_get(VALUE self) {
85
88
  int fs_driver_init(fs_state *s, int driver);
86
89
  int fs_driver_update(fs_state *s, fs_reg *reg, int events);
87
90
  int fs_driver_receive(fs_state *s, fs_wait *w, int fd, void *buffer, size_t size);
91
+ int fs_driver_futex(fs_state *s, fs_wait *w, containers_futex_t *word);
88
92
  void fs_cancel_receive(VALUE self, fs_wait *w);
89
93
  void fs_driver_close(fs_state *s);
90
94
  void *fs_driver_poll(void *data);