farce 0.4.0-x86-linux-gnu → 0.5.0-x86-linux-gnu

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -1
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +31 -0
  14. data/docs/variants.md +9 -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 +17 -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 +24 -57
  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 +62 -59
  31. data/ext/farce/queue.c +142 -8
  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 +16 -63
  39. data/ext/farce/weak_map.c +13 -4
  40. data/ext/java/org/farce/PriorityQueue.java +18 -2
  41. data/lib/farce/abstract/concurrent_map.rb +122 -0
  42. data/lib/farce/abstract/counter.rb +35 -1
  43. data/lib/farce/abstract/executor.rb +30 -7
  44. data/lib/farce/abstract/flag.rb +12 -0
  45. data/lib/farce/abstract/pool.rb +158 -47
  46. data/lib/farce/abstract/priority_queue.rb +1 -1
  47. data/lib/farce/abstract/publisher.rb +189 -0
  48. data/lib/farce/abstract/queue.rb +29 -3
  49. data/lib/farce/abstract/scheduler.rb +41 -14
  50. data/lib/farce/abstract/timer_queue.rb +3 -3
  51. data/lib/farce/abstract/vector.rb +3 -1
  52. data/lib/farce/atom.rb +9 -2
  53. data/lib/farce/clock.rb +12 -0
  54. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  55. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  56. data/lib/farce/concurrent/event.rb +60 -0
  57. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  58. data/lib/farce/concurrent/local_var.rb +65 -0
  59. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  60. data/lib/farce/concurrent/tuple.rb +74 -0
  61. data/lib/farce/concurrent.rb +50 -0
  62. data/lib/farce/config.rb +112 -2
  63. data/lib/farce/counter.rb +0 -16
  64. data/lib/farce/engine/jruby/map.rb +8 -6
  65. data/lib/farce/engine/jruby/signal.rb +2 -2
  66. data/lib/farce/engine/jvm/farce.jar +0 -0
  67. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  68. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  69. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  70. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  71. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  72. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  73. data/lib/farce/engine/shared/exchanger.rb +2 -2
  74. data/lib/farce/engine/shared/queue.rb +43 -5
  75. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  76. data/lib/farce/engine/shared/strict_map.rb +5 -0
  77. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  78. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  79. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  80. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  81. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  82. data/lib/farce/engine/truffleruby/map.rb +8 -6
  83. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  84. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  85. data/lib/farce/integrations/concurrent.rb +16 -3
  86. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  87. data/lib/farce/internal/flag_waiting.rb +1 -1
  88. data/lib/farce/internal/lease_initialization.rb +1 -1
  89. data/lib/farce/internal/lease_pool_state.rb +1 -1
  90. data/lib/farce/internal/lease_state.rb +2 -2
  91. data/lib/farce/internal/local_executor_methods.rb +3 -3
  92. data/lib/farce/internal/map_value_modes.rb +9 -2
  93. data/lib/farce/internal/pool_settings.rb +9 -0
  94. data/lib/farce/internal/pool_worker.rb +34 -21
  95. data/lib/farce/internal/reservation_waiting.rb +1 -1
  96. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  97. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  98. data/lib/farce/internal.rb +8 -8
  99. data/lib/farce/lazy.rb +3 -0
  100. data/lib/farce/lazy_ref.rb +3 -0
  101. data/lib/farce/local/lease.rb +2 -9
  102. data/lib/farce/local/lease_map.rb +2 -9
  103. data/lib/farce/local/pool.rb +38 -5
  104. data/lib/farce/local/publisher.rb +30 -0
  105. data/lib/farce/local/scheduler.rb +18 -5
  106. data/lib/farce/local/vector.rb +3 -2
  107. data/lib/farce/memoize.rb +314 -0
  108. data/lib/farce/pool.rb +5 -0
  109. data/lib/farce/priority_queue.rb +1 -1
  110. data/lib/farce/publisher.rb +57 -0
  111. data/lib/farce/signal.rb +1 -1
  112. data/lib/farce/strict/publisher.rb +31 -0
  113. data/lib/farce/strict/vector.rb +3 -2
  114. data/lib/farce/subscription/definitions.rb +27 -0
  115. data/lib/farce/subscription.rb +121 -0
  116. data/lib/farce/transaction/map.rb +35 -1
  117. data/lib/farce/unshared/pool.rb +1 -1
  118. data/lib/farce/unshared/publisher.rb +25 -0
  119. data/lib/farce/unshared/scheduler.rb +3 -3
  120. data/lib/farce/unshared/vector.rb +3 -2
  121. data/lib/farce/vector.rb +12 -9
  122. data/lib/farce/version.rb +1 -1
  123. data/lib/farce/walker/definitions.rb +13 -2
  124. data/lib/farce/weak_atom.rb +5 -2
  125. data/lib/farce.rb +3 -1
  126. metadata +23 -1
data/ext/farce/queue.c CHANGED
@@ -33,6 +33,8 @@ typedef struct {
33
33
  #endif
34
34
  } readiness_signal_t;
35
35
 
36
+ typedef struct queue_wait_context queue_wait_context_t;
37
+
36
38
  typedef struct {
37
39
  pthread_mutex_t lock;
38
40
  VALUE *values;
@@ -44,8 +46,14 @@ typedef struct {
44
46
  size_t tail;
45
47
  size_t pop_waiters;
46
48
  size_t push_waiters;
49
+ VALUE interrupt_token;
50
+ uint64_t interrupt_generation;
51
+ size_t interrupt_pending;
52
+ queue_wait_context_t *interrupt_waiters;
47
53
  readiness_signal_t can_pop;
48
54
  readiness_signal_t can_push;
55
+ readiness_signal_t interrupts_drained;
56
+ size_t interrupt_drain_waiters;
49
57
  farce_unshared_wait_list_t unshared_pop_waiters;
50
58
  farce_unshared_wait_list_t unshared_push_waiters;
51
59
  bool bounded;
@@ -160,20 +168,25 @@ queue_update_readiness(queue_t *queue)
160
168
  }
161
169
  readiness_set(
162
170
  &queue->can_pop,
163
- queue->pop_waiters > 0 && (queue->closed || queue->size > 0)
171
+ queue->pop_waiters > 0 && (queue->closed || queue->size > 0 || queue->interrupt_pending > 0)
164
172
  );
165
173
  readiness_set(
166
174
  &queue->can_push,
167
175
  queue->push_waiters > 0 &&
168
176
  (queue->sealed || !queue->bounded || queue->size < queue->limit)
169
177
  );
178
+ readiness_set(
179
+ &queue->interrupts_drained,
180
+ queue->interrupt_drain_waiters > 0 && (queue->closed || queue->interrupt_pending == 0)
181
+ );
170
182
  if (!queue->shared) {
171
183
  /* Keep the slow path until both waiter mechanisms and every pending
172
184
  * readiness byte have drained. Interrupted direct waits may leave the
173
185
  * flag set, which is harmless and corrected by the next mutation. */
174
186
  queue->unshared_notifications_needed = queue->pop_waiters || queue->push_waiters ||
175
187
  queue->unshared_pop_waiters.count || queue->unshared_push_waiters.count ||
176
- queue->can_pop.set || queue->can_push.set;
188
+ queue->can_pop.set || queue->can_push.set ||
189
+ queue->interrupt_drain_waiters || queue->interrupts_drained.set;
177
190
  }
178
191
  }
179
192
 
@@ -181,6 +194,7 @@ static void
181
194
  queue_mark(void *pointer)
182
195
  {
183
196
  queue_t *queue = pointer;
197
+ rb_gc_mark_movable(queue->interrupt_token);
184
198
  farce_unshared_wait_mark(&queue->unshared_pop_waiters);
185
199
  farce_unshared_wait_mark(&queue->unshared_push_waiters);
186
200
  if (!queue->values) return;
@@ -195,6 +209,7 @@ static void
195
209
  queue_compact(void *pointer)
196
210
  {
197
211
  queue_t *queue = pointer;
212
+ containers_gc_update(&queue->interrupt_token);
198
213
  if (!queue->values) return;
199
214
  size_t index = queue->head;
200
215
  for (size_t remaining = queue->size; remaining > 0; remaining--) {
@@ -212,6 +227,7 @@ queue_free(void *pointer)
212
227
  free(queue->enqueued_at);
213
228
  readiness_close(&queue->can_pop);
214
229
  readiness_close(&queue->can_push);
230
+ readiness_close(&queue->interrupts_drained);
215
231
  ruby_xfree(queue);
216
232
  }
217
233
 
@@ -224,7 +240,7 @@ queue_memsize(const void *pointer)
224
240
  }
225
241
 
226
242
  static const rb_data_type_t queue_type = {
227
- .wrap_struct_name = "Ractor::Containers::Queue",
243
+ .wrap_struct_name = "Farce::Internal::Queue",
228
244
  .function = {
229
245
  .dmark = queue_mark,
230
246
  .dfree = queue_free,
@@ -277,8 +293,14 @@ queue_allocate_type(VALUE klass, bool shared)
277
293
  queue->tail = 0;
278
294
  queue->pop_waiters = 0;
279
295
  queue->push_waiters = 0;
296
+ queue->interrupt_token = Qnil;
297
+ queue->interrupt_generation = 0;
298
+ queue->interrupt_pending = 0;
299
+ queue->interrupt_waiters = NULL;
280
300
  queue->can_pop = (readiness_signal_t){.read_fd = -1, .write_fd = -1, .set = false};
281
301
  queue->can_push = (readiness_signal_t){.read_fd = -1, .write_fd = -1, .set = false};
302
+ queue->interrupts_drained = (readiness_signal_t){.read_fd = -1, .write_fd = -1, .set = false};
303
+ queue->interrupt_drain_waiters = 0;
282
304
  queue->unshared_pop_waiters = (farce_unshared_wait_list_t){0};
283
305
  queue->unshared_push_waiters = (farce_unshared_wait_list_t){0};
284
306
  queue->bounded = true;
@@ -384,6 +406,8 @@ typedef struct {
384
406
  bool finite;
385
407
  bool collected;
386
408
  double deadline;
409
+ bool interruptible;
410
+ VALUE token;
387
411
  } queue_timeout_t;
388
412
 
389
413
  static double
@@ -449,13 +473,44 @@ queue_wait_for_descriptor(int fd, queue_timeout_t *timeout)
449
473
  return containers_wait_for_readable(fd, wait_timeout);
450
474
  }
451
475
 
452
- typedef struct {
476
+ struct queue_wait_context {
453
477
  queue_t *queue;
454
478
  readiness_signal_t *signal;
455
479
  size_t *waiter_count;
456
480
  queue_timeout_t *timeout;
457
481
  int wait_fd;
458
- } queue_wait_context_t;
482
+ bool linked;
483
+ uint64_t generation;
484
+ queue_wait_context_t *previous;
485
+ queue_wait_context_t *next;
486
+ };
487
+
488
+ /* Keep a wakeup readable until every interrupted descriptor waiter has left.
489
+ * Registration and removal happen under the same queue lock as publication. */
490
+ static void
491
+ queue_register_interrupt_waiter(queue_wait_context_t *context)
492
+ {
493
+ if (!context->timeout->interruptible || context->signal != &context->queue->can_pop) return;
494
+ queue_t *queue = context->queue;
495
+ context->generation = queue->interrupt_generation;
496
+ context->previous = NULL;
497
+ context->next = queue->interrupt_waiters;
498
+ if (context->next) context->next->previous = context;
499
+ queue->interrupt_waiters = context;
500
+ context->linked = true;
501
+ }
502
+
503
+ static void
504
+ queue_remove_interrupt_waiter(queue_wait_context_t *context)
505
+ {
506
+ if (!context->linked) return;
507
+ queue_t *queue = context->queue;
508
+ if (context->previous) context->previous->next = context->next;
509
+ else queue->interrupt_waiters = context->next;
510
+ if (context->next) context->next->previous = context->previous;
511
+ if (context->generation != queue->interrupt_generation) queue->interrupt_pending--;
512
+ context->linked = false;
513
+ }
459
514
 
460
515
  static VALUE
461
516
  queue_wait_body(VALUE opaque)
@@ -527,6 +582,7 @@ queue_wait_cleanup(VALUE opaque)
527
582
  queue_wait_context_t *context = (queue_wait_context_t *)opaque;
528
583
  if (context->wait_fd >= 0) close(context->wait_fd);
529
584
  queue_lock(context->queue);
585
+ queue_remove_interrupt_waiter(context);
530
586
  (*context->waiter_count)--;
531
587
  queue_update_readiness(context->queue);
532
588
  if (context->queue->closed && *context->waiter_count == 0) {
@@ -588,8 +644,16 @@ queue_wait(VALUE self, queue_t *queue, readiness_signal_t *signal, size_t *waite
588
644
  queue_unlock(queue);
589
645
  return false;
590
646
  }
647
+ /* A fresh wait must not repeatedly consume readiness reserved for older
648
+ * interrupted fibers on the same scheduler. Wait for their acknowledgments
649
+ * on a separate signal, without joining the interrupted generation. */
650
+ if (signal == &queue->can_pop && queue->interrupt_pending > 0) {
651
+ signal = &queue->interrupts_drained;
652
+ waiter_count = &queue->interrupt_drain_waiters;
653
+ }
591
654
  if (!queue->shared) queue->unshared_notifications_needed = true;
592
- if (!queue->shared && (!queue->unshared_fiber_io || NIL_P(rb_fiber_scheduler_current()))) {
655
+ if (signal != &queue->interrupts_drained && !queue->shared &&
656
+ (!queue->unshared_fiber_io || NIL_P(rb_fiber_scheduler_current()))) {
593
657
  farce_unshared_wait_list_t *list = signal == &queue->can_pop ?
594
658
  &queue->unshared_pop_waiters : &queue->unshared_push_waiters;
595
659
  return farce_unshared_wait(list, self, timeout->finite, timeout->deadline);
@@ -611,6 +675,7 @@ queue_wait(VALUE self, queue_t *queue, readiness_signal_t *signal, size_t *waite
611
675
  .timeout = timeout,
612
676
  .wait_fd = -1,
613
677
  };
678
+ queue_register_interrupt_waiter(&context);
614
679
  (*waiter_count)++;
615
680
  queue_update_readiness(queue);
616
681
  queue_unlock(queue);
@@ -632,6 +697,7 @@ queue_wait(VALUE self, queue_t *queue, readiness_signal_t *signal, size_t *waite
632
697
  .timeout = timeout,
633
698
  .wait_fd = wait_fd,
634
699
  };
700
+ queue_register_interrupt_waiter(&context);
635
701
  (*waiter_count)++;
636
702
  queue_update_readiness(queue);
637
703
  queue_unlock(queue);
@@ -664,7 +730,54 @@ static VALUE
664
730
  queue_capacity(VALUE self)
665
731
  {
666
732
  queue_t *queue = get_queue(self);
667
- return queue->bounded ? SIZET2NUM(queue->limit) : Qnil;
733
+ queue_lock(queue);
734
+ bool bounded = queue->bounded;
735
+ size_t limit = queue->limit;
736
+ queue_unlock(queue);
737
+ return bounded ? SIZET2NUM(limit) : Qnil;
738
+ }
739
+
740
+ static VALUE
741
+ queue_set_capacity(VALUE self, VALUE value)
742
+ {
743
+ size_t limit = 0;
744
+ if (!NIL_P(value)) {
745
+ long long converted = NUM2LL(rb_to_int(value));
746
+ if (converted <= 0) rb_raise(rb_eArgError, "capacity must be positive or nil");
747
+ if ((unsigned long long)converted > SIZE_MAX / sizeof(VALUE)) rb_raise(rb_eArgError, "capacity is too large");
748
+ limit = (size_t)converted;
749
+ }
750
+ queue_t *queue = get_queue(self);
751
+ queue_lock(queue);
752
+ queue->bounded = !NIL_P(value);
753
+ queue->limit = limit;
754
+ queue_update_readiness(queue);
755
+ queue_unlock(queue);
756
+ return value;
757
+ }
758
+
759
+ static VALUE
760
+ queue_interrupt(VALUE self, VALUE token, VALUE generation_value)
761
+ {
762
+ queue_t *queue = get_queue(self);
763
+ if (queue->shared) containers_check_shareable(token);
764
+ uint64_t generation = NUM2ULL(generation_value);
765
+ queue_lock(queue);
766
+ /* Concurrent writers can finish their notifications out of order. */
767
+ if (!NIL_P(queue->interrupt_token) && generation <= queue->interrupt_generation) {
768
+ queue_unlock(queue);
769
+ return self;
770
+ }
771
+ RB_OBJ_WRITE(self, &queue->interrupt_token, token);
772
+ queue->interrupt_generation = generation;
773
+ queue->interrupt_pending = 0;
774
+ for (queue_wait_context_t *waiter = queue->interrupt_waiters; waiter; waiter = waiter->next) {
775
+ if (waiter->generation != generation) queue->interrupt_pending++;
776
+ }
777
+ if (!queue->shared) farce_unshared_wait_notify_all(&queue->unshared_pop_waiters);
778
+ queue_update_readiness(queue);
779
+ queue_unlock(queue);
780
+ return self;
668
781
  }
669
782
 
670
783
  /* Called with queue->lock held. The queue is unchanged if allocation fails.
@@ -719,7 +832,7 @@ queue_num_waiting(VALUE self)
719
832
  queue_t *queue = get_queue(self);
720
833
  size_t num_waiting;
721
834
  queue_lock(queue);
722
- num_waiting = queue->pop_waiters + queue->push_waiters +
835
+ num_waiting = queue->pop_waiters + queue->push_waiters + queue->interrupt_drain_waiters +
723
836
  queue->unshared_pop_waiters.count + queue->unshared_push_waiters.count;
724
837
  queue_unlock(queue);
725
838
  return SIZET2NUM(num_waiting);
@@ -848,6 +961,7 @@ queue_close(VALUE self)
848
961
  queue_update_readiness(queue);
849
962
  if (queue->pop_waiters == 0) readiness_close(&queue->can_pop);
850
963
  if (queue->push_waiters == 0) readiness_close(&queue->can_push);
964
+ if (queue->interrupt_drain_waiters == 0) readiness_close(&queue->interrupts_drained);
851
965
  }
852
966
  queue_unlock(queue);
853
967
  return self;
@@ -862,6 +976,7 @@ queue_release_wait_descriptors(VALUE self)
862
976
  queue_lock(queue);
863
977
  if (queue->pop_waiters == 0) readiness_close(&queue->can_pop);
864
978
  if (queue->push_waiters == 0) readiness_close(&queue->can_push);
979
+ if (queue->interrupt_drain_waiters == 0) readiness_close(&queue->interrupts_drained);
865
980
  queue_unlock(queue);
866
981
  return self;
867
982
  }
@@ -871,6 +986,10 @@ queue_pop_with_timeout(VALUE self, queue_t *queue, queue_timeout_t timeout)
871
986
  {
872
987
  for (;;) {
873
988
  queue_lock(queue);
989
+ if (timeout.interruptible && timeout.token != queue->interrupt_token) {
990
+ queue_unlock(queue);
991
+ return Qfalse;
992
+ }
874
993
  if (queue->closed) {
875
994
  queue_unlock(queue);
876
995
  raise_queue_closed();
@@ -902,6 +1021,18 @@ queue_pop(int argc, VALUE *argv, VALUE self)
902
1021
  return queue_pop_with_timeout(self, queue, timeout);
903
1022
  }
904
1023
 
1024
+ /* Pool dispatch uses positional arguments to avoid another keyword hash. */
1025
+ static VALUE
1026
+ queue_pop_interruptible(VALUE self, VALUE token, VALUE timeout_value)
1027
+ {
1028
+ queue_timeout_t timeout = parse_timeout(timeout_value);
1029
+ timeout.interruptible = true;
1030
+ timeout.token = token;
1031
+ VALUE result = queue_pop_with_timeout(self, get_queue(self), timeout);
1032
+ RB_GC_GUARD(token);
1033
+ return result;
1034
+ }
1035
+
905
1036
  static VALUE
906
1037
  queue_try_pop(VALUE self)
907
1038
  {
@@ -1042,6 +1173,9 @@ containers_init_queue(VALUE namespace)
1042
1173
  rb_define_method(unshared, "fiber_wait", unshared_queue_fiber_wait, 0);
1043
1174
  rb_define_method(cQueue, "initialize", queue_initialize, -1);
1044
1175
  rb_define_method(cQueue, "capacity", queue_capacity, 0);
1176
+ rb_define_method(cQueue, "capacity=", queue_set_capacity, 1);
1177
+ rb_define_method(cQueue, "interrupt", queue_interrupt, 2);
1178
+ rb_define_method(cQueue, "pop_interruptible", queue_pop_interruptible, 2);
1045
1179
  rb_define_method(cQueue, "size", queue_size, 0);
1046
1180
  rb_define_method(cQueue, "num_waiting", queue_num_waiting, 0);
1047
1181
  rb_define_method(cQueue, "clear", queue_clear, 0);
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);
data/ext/farce/signal.c CHANGED
@@ -1,21 +1,16 @@
1
1
  #include "containers.h"
2
- #include "ruby/io.h"
2
+ #include "native_wait.h"
3
3
 
4
- #include <errno.h>
5
- #include <fcntl.h>
6
4
  #include <math.h>
7
5
  #include <stdint.h>
8
6
  #include <time.h>
9
- #include <unistd.h>
10
7
 
11
8
  static VALUE cSignal;
12
9
 
13
10
  typedef struct containers_signal_waiter containers_signal_waiter_t;
14
11
 
15
12
  struct containers_signal_waiter {
16
- int read_fd;
17
- int write_fd;
18
- bool notified;
13
+ containers_waiter_t notification;
19
14
  containers_signal_waiter_t *next;
20
15
  };
21
16
 
@@ -66,34 +61,13 @@ containers_signal_parse_timeout(VALUE value)
66
61
  return timeout;
67
62
  }
68
63
 
69
- static void
70
- containers_signal_set_fd_flags(int fd)
71
- {
72
- #ifdef _WIN32
73
- (void)fd;
74
- #else
75
- int flags = fcntl(fd, F_GETFL, 0);
76
- if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
77
- flags = fcntl(fd, F_GETFD, 0);
78
- if (flags >= 0) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
79
- #endif
80
- }
81
-
82
- /* Called with signal->lock held. Each waiter has a separate descriptor, so a
83
- * single broadcast makes every scheduled Fiber or native thread runnable. */
64
+ /* Called with signal->lock held. The notification remains latched while
65
+ * the waiting caller selects its parking backend outside the state mutex. */
84
66
  static void
85
67
  containers_signal_notify_waiters_locked(containers_signal_t *signal)
86
68
  {
87
- unsigned char byte = 1;
88
69
  for (containers_signal_waiter_t *waiter = signal->waiters; waiter; waiter = waiter->next) {
89
- if (waiter->notified) continue;
90
- ssize_t result;
91
- do {
92
- result = write(waiter->write_fd, &byte, 1);
93
- } while (result < 0 && errno == EINTR);
94
- if (result == 1 || (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
95
- waiter->notified = true;
96
- }
70
+ (void)containers_waiter_notify(&waiter->notification);
97
71
  }
98
72
  }
99
73
 
@@ -112,7 +86,7 @@ containers_signal_memsize(const void *pointer)
112
86
  }
113
87
 
114
88
  static const rb_data_type_t containers_signal_type = {
115
- .wrap_struct_name = "Ractor::Containers::Signal",
89
+ .wrap_struct_name = "Farce::Internal::Signal",
116
90
  .function = {
117
91
  .dmark = NULL,
118
92
  .dfree = containers_signal_free,
@@ -184,26 +158,14 @@ containers_signal_new(void)
184
158
  return containers_signal_initialize(signal);
185
159
  }
186
160
 
187
- static bool
188
- containers_signal_wait_for_descriptor(int fd, containers_signal_timeout_t *timeout)
189
- {
190
- VALUE wait_timeout = Qnil;
191
- if (timeout->finite) {
192
- double remaining = timeout->deadline - containers_signal_monotonic_now();
193
- if (remaining <= 0) return false;
194
- wait_timeout = DBL2NUM(remaining);
195
- }
196
-
197
- return containers_wait_for_readable(fd, wait_timeout);
198
- }
199
-
200
161
  static VALUE
201
162
  containers_signal_wait_body(VALUE opaque)
202
163
  {
203
164
  containers_signal_wait_context_t *context = (containers_signal_wait_context_t *)opaque;
204
- return containers_signal_wait_for_descriptor(context->waiter.read_fd, context->timeout)
205
- ? Qtrue
206
- : Qfalse;
165
+ return containers_waiter_wait(
166
+ &context->waiter.notification, &context->signal->lock,
167
+ context->timeout->finite, context->timeout->deadline, NULL
168
+ ) ? Qtrue : Qfalse;
207
169
  }
208
170
 
209
171
  static VALUE
@@ -220,8 +182,7 @@ containers_signal_wait_cleanup(VALUE opaque)
220
182
  }
221
183
  }
222
184
  pthread_mutex_unlock(&context->signal->lock);
223
- close(context->waiter.read_fd);
224
- close(context->waiter.write_fd);
185
+ containers_waiter_close(&context->waiter.notification);
225
186
  return Qnil;
226
187
  }
227
188
 
@@ -232,24 +193,12 @@ containers_signal_wait_once(
232
193
  containers_signal_timeout_t *timeout
233
194
  )
234
195
  {
235
- int descriptors[2];
236
- if (pipe(descriptors) != 0) {
237
- pthread_mutex_unlock(&signal->lock);
238
- rb_sys_fail("pipe");
239
- }
240
- containers_signal_set_fd_flags(descriptors[0]);
241
- containers_signal_set_fd_flags(descriptors[1]);
242
-
243
196
  containers_signal_wait_context_t context = {
244
197
  .signal = signal,
245
- .waiter = {
246
- .read_fd = descriptors[0],
247
- .write_fd = descriptors[1],
248
- .notified = false,
249
- .next = signal->waiters,
250
- },
198
+ .waiter = {.next = signal->waiters},
251
199
  .timeout = timeout,
252
200
  };
201
+ containers_waiter_init(&context.waiter.notification);
253
202
  signal->waiters = &context.waiter;
254
203
  pthread_mutex_unlock(&signal->lock);
255
204
  return RTEST(rb_ensure(
@@ -330,6 +279,10 @@ containers_signal_wait(int argc, VALUE *argv, VALUE self)
330
279
  pthread_mutex_unlock(&signal->lock);
331
280
  return ULL2NUM(generation);
332
281
  }
282
+ if (timeout.finite && timeout.deadline <= containers_signal_monotonic_now()) {
283
+ pthread_mutex_unlock(&signal->lock);
284
+ return containers_signal_timeout_result();
285
+ }
333
286
  bool awakened = containers_signal_wait_once(signal, &timeout);
334
287
  pthread_mutex_lock(&signal->lock);
335
288
  if (!awakened && signal->generation == observed) {