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/atom.c CHANGED
@@ -1,14 +1,12 @@
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 <time.h>
11
- #include <unistd.h>
12
10
 
13
11
  static VALUE cAtom;
14
12
  #ifdef RC_HAVE_NATIVE_WEAK_REFERENCES
@@ -24,9 +22,7 @@ bool rb_gc_handle_weak_references_alive_p(VALUE object);
24
22
  typedef struct atom_waiter atom_waiter_t;
25
23
 
26
24
  struct atom_waiter {
27
- int read_fd;
28
- int write_fd;
29
- bool notified;
25
+ containers_waiter_t notification;
30
26
  atom_waiter_t *next;
31
27
  };
32
28
 
@@ -105,35 +101,13 @@ atom_parse_timeout(VALUE timeout)
105
101
  return parsed;
106
102
  }
107
103
 
108
- static void
109
- atom_set_fd_flags(int fd)
110
- {
111
- #ifdef _WIN32
112
- (void)fd;
113
- #else
114
- int flags = fcntl(fd, F_GETFL, 0);
115
- if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
116
- flags = fcntl(fd, F_GETFD, 0);
117
- if (flags >= 0) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
118
- #endif
119
- }
120
-
121
- /* Called with atom->lock held. Every waiter owns a separate descriptor, so a
122
- * single state transition can wake all waiters without consuming a shared
123
- * notification. */
104
+ /* Called with atom->lock held. Every registration owns an independent
105
+ * notification so a transition can wake all waiters. */
124
106
  static void
125
107
  atom_notify_waiters(atom_t *atom)
126
108
  {
127
- unsigned char byte = 1;
128
109
  for (atom_waiter_t *waiter = atom->waiters; waiter; waiter = waiter->next) {
129
- if (waiter->notified) continue;
130
- ssize_t result;
131
- do {
132
- result = write(waiter->write_fd, &byte, 1);
133
- } while (result < 0 && errno == EINTR);
134
- if (result == 1 || (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
135
- waiter->notified = true;
136
- }
110
+ (void)containers_waiter_notify(&waiter->notification);
137
111
  }
138
112
  }
139
113
 
@@ -201,7 +175,7 @@ atom_memsize(const void *pointer)
201
175
  }
202
176
 
203
177
  static const rb_data_type_t atom_type = {
204
- .wrap_struct_name = "Ractor::Containers::Atom",
178
+ .wrap_struct_name = "Farce::Internal::Atom",
205
179
  .function = {
206
180
  .dmark = atom_mark,
207
181
  .dfree = atom_free,
@@ -352,23 +326,24 @@ typedef struct {
352
326
  atom_timeout_t *timeout;
353
327
  } atom_wait_context_t;
354
328
 
355
- static bool
356
- atom_wait_for_descriptor(int fd, atom_timeout_t *timeout)
357
- {
358
- VALUE wait_timeout = Qnil;
359
- if (timeout->finite) {
360
- double remaining = timeout->deadline - atom_monotonic_now();
361
- if (remaining <= 0) return false;
362
- wait_timeout = DBL2NUM(remaining);
363
- }
364
- return containers_wait_for_readable(fd, wait_timeout);
365
- }
366
-
367
329
  static VALUE
368
330
  atom_wait_body(VALUE opaque)
369
331
  {
370
332
  atom_wait_context_t *context = (atom_wait_context_t *)opaque;
371
- return atom_wait_for_descriptor(context->waiter.read_fd, context->timeout) ? Qtrue : Qfalse;
333
+ atom_timeout_t *timeout = context->timeout;
334
+ int error = 0;
335
+ bool awakened = containers_waiter_wait(
336
+ &context->waiter.notification, &context->atom->lock,
337
+ timeout->finite, timeout->deadline, &error
338
+ );
339
+ if (error) {
340
+ double seconds = timeout->finite ? timeout->deadline - atom_monotonic_now() : 0;
341
+ if ((error == EMFILE || error == ENFILE) && timeout->finite && seconds <= 0) return Qfalse;
342
+ VALUE remaining = timeout->finite ? DBL2NUM(seconds) : Qnil;
343
+ containers_wait_without_descriptor(error, "pipe", &timeout->collected, remaining);
344
+ return Qtrue;
345
+ }
346
+ return awakened ? Qtrue : Qfalse;
372
347
  }
373
348
 
374
349
  static VALUE
@@ -385,8 +360,7 @@ atom_wait_cleanup(VALUE opaque)
385
360
  }
386
361
  }
387
362
  pthread_mutex_unlock(&context->atom->lock);
388
- close(context->waiter.read_fd);
389
- close(context->waiter.write_fd);
363
+ containers_waiter_close(&context->waiter.notification);
390
364
  return Qnil;
391
365
  }
392
366
 
@@ -394,29 +368,12 @@ atom_wait_cleanup(VALUE opaque)
394
368
  static bool
395
369
  atom_wait_once(atom_t *atom, atom_timeout_t *timeout)
396
370
  {
397
- int descriptors[2];
398
- if (pipe(descriptors) != 0) {
399
- int error = errno;
400
- pthread_mutex_unlock(&atom->lock);
401
- double seconds = timeout->finite ? timeout->deadline - atom_monotonic_now() : 0;
402
- if ((error == EMFILE || error == ENFILE) && timeout->finite && seconds <= 0) return false;
403
- VALUE remaining = timeout->finite ? DBL2NUM(seconds) : Qnil;
404
- containers_wait_without_descriptor(error, "pipe", &timeout->collected, remaining);
405
- return true;
406
- }
407
- atom_set_fd_flags(descriptors[0]);
408
- atom_set_fd_flags(descriptors[1]);
409
-
410
371
  atom_wait_context_t context = {
411
372
  .atom = atom,
412
- .waiter = {
413
- .read_fd = descriptors[0],
414
- .write_fd = descriptors[1],
415
- .notified = false,
416
- .next = atom->waiters,
417
- },
373
+ .waiter = {.next = atom->waiters},
418
374
  .timeout = timeout,
419
375
  };
376
+ containers_waiter_init(&context.waiter.notification);
420
377
  atom->waiters = &context.waiter;
421
378
  pthread_mutex_unlock(&atom->lock);
422
379
  return RTEST(rb_ensure(atom_wait_body, (VALUE)&context, atom_wait_cleanup, (VALUE)&context));
data/ext/farce/counter.c CHANGED
@@ -55,7 +55,7 @@ RUBY_REFERENCES(counter_references) = {
55
55
  };
56
56
 
57
57
  static const rb_data_type_t counter_type = {
58
- .wrap_struct_name = "Ractor::Containers::Counter",
58
+ .wrap_struct_name = "Farce::Internal::Counter",
59
59
  .function = {
60
60
  .dmark = RUBY_REFS_LIST_PTR(counter_references),
61
61
  .dfree = counter_free,
data/ext/farce/depend CHANGED
@@ -1,12 +1,20 @@
1
1
  bounded_map.o: bounded_map.c containers.h
2
2
  dict.o: dict.c dict.h
3
- farce.o: farce.c containers.h shareable.h
4
- lock.o: lock.c containers.h
5
- priority_queue.o: priority_queue.c containers.h dict.h unshared_wait.h
6
- reactor.o: reactor.c reactor.h
7
- drivers.o: drivers.c reactor.h
8
- io.o: io.c reactor.h
9
- tree_map.o: tree_map.c containers.h
3
+ farce.o: farce.c containers.h shareable.h futex.h
4
+ lock.o: lock.c containers.h native_wait.h futex.h
5
+ priority_queue.o: priority_queue.c containers.h dict.h unshared_wait.h native_wait.h futex.h
6
+ reactor.o: reactor.c reactor.h futex.h
7
+ drivers.o: drivers.c reactor.h futex.h
8
+ io.o: io.c reactor.h futex.h
9
+ tree_map.o: tree_map.c containers.h native_wait.h futex.h
10
10
 
11
11
  queue.o: queue.c containers.h unshared_wait.h
12
12
  unshared_signal.o: unshared_signal.c containers.h unshared_wait.h unshared_io_pool.h
13
+
14
+ atom.o: atom.c containers.h transaction.h native_wait.h futex.h
15
+ map.o: map.c containers.h transaction.h native_wait.h futex.h
16
+ vector.o: vector.c containers.h transaction.h native_wait.h futex.h
17
+ signal.o: signal.c containers.h native_wait.h futex.h
18
+ futex.o: futex.c futex.h
19
+ native_wait.o: native_wait.c native_wait.h containers.h futex.h
20
+ unsupported.o: unsupported.c futex.h
data/ext/farce/drivers.c CHANGED
@@ -5,6 +5,9 @@
5
5
  #include <limits.h>
6
6
  #include <fcntl.h>
7
7
  #include <math.h>
8
+ #ifdef FARCE_HAVE_URING_FUTEX
9
+ #include <linux/futex.h>
10
+ #endif
8
11
  #ifdef HAVE_SYS_EPOLL_H
9
12
  #include <sys/epoll.h>
10
13
  #endif
@@ -12,6 +15,16 @@
12
15
  #include <sys/event.h>
13
16
  #endif
14
17
 
18
+ // Keep compiled backends identifiable when static dependency symbols are hidden.
19
+ #ifdef FARCE_URING
20
+ RUBY_FUNC_EXPORTED const char farce_backend_io_uring[];
21
+ const char farce_backend_io_uring[] = "io_uring";
22
+ #endif
23
+ #ifdef HAVE_SYS_EVENT_H
24
+ RUBY_FUNC_EXPORTED const char farce_backend_kqueue[];
25
+ const char farce_backend_kqueue[] = "kqueue";
26
+ #endif
27
+
15
28
  static struct timespec relative_time(double seconds) {
16
29
  struct timespec t;
17
30
  t.tv_sec = (time_t)seconds;
@@ -38,6 +51,13 @@ int fs_driver_init(fs_state *s, int driver) {
38
51
  int result = io_uring_queue_init(1024, &s->ring, 0);
39
52
  if (result < 0) return -result;
40
53
  s->ring_live = 1;
54
+ #ifdef FARCE_HAVE_URING_FUTEX
55
+ struct io_uring_probe *probe = io_uring_get_probe_ring(&s->ring);
56
+ if (probe) {
57
+ s->futex_supported = io_uring_opcode_supported(probe, IORING_OP_FUTEX_WAIT);
58
+ io_uring_free_probe(probe);
59
+ }
60
+ #endif
41
61
  return 0;
42
62
  }
43
63
  #endif
@@ -79,6 +99,28 @@ int fs_driver_receive(fs_state *s, fs_wait *w, int fd, void *buffer, size_t size
79
99
  #endif
80
100
  }
81
101
 
102
+ // Submit one private 32-bit futex wait. The caller retains the word until the
103
+ // original completion has been consumed, including after timeout or cancellation.
104
+ int fs_driver_futex(fs_state *s, fs_wait *w, containers_futex_t *word) {
105
+ #ifdef FARCE_HAVE_URING_FUTEX
106
+ struct io_uring_sqe *entry = sqe(s);
107
+ if (!entry) return errno;
108
+ io_uring_prep_futex_wait(entry, (uint32_t *)&word->value, 0,
109
+ FUTEX_BITSET_MATCH_ANY, FUTEX_32 | FUTEX_PRIVATE_FLAG, 0);
110
+ entry->user_data = FS_FUTEX | NUM2ULL(w->token);
111
+ w->futex_pending = 1;
112
+ // Submit before yielding. Once queued, cleanup must drain the original CQE
113
+ // even if submission fails after making some progress.
114
+ while (io_uring_sq_ready(&s->ring)) {
115
+ int result = io_uring_submit(&s->ring);
116
+ if (result < 0 && result != -EINTR) return -result;
117
+ }
118
+ return 0;
119
+ #else
120
+ return ENOSYS;
121
+ #endif
122
+ }
123
+
82
124
  int fs_driver_update(fs_state *s, fs_reg *reg, int events) {
83
125
  uint64_t previous = reg->generation;
84
126
  uint64_t generation = ++s->generation;
@@ -210,13 +252,13 @@ void *fs_driver_poll(void *data) {
210
252
  consumed++;
211
253
  if (cqe->user_data && cqe->user_data != LIBURING_UDATA_TIMEOUT) {
212
254
  int e = cqe->res, mask = 0;
213
- if (!(cqe->user_data & FS_RECEIVE) && e >= 0) {
255
+ if (!(cqe->user_data & FS_OPERATION) && e >= 0) {
214
256
  if (e & (POLLIN | POLLHUP | POLLERR)) mask |= FS_READ;
215
257
  if (e & (POLLOUT | POLLHUP | POLLERR)) mask |= FS_WRITE;
216
258
  if (e & POLLPRI) mask |= FS_PRI;
217
259
  }
218
260
  s->events[s->event_count++] = (fs_event){cqe->user_data, mask,
219
- cqe->user_data & FS_RECEIVE ? e : e < 0 && e != -ECANCELED ? -e : 0};
261
+ cqe->user_data & FS_OPERATION ? e : e < 0 && e != -ECANCELED ? -e : 0};
220
262
  }
221
263
  }
222
264
  io_uring_cq_advance(&s->ring, consumed);
@@ -140,7 +140,7 @@ exchanger_memsize(const void *pointer)
140
140
  }
141
141
 
142
142
  static const rb_data_type_t exchanger_type = {
143
- .wrap_struct_name = "Ractor::Containers::Exchanger",
143
+ .wrap_struct_name = "Farce::Internal::Exchanger",
144
144
  .function = {
145
145
  .dmark = NULL,
146
146
  .dfree = exchanger_free,
data/ext/farce/extconf.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
2
4
 
3
5
  require "mkmf"
4
6
  require_relative "../ext_helper"
@@ -44,19 +46,60 @@ end
44
46
 
45
47
  have_func("sysctlbyname", "sys/sysctl.h") if have_header("sys/sysctl.h")
46
48
 
49
+ futex = false
50
+ if enable_config("futex", true) && RUBY_PLATFORM.include?("linux") &&
51
+ have_header("linux/futex.h") && have_header("sys/syscall.h")
52
+ futex = try_compile(<<~SOURCE)
53
+ #include <linux/futex.h>
54
+ #include <sys/syscall.h>
55
+ #include <stdint.h>
56
+ #include <stdatomic.h>
57
+ #include <time.h>
58
+ #include <unistd.h>
59
+
60
+ _Static_assert(sizeof(_Atomic(uint32_t)) == 4, "32-bit futex word");
61
+ _Static_assert(_Alignof(_Atomic(uint32_t)) >= 4, "aligned futex word");
62
+ int main(void) {
63
+ _Atomic(uint32_t) word = 1;
64
+ struct timespec now;
65
+ (void)clock_gettime(CLOCK_MONOTONIC, &now);
66
+ (void)syscall(SYS_futex, &word, FUTEX_WAIT_PRIVATE, 0, 0, 0, 0);
67
+ (void)syscall(SYS_futex, &word, FUTEX_WAKE_PRIVATE, 1, 0, 0, 0);
68
+ return 0;
69
+ }
70
+ SOURCE
71
+ end
72
+ $defs << "-DFARCE_HAVE_FUTEX" if futex
73
+
47
74
  epoll = have_header("sys/epoll.h")
48
75
  kqueue = have_header("sys/event.h")
49
76
  $srcs = %w[
50
- atom.c bounded_map.c counter.c darwin.c dict.c exchanger.c farce.c flag.c lock.c map.c priority_queue.c
51
- queue.c signal.c transaction.c tree_map.c trie.c unshareable.c unshared_signal.c vector.c weak_map.c
77
+ atom.c bounded_map.c counter.c darwin.c dict.c exchanger.c farce.c flag.c futex.c lock.c map.c native_wait.c
78
+ priority_queue.c queue.c signal.c transaction.c tree_map.c trie.c unshareable.c unshared_signal.c vector.c weak_map.c
52
79
  ]
53
80
  $srcs.concat(epoll || kqueue ? %w[drivers.c io.c reactor.c] : ["unsupported.c"])
54
81
 
55
82
  if epoll || kqueue
83
+ dir_config("uring")
56
84
  have_func("epoll_pwait2", "sys/epoll.h") if epoll
57
85
  have_func("rb_io_timeout", "ruby/io.h")
58
86
  if enable_config("io-uring", true) && have_header("liburing.h") && have_library("uring", "io_uring_queue_init")
59
87
  $defs << "-DFARCE_HAVE_LIBURING"
88
+ $DLDFLAGS << " -Wl,--exclude-libs,liburing.a" if enable_config("uring-static", false)
89
+ if futex && try_compile(<<~SOURCE)
90
+ #include <linux/futex.h>
91
+ #include <liburing.h>
92
+
93
+ int main(void) {
94
+ struct io_uring_sqe sqe;
95
+ uint32_t word = 0;
96
+ io_uring_prep_futex_wait(&sqe, &word, 0, FUTEX_BITSET_MATCH_ANY,
97
+ FUTEX_32 | FUTEX_PRIVATE_FLAG, 0);
98
+ return IORING_OP_FUTEX_WAIT == 0;
99
+ }
100
+ SOURCE
101
+ $defs << "-DFARCE_HAVE_URING_FUTEX"
102
+ end
60
103
  end
61
104
  end
62
105
 
data/ext/farce/farce.c CHANGED
@@ -1,4 +1,5 @@
1
1
  #include "containers.h"
2
+ #include "futex.h"
2
3
  #include "transaction.h"
3
4
  #include "shareable.h"
4
5
  #include "ruby/fiber/scheduler.h"
@@ -305,6 +306,20 @@ containers_raise_key_error(VALUE receiver, VALUE key)
305
306
  rb_exc_raise(exception);
306
307
  }
307
308
 
309
+ static VALUE
310
+ farce_value_changed(VALUE namespace, VALUE current, VALUE expected, VALUE identity)
311
+ {
312
+ if (current == expected) return Qfalse;
313
+ if (RTEST(identity)) return identity;
314
+ return RTEST(rb_equal(current, expected)) ? Qfalse : Qtrue;
315
+ }
316
+
317
+ static VALUE
318
+ farce_futex_supported(VALUE namespace)
319
+ {
320
+ return containers_futex_available() ? Qtrue : Qfalse;
321
+ }
322
+
308
323
  static VALUE
309
324
  farce_load_native_fiber_scheduler(VALUE namespace)
310
325
  {
@@ -329,6 +344,7 @@ Init_farce(void)
329
344
  VALUE mInternal = rb_const_get(mFarce, rb_intern("Internal"));
330
345
  VALUE singleton = rb_singleton_class(mInternal);
331
346
  eIsolationError = rb_const_get(rb_cRactor, rb_intern("IsolationError"));
347
+ rb_define_singleton_method(mInternal, "value_changed?", farce_value_changed, 3);
332
348
  #if RUBY_API_VERSION_CODE == 30400
333
349
  rb_define_singleton_method(mInternal, "lock_native_thread", farce_lock_native_thread, 0);
334
350
  #endif
@@ -338,6 +354,7 @@ Init_farce(void)
338
354
  farce_load_native_fiber_scheduler,
339
355
  0
340
356
  );
357
+ rb_define_private_method(singleton, "futex_supported?", farce_futex_supported, 0);
341
358
  containers_init_transaction(mInternal);
342
359
  containers_init_atom(mInternal);
343
360
  containers_init_counter(mInternal);
data/ext/farce/flag.c CHANGED
@@ -49,7 +49,7 @@ RUBY_REFERENCES(flag_references) = {
49
49
  };
50
50
 
51
51
  static const rb_data_type_t flag_type = {
52
- .wrap_struct_name = "Ractor::Containers::Flag",
52
+ .wrap_struct_name = "Farce::Internal::Flag",
53
53
  .function = {
54
54
  .dmark = RUBY_REFS_LIST_PTR(flag_references),
55
55
  .dfree = flag_free,
data/ext/farce/futex.c ADDED
@@ -0,0 +1,76 @@
1
+ #include "futex.h"
2
+
3
+ #include <errno.h>
4
+
5
+ #ifdef FARCE_HAVE_FUTEX
6
+ #include <linux/futex.h>
7
+ #include <pthread.h>
8
+ #include <sys/syscall.h>
9
+ #include <unistd.h>
10
+
11
+ enum { FUTEX_WAITING = 0, FUTEX_NOTIFIED = 1 };
12
+
13
+ static pthread_once_t futex_probe_once = PTHREAD_ONCE_INIT;
14
+ static int futex_probe_error;
15
+ static bool futex_supported;
16
+
17
+ static bool
18
+ futex_unsupported_error(int error)
19
+ {
20
+ return error == ENOSYS || error == EPERM || error == EACCES || error == EOPNOTSUPP;
21
+ }
22
+
23
+ static void
24
+ futex_probe(void)
25
+ {
26
+ containers_futex_t futex;
27
+ atomic_init(&futex.value, FUTEX_NOTIFIED);
28
+ /* The mismatch never sleeps, including on kernels without futex support. */
29
+ long result = syscall(SYS_futex, &futex.value, FUTEX_WAIT_PRIVATE,
30
+ FUTEX_WAITING, NULL, NULL, 0);
31
+ if (result != -1 || errno != EAGAIN) {
32
+ futex_probe_error = result == -1 ? errno : EIO;
33
+ return;
34
+ }
35
+ result = syscall(SYS_futex, &futex.value, FUTEX_WAKE_PRIVATE, 1, NULL, NULL, 0);
36
+ if (result < 0) {
37
+ futex_probe_error = errno;
38
+ return;
39
+ }
40
+ futex_supported = true;
41
+ }
42
+
43
+ #endif
44
+
45
+ bool
46
+ containers_futex_available(void)
47
+ {
48
+ #ifdef FARCE_HAVE_FUTEX
49
+ int error = pthread_once(&futex_probe_once, futex_probe);
50
+ if (error) rb_syserr_fail(error, "pthread_once");
51
+ if (futex_probe_error && !futex_unsupported_error(futex_probe_error)) {
52
+ rb_syserr_fail(futex_probe_error, "futex capability probe");
53
+ }
54
+ return futex_supported;
55
+ #else
56
+ return false;
57
+ #endif
58
+ }
59
+
60
+ void
61
+ containers_futex_init(containers_futex_t *futex)
62
+ {
63
+ atomic_init(&futex->value, 0);
64
+ }
65
+
66
+ void
67
+ containers_futex_notify(containers_futex_t *futex)
68
+ {
69
+ atomic_store_explicit(&futex->value, 1, memory_order_release);
70
+ #ifdef FARCE_HAVE_FUTEX
71
+ int saved_errno = errno;
72
+ while (syscall(SYS_futex, &futex->value, FUTEX_WAKE_PRIVATE,
73
+ 1, NULL, NULL, 0) < 0 && errno == EINTR) {}
74
+ errno = saved_errno;
75
+ #endif
76
+ }
data/ext/farce/futex.h ADDED
@@ -0,0 +1,31 @@
1
+ #ifndef FARCE_FUTEX_H
2
+ #define FARCE_FUTEX_H
3
+
4
+ #include "ruby.h"
5
+
6
+ #include <stdbool.h>
7
+ #include <stdint.h>
8
+ #include <stdatomic.h>
9
+
10
+ /* Each registered waiter owns this word until all native waits have ended. */
11
+ typedef struct {
12
+ _Atomic(uint32_t) value;
13
+ } containers_futex_t;
14
+
15
+ _Static_assert(sizeof(containers_futex_t) == 4, "a futex word must contain exactly 32 bits");
16
+ _Static_assert(_Alignof(containers_futex_t) >= 4, "a futex word must be four-byte aligned");
17
+
18
+ /* Call without native container locks. Unsupported builds and kernels return false. */
19
+ bool containers_futex_available(void);
20
+ /* These two operations are native-only and may run under a container mutex. */
21
+ void containers_futex_init(containers_futex_t *futex);
22
+ void containers_futex_notify(containers_futex_t *futex);
23
+
24
+ /* Scheduler support is opt-in and never blocks an unsupported event loop. */
25
+ bool containers_futex_scheduler_supported(VALUE scheduler);
26
+ /* True requests a condition/deadline recheck after notification or backend
27
+ * rejection. False means the scheduler timer expired. Every caller still
28
+ * rechecks synchronized state and its original absolute deadline. */
29
+ bool containers_futex_scheduler_wait(VALUE scheduler, containers_futex_t *futex, VALUE timeout);
30
+
31
+ #endif
data/ext/farce/lock.c CHANGED
@@ -1,19 +1,13 @@
1
1
  #include "containers.h"
2
+ #include "native_wait.h"
2
3
  #include "ruby/fiber/scheduler.h"
3
- #include "ruby/io.h"
4
-
5
- #include <errno.h>
6
- #include <fcntl.h>
7
- #include <unistd.h>
8
4
 
9
5
  static VALUE cLock;
10
6
 
11
7
  typedef struct containers_lock_waiter containers_lock_waiter_t;
12
8
 
13
9
  struct containers_lock_waiter {
14
- int read_fd;
15
- int write_fd;
16
- bool notified;
10
+ containers_waiter_t notification;
17
11
  containers_lock_waiter_t *next;
18
12
  };
19
13
 
@@ -29,7 +23,6 @@ typedef struct {
29
23
  typedef struct {
30
24
  containers_lock_t *lock;
31
25
  containers_lock_waiter_t waiter;
32
- bool completed;
33
26
  } containers_lock_wait_context_t;
34
27
 
35
28
  typedef struct {
@@ -51,35 +44,14 @@ typedef struct {
51
44
  VALUE opaque;
52
45
  } containers_lock_call_context_t;
53
46
 
54
- static void
55
- containers_lock_set_fd_flags(int fd)
56
- {
57
- #ifdef _WIN32
58
- (void)fd;
59
- #else
60
- int flags = fcntl(fd, F_GETFL, 0);
61
- if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
62
- flags = fcntl(fd, F_GETFD, 0);
63
- if (flags >= 0) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
64
- #endif
65
- }
66
-
67
- /* Called with lock->guard held. Mutex wakeups only need to make one waiter
68
- * runnable; the selected waiter remains linked until its ensure cleanup. */
47
+ /* Called with lock->guard held. The selected waiter remains linked until
48
+ * ensure cleanup, and its notification latches before backend selection. */
69
49
  static void
70
50
  containers_lock_notify_one_locked(containers_lock_t *lock)
71
51
  {
72
- unsigned char byte = 1;
73
52
  for (containers_lock_waiter_t *waiter = lock->waiters; waiter; waiter = waiter->next) {
74
- if (waiter->notified) continue;
75
- ssize_t result;
76
- do {
77
- result = write(waiter->write_fd, &byte, 1);
78
- } while (result < 0 && errno == EINTR);
79
- if (result == 1 || (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
80
- waiter->notified = true;
81
- return;
82
- }
53
+ if (waiter->notification.notified) continue;
54
+ if (containers_waiter_notify(&waiter->notification)) return;
83
55
  }
84
56
  }
85
57
 
@@ -104,7 +76,7 @@ RUBY_REFERENCES(containers_lock_references) = {
104
76
  };
105
77
 
106
78
  static const rb_data_type_t containers_lock_type = {
107
- .wrap_struct_name = "Ractor::Containers::Lock",
79
+ .wrap_struct_name = "Farce::Internal::Lock",
108
80
  .function = {
109
81
  .dmark = RUBY_REFS_LIST_PTR(containers_lock_references),
110
82
  .dfree = containers_lock_free,
@@ -188,19 +160,13 @@ containers_lock_initialize_copy(VALUE self, VALUE other)
188
160
  return containers_publish_native_with_references(self, containers_lock_validate_references);
189
161
  }
190
162
 
191
- static bool
192
- containers_lock_wait_for_descriptor(int fd)
193
- {
194
- return containers_wait_for_readable(fd, Qnil);
195
- }
196
-
197
163
  static VALUE
198
164
  containers_lock_wait_body(VALUE opaque)
199
165
  {
200
166
  containers_lock_wait_context_t *context = (containers_lock_wait_context_t *)opaque;
201
- bool awakened = containers_lock_wait_for_descriptor(context->waiter.read_fd);
202
- context->completed = true;
203
- return awakened ? Qtrue : Qfalse;
167
+ return containers_waiter_wait(
168
+ &context->waiter.notification, &context->lock->guard, false, 0, NULL
169
+ ) ? Qtrue : Qfalse;
204
170
  }
205
171
 
206
172
  static VALUE
@@ -221,12 +187,13 @@ containers_lock_wait_cleanup(VALUE opaque)
221
187
  }
222
188
  previous = *link;
223
189
  }
224
- if (!context->completed && context->waiter.notified && NIL_P(context->lock->owner)) {
190
+ /* A wake does not reserve ownership. Pass the baton even on a normal
191
+ * return, since cancellation can arrive before the caller claims the lock. */
192
+ if (context->waiter.notification.notified && NIL_P(context->lock->owner)) {
225
193
  containers_lock_notify_one_locked(context->lock);
226
194
  }
227
195
  pthread_mutex_unlock(&context->lock->guard);
228
- close(context->waiter.read_fd);
229
- close(context->waiter.write_fd);
196
+ containers_waiter_close(&context->waiter.notification);
230
197
  return Qnil;
231
198
  }
232
199
 
@@ -234,24 +201,11 @@ containers_lock_wait_cleanup(VALUE opaque)
234
201
  static void
235
202
  containers_lock_wait_once(containers_lock_t *lock)
236
203
  {
237
- int descriptors[2];
238
- if (pipe(descriptors) != 0) {
239
- pthread_mutex_unlock(&lock->guard);
240
- rb_sys_fail("pipe");
241
- }
242
- containers_lock_set_fd_flags(descriptors[0]);
243
- containers_lock_set_fd_flags(descriptors[1]);
244
-
245
204
  containers_lock_wait_context_t context = {
246
205
  .lock = lock,
247
- .waiter = {
248
- .read_fd = descriptors[0],
249
- .write_fd = descriptors[1],
250
- .notified = false,
251
- .next = NULL,
252
- },
253
- .completed = false,
206
+ .waiter = {.next = NULL},
254
207
  };
208
+ containers_waiter_init(&context.waiter.notification);
255
209
  if (lock->last_waiter) lock->last_waiter->next = &context.waiter;
256
210
  else lock->waiters = &context.waiter;
257
211
  lock->last_waiter = &context.waiter;