ractor-sharing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,869 @@
1
+ #include "ruby/ruby.h"
2
+ #include "ruby/util.h"
3
+ #include "ruby/thread_native.h"
4
+ #include "ruby/ractor.h"
5
+
6
+ // quoted (and modified) from "internal/fixnum.h"
7
+
8
+ static inline long
9
+ rb_overflowed_fix_to_int(long x)
10
+ {
11
+ return (long)((unsigned long)(x >> 1) ^ (1LU << (SIZEOF_LONG * CHAR_BIT - 1)));
12
+ }
13
+
14
+ static inline VALUE
15
+ rb_fix_plus_fix(VALUE x, VALUE y)
16
+ {
17
+ #if !HAVE_BUILTIN___BUILTIN_ADD_OVERFLOW
18
+ long lz = FIX2LONG(x) + FIX2LONG(y);
19
+ return LONG2NUM(lz);
20
+ #else
21
+ long lz;
22
+ /* NOTE
23
+ * (1) `LONG2FIX(FIX2LONG(x)+FIX2LONG(y))`
24
+ + = `((lx*2+1)/2 + (ly*2+1)/2)*2+1`
25
+ + = `lx*2 + ly*2 + 1`
26
+ + = `(lx*2+1) + (ly*2+1) - 1`
27
+ + = `x + y - 1`
28
+ * (2) Fixnum's LSB is always 1.
29
+ * It means you can always run `x - 1` without overflow.
30
+ * (3) Of course `z = x + (y-1)` may overflow.
31
+ * At that time true value is
32
+ * * positive: 0b0 1xxx...1, and z = 0b1xxx...1
33
+ * * nevative: 0b1 0xxx...1, and z = 0b0xxx...1
34
+ * To convert this true value to long,
35
+ * (a) Use arithmetic shift
36
+ * * positive: 0b11xxx...
37
+ * * negative: 0b00xxx...
38
+ * (b) invert MSB
39
+ * * positive: 0b01xxx...
40
+ * * negative: 0b10xxx...
41
+ */
42
+ if (__builtin_add_overflow((long)x, (long)y-1, &lz)) {
43
+ return rb_int2big(rb_overflowed_fix_to_int(lz));
44
+ }
45
+ else {
46
+ return (VALUE)lz;
47
+ }
48
+ #endif
49
+ }
50
+
51
+ static inline unsigned int
52
+ rb_popcount32(uint32_t x)
53
+ {
54
+ #if defined(_MSC_VER) && defined(__AVX__)
55
+ /* Note: CPUs since Nehalem and Barcelona have had this instruction so SSE
56
+ * 4.2 should suffice, but it seems there is no such thing like __SSE_4_2__
57
+ * predefined macro in MSVC. They do have __AVX__ so use it instead. */
58
+ return (unsigned int)__popcnt(x);
59
+
60
+ #elif HAVE_BUILTIN___BUILTIN_POPCOUNT
61
+ return (unsigned int)__builtin_popcount(x);
62
+ #else
63
+ x = (x & 0x55555555) + (x >> 1 & 0x55555555);
64
+ x = (x & 0x33333333) + (x >> 2 & 0x33333333);
65
+ x = (x & 0x0f0f0f0f) + (x >> 4 & 0x0f0f0f0f);
66
+ x = (x & 0x001f001f) + (x >> 8 & 0x001f001f);
67
+ x = (x & 0x0000003f) + (x >>16 & 0x0000003f);
68
+ return (unsigned int)x;
69
+ #endif
70
+ }
71
+
72
+ // Thread/Ractor support transactional variable Thread::TVar
73
+
74
+ // 0: null (BUG/only for evaluation)
75
+ // 1: mutex
76
+ // TODO: 1: atomic
77
+ #define SLOT_LOCK_TYPE 1
78
+
79
+ struct slot_lock {
80
+ #if SLOT_LOCK_TYPE == 0
81
+ #elif SLOT_LOCK_TYPE == 1
82
+ rb_nativethread_lock_t lock;
83
+ #else
84
+ #error unknown
85
+ #endif
86
+ };
87
+
88
+ struct tvar_slot {
89
+ uint64_t version;
90
+ VALUE value;
91
+ VALUE index;
92
+ struct slot_lock lock;
93
+ };
94
+
95
+ struct tx_global {
96
+ uint64_t version;
97
+ rb_nativethread_lock_t version_lock;
98
+
99
+ uint64_t slot_index;
100
+ rb_nativethread_lock_t slot_index_lock;
101
+ };
102
+
103
+ struct tx_log {
104
+ VALUE value;
105
+ struct tvar_slot *slot;
106
+ VALUE tvar; // mark slot
107
+ };
108
+
109
+ struct tx_logs {
110
+ uint64_t version;
111
+ uint32_t logs_cnt;
112
+ uint32_t logs_capa;
113
+
114
+ struct tx_log *logs;
115
+
116
+ bool enabled;
117
+ bool stop_adding;
118
+
119
+ uint32_t retry_history;
120
+ size_t retry_on_commit;
121
+ size_t retry_on_read_lock;
122
+ size_t retry_on_read_version;
123
+ };
124
+
125
+ static struct tx_global tx_global;
126
+
127
+ static VALUE rb_eTxRetry;
128
+ static VALUE rb_eTxError;
129
+ static VALUE rb_exc_tx_retry;
130
+ static VALUE rb_cRactorTVar;
131
+ static VALUE rb_cRactorTxLogs;
132
+
133
+ static ID id_tx_logs;
134
+
135
+ static struct tx_global *
136
+ tx_global_ptr(void)
137
+ {
138
+ return &tx_global;
139
+ }
140
+
141
+ #define TVAR_DEBUG_LOG(...)
142
+
143
+ static VALUE
144
+ txg_next_index(struct tx_global *txg)
145
+ {
146
+ VALUE index;
147
+ rb_native_mutex_lock(&txg->slot_index_lock);
148
+ {
149
+ txg->slot_index++;
150
+ index = INT2FIX(txg->slot_index);
151
+ }
152
+ rb_native_mutex_unlock(&txg->slot_index_lock);
153
+
154
+ return index;
155
+ }
156
+
157
+ static uint64_t
158
+ txg_version(const struct tx_global *txg)
159
+ {
160
+ uint64_t version;
161
+ version = txg->version;
162
+ return version;
163
+ }
164
+
165
+ static uint64_t
166
+ txg_next_version(struct tx_global *txg)
167
+ {
168
+ uint64_t version;
169
+
170
+ rb_native_mutex_lock(&txg->version_lock);
171
+ {
172
+ txg->version++;
173
+ version = txg->version;
174
+ TVAR_DEBUG_LOG("new_version:%lu", version);
175
+ }
176
+ rb_native_mutex_unlock(&txg->version_lock);
177
+
178
+ return version;
179
+ }
180
+
181
+ // tx: transaction
182
+
183
+ static void
184
+ tx_slot_lock_init(struct slot_lock *lock)
185
+ {
186
+ #if SLOT_LOCK_TYPE == 0
187
+ #elif SLOT_LOCK_TYPE == 1
188
+ rb_native_mutex_initialize(&lock->lock);
189
+ #else
190
+ #error unknown
191
+ #endif
192
+ }
193
+
194
+ static void
195
+ tx_slot_lock_free(struct slot_lock *lock)
196
+ {
197
+ #if SLOT_LOCK_TYPE == 0
198
+ #elif SLOT_LOCK_TYPE == 1
199
+ rb_native_mutex_destroy(&lock->lock);
200
+ #else
201
+ #error unknown
202
+ #endif
203
+ }
204
+
205
+ static bool
206
+ tx_slot_lock_trylock(struct slot_lock *lock)
207
+ {
208
+ #if SLOT_LOCK_TYPE == 0
209
+ return true;
210
+ #elif SLOT_LOCK_TYPE == 1
211
+ return rb_native_mutex_trylock(&lock->lock) == 0;
212
+ #else
213
+ #error unknown
214
+ #endif
215
+ }
216
+
217
+ static void
218
+ tx_slot_lock_lock(struct slot_lock *lock)
219
+ {
220
+ #if SLOT_LOCK_TYPE == 0
221
+ #elif SLOT_LOCK_TYPE == 1
222
+ rb_native_mutex_lock(&lock->lock);
223
+ #else
224
+ #error unknown
225
+ #endif
226
+ }
227
+
228
+ static void
229
+ tx_slot_lock_unlock(struct slot_lock *lock)
230
+ {
231
+ #if SLOT_LOCK_TYPE == 0
232
+ #elif SLOT_LOCK_TYPE == 1
233
+ rb_native_mutex_unlock(&lock->lock);
234
+ #else
235
+ #error unknown
236
+ #endif
237
+ }
238
+
239
+ static bool
240
+ tx_slot_trylock(struct tvar_slot *slot)
241
+ {
242
+ return tx_slot_lock_trylock(&slot->lock);
243
+ }
244
+
245
+ static void
246
+ tx_slot_lock(struct tvar_slot *slot)
247
+ {
248
+ tx_slot_lock_lock(&slot->lock);
249
+ }
250
+
251
+ static void
252
+ tx_slot_unlock(struct tvar_slot *slot)
253
+ {
254
+ tx_slot_lock_unlock(&slot->lock);
255
+ }
256
+
257
+
258
+ static void
259
+ tx_mark(void *ptr)
260
+ {
261
+ struct tx_logs *tx = (struct tx_logs *)ptr;
262
+
263
+ for (uint32_t i=0; i<tx->logs_cnt; i++) {
264
+ rb_gc_mark(tx->logs[i].value);
265
+ /* The slot is plain memory owned by the TVar: unmarked, a TVar reachable
266
+ * only from a transaction in flight is collected and the commit locks a
267
+ * freed mutex. */
268
+ rb_gc_mark(tx->logs[i].tvar);
269
+ }
270
+ }
271
+
272
+ static void
273
+ tx_free(void *ptr)
274
+ {
275
+ struct tx_logs *tx = (struct tx_logs *)ptr;
276
+
277
+ TVAR_DEBUG_LOG("retry %5lu commit:%lu read_lock:%lu read_version:%lu",
278
+ tx->retry_on_commit + tx->retry_on_read_lock + tx->retry_on_read_version,
279
+ tx->retry_on_commit,
280
+ tx->retry_on_read_lock,
281
+ tx->retry_on_read_version);
282
+
283
+ ruby_xfree(tx->logs);
284
+ ruby_xfree(tx);
285
+ }
286
+
287
+ static size_t
288
+ tx_memsize(const void *ptr)
289
+ {
290
+ const struct tx_logs *tx = (const struct tx_logs *)ptr;
291
+ return sizeof(struct tx_logs) + (size_t)tx->logs_capa * sizeof(struct tx_log);
292
+ }
293
+
294
+ static const rb_data_type_t txlogs_type = {
295
+ "txlogs",
296
+ {tx_mark, tx_free, tx_memsize, NULL},
297
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
298
+ };
299
+
300
+ static VALUE
301
+ tx_logs_obj(void)
302
+ {
303
+ VALUE cth = rb_thread_current();
304
+ VALUE txobj = rb_thread_local_aref(cth, id_tx_logs);
305
+
306
+ if (txobj == Qnil) {
307
+ struct tx_logs *tx;
308
+ txobj = TypedData_Make_Struct(rb_cRactorTxLogs, struct tx_logs, &txlogs_type, tx);
309
+ tx->logs_capa = 0x10; // default
310
+ tx->logs = ALLOC_N(struct tx_log, tx->logs_capa);
311
+ rb_thread_local_aset(cth, id_tx_logs, txobj);
312
+ }
313
+ else {
314
+ struct tx_logs *tx;
315
+ /* Thread.current[:...] is writable from Ruby, so this has to be checked;
316
+ * DATA_PTR would take an Object.new and dereference it. */
317
+ TypedData_Get_Struct(txobj, struct tx_logs, &txlogs_type, tx);
318
+ (void)tx;
319
+ }
320
+ return txobj;
321
+ }
322
+
323
+ static struct tx_logs *
324
+ tx_logs(void)
325
+ {
326
+ struct tx_logs *tx;
327
+ TypedData_Get_Struct(tx_logs_obj(), struct tx_logs, &txlogs_type, tx);
328
+ return tx;
329
+ }
330
+
331
+ static struct tx_log *
332
+ tx_lookup(struct tx_logs *tx, VALUE tvar)
333
+ {
334
+ struct tx_log *copies = tx->logs;
335
+ uint32_t cnt = tx->logs_cnt;
336
+
337
+ for (uint32_t i = 0; i< cnt; i++) {
338
+ if (copies[i].tvar == tvar) {
339
+ return &copies[i];
340
+ }
341
+ }
342
+
343
+ return NULL;
344
+ }
345
+
346
+ static void
347
+ tx_add(struct tx_logs *tx, VALUE val, struct tvar_slot *slot, VALUE tvar)
348
+ {
349
+ if (RB_UNLIKELY(tx->logs_capa == tx->logs_cnt)) {
350
+ uint32_t new_capa = tx->logs_capa * 2;
351
+ RB_REALLOC_N(tx->logs, struct tx_log, new_capa);
352
+ tx->logs_capa = new_capa;
353
+ }
354
+ if (RB_UNLIKELY(tx->stop_adding)) {
355
+ rb_raise(rb_eTxError, "can not handle more transactional variable: %"PRIxVALUE, rb_inspect(tvar));
356
+ }
357
+ struct tx_log *log = &tx->logs[tx->logs_cnt++];
358
+
359
+ log->value = val;
360
+ log->slot = slot;
361
+ log->tvar = tvar;
362
+ }
363
+
364
+ static VALUE
365
+ tx_get(struct tx_logs *tx, struct tvar_slot *slot, VALUE tvar)
366
+ {
367
+ struct tx_log *ent = tx_lookup(tx, tvar);
368
+
369
+ if (ent == NULL) {
370
+ VALUE val;
371
+
372
+ if (tx_slot_trylock(slot)) {
373
+ if (slot->version > tx->version) {
374
+ TVAR_DEBUG_LOG("RV < slot->V slot:%u slot->version:%lu, tx->version:%lu", FIX2INT(slot->index), slot->version, tx->version);
375
+ tx_slot_unlock(slot);
376
+ tx->retry_on_read_version++;
377
+ goto abort_and_retry;
378
+ }
379
+ val = slot->value;
380
+ tx_slot_unlock(slot);
381
+ }
382
+ else {
383
+ TVAR_DEBUG_LOG("RV < slot->V slot:%u slot->version:%lu, tx->version:%lu", FIX2INT(slot->index), slot->version, tx->version);
384
+ tx->retry_on_read_lock++;
385
+ goto abort_and_retry;
386
+ }
387
+ tx_add(tx, val, slot, tvar);
388
+ return val;
389
+
390
+ abort_and_retry:
391
+ rb_raise(rb_eTxRetry, "retry");
392
+ }
393
+ else {
394
+ return ent->value;
395
+ }
396
+ }
397
+
398
+ static void
399
+ tx_set(struct tx_logs *tx, VALUE val, struct tvar_slot *slot, VALUE tvar)
400
+ {
401
+ struct tx_log *ent = tx_lookup(tx, tvar);
402
+
403
+ if (ent == NULL) {
404
+ tx_add(tx, val, slot, tvar);
405
+ }
406
+ else {
407
+ ent->value = val;
408
+ }
409
+ }
410
+
411
+ static void
412
+ tx_check(struct tx_logs *tx)
413
+ {
414
+ if (RB_UNLIKELY(!tx->enabled)) {
415
+ rb_raise(rb_eTxError, "can not set without transaction");
416
+ }
417
+ }
418
+
419
+ static void
420
+ tx_setup(struct tx_global *txg, struct tx_logs *tx)
421
+ {
422
+ RUBY_ASSERT(tx->enabled);
423
+ RUBY_ASSERT(tx->logs_cnt == 0);
424
+
425
+ tx->version = txg_version(txg);
426
+
427
+ TVAR_DEBUG_LOG("tx:%lu", tx->version);
428
+ }
429
+
430
+ static struct tx_logs *
431
+ tx_begin(void)
432
+ {
433
+ struct tx_global *txg = tx_global_ptr();
434
+ struct tx_logs *tx = tx_logs();
435
+
436
+ RUBY_ASSERT(tx->stop_adding == false);
437
+ RUBY_ASSERT(tx->logs_cnt == 0);
438
+
439
+ if (tx->enabled == false) {
440
+ tx->enabled = true;
441
+ tx_setup(txg, tx);
442
+ return tx;
443
+ }
444
+ else {
445
+ return NULL;
446
+ }
447
+ }
448
+
449
+ static VALUE
450
+ tx_reset(struct tx_logs *tx)
451
+ {
452
+ struct tx_global *txg = tx_global_ptr();
453
+ tx->logs_cnt = 0;
454
+
455
+ // contention management (CM): back off by 1us per retry seen in the last
456
+ // 32 attempts. This had been dead twice over -- the popcount result was
457
+ // discarded by a split statement, and nothing ever set a bit in
458
+ // retry_history -- so the sleep below had never once run.
459
+ if (tx->retry_history != 0) {
460
+ int recent_retries = rb_popcount32(tx->retry_history);
461
+ TVAR_DEBUG_LOG("retry recent_retries:%d", recent_retries);
462
+
463
+ /* Spin, not sleep: the shortest rb_thread_wait_for actually sleeps is
464
+ * ~55us here (timer slack), 180x a transaction. 100ns per consecutive
465
+ * loss, 3.2us at most, runs no Ruby and checks no interrupts. */
466
+ {
467
+ struct timespec t0, t;
468
+ uint64_t budget = (uint64_t)recent_retries * 100;
469
+
470
+ clock_gettime(CLOCK_MONOTONIC, &t0);
471
+ do {
472
+ clock_gettime(CLOCK_MONOTONIC, &t);
473
+ } while ((uint64_t)(t.tv_sec - t0.tv_sec) * 1000000000 + (t.tv_nsec - t0.tv_nsec) < budget);
474
+ }
475
+ }
476
+
477
+ // Record this retry after the check above, so a lone retry in a calm
478
+ // window pays nothing and only consecutive ones back off. A commit ages
479
+ // the window with the shift in tx_commit.
480
+ tx->retry_history = (tx->retry_history << 1) | 1;
481
+
482
+ tx_setup(txg, tx);
483
+ TVAR_DEBUG_LOG("tx:%lu", tx->version);
484
+
485
+ return Qnil;
486
+ }
487
+
488
+ static VALUE
489
+ tx_end(struct tx_logs *tx)
490
+ {
491
+ TVAR_DEBUG_LOG("tx:%lu", tx->version);
492
+
493
+ RUBY_ASSERT(tx->enabled);
494
+ RUBY_ASSERT(tx->stop_adding == false);
495
+ tx->enabled = false;
496
+ tx->logs_cnt = 0;
497
+ return Qnil;
498
+ }
499
+
500
+ static void
501
+ tx_commit_release(struct tx_logs *tx, uint32_t n)
502
+ {
503
+ struct tx_log *copies = tx->logs;
504
+
505
+ for (uint32_t i = 0; i<n; i++) {
506
+ struct tx_log *copy = &copies[i];
507
+ struct tvar_slot *slot = copy->slot;
508
+ tx_slot_unlock(slot);
509
+ }
510
+ }
511
+
512
+ static VALUE
513
+ tx_commit(struct tx_logs *tx)
514
+ {
515
+ struct tx_global *txg = tx_global_ptr();
516
+ uint32_t i;
517
+ struct tx_log *copies = tx->logs;
518
+ uint32_t logs_cnt = tx->logs_cnt;
519
+
520
+ for (i=0; i<logs_cnt; i++) {
521
+ struct tx_log *copy = &copies[i];
522
+ struct tvar_slot *slot = copy->slot;
523
+
524
+ if (RB_LIKELY(tx_slot_trylock(slot))) {
525
+ if (RB_UNLIKELY(slot->version > tx->version)) {
526
+ TVAR_DEBUG_LOG("RV < slot->V slot:%lu tx:%lu rs:%lu", slot->version, tx->version, txg->version);
527
+ tx_commit_release(tx, i+1);
528
+ goto abort_and_retry;
529
+ }
530
+ else {
531
+ // lock success
532
+ TVAR_DEBUG_LOG("lock slot:%lu tx:%lu rs:%lu", slot->version, tx->version, txg->version);
533
+ }
534
+ }
535
+ else {
536
+ TVAR_DEBUG_LOG("trylock fail slot:%lu tx:%lu rs:%lu", slot->version, tx->version, txg->version);
537
+ tx_commit_release(tx, i);
538
+ goto abort_and_retry;
539
+ }
540
+ }
541
+
542
+ // ok
543
+ /* A success ends the losing streak, so the window counts consecutive
544
+ * losses: an occasional loser pays nothing, a storm backs off by its own
545
+ * length. (Shift-decay measured the same in the storm and no better in
546
+ * the occasional case.) */
547
+ tx->retry_history = 0;
548
+
549
+ uint64_t new_version = txg_next_version(txg);
550
+
551
+ for (i=0; i<logs_cnt; i++) {
552
+ struct tx_log *copy = &copies[i];
553
+ struct tvar_slot *slot = copy->slot;
554
+
555
+ if (slot->value != copy->value) {
556
+ TVAR_DEBUG_LOG("write slot:%d %d->%d slot->version:%lu->%lu tx:%lu rs:%lu",
557
+ FIX2INT(slot->index), FIX2INT(slot->value), FIX2INT(copy->value),
558
+ slot->version, new_version, tx->version, txg->version);
559
+
560
+ slot->version = new_version;
561
+ slot->value = copy->value;
562
+ }
563
+ }
564
+
565
+ tx_commit_release(tx, logs_cnt);
566
+
567
+ return Qtrue;
568
+
569
+ abort_and_retry:
570
+ tx->retry_on_commit++;
571
+
572
+ return Qfalse;
573
+ }
574
+
575
+ // tvar
576
+
577
+ static void
578
+ tvar_mark(void *ptr)
579
+ {
580
+ struct tvar_slot *slot = (struct tvar_slot *)ptr;
581
+ rb_gc_mark(slot->value);
582
+ }
583
+
584
+ static void
585
+ tvar_free(void *ptr)
586
+ {
587
+ struct tvar_slot *slot = (struct tvar_slot *)ptr;
588
+ tx_slot_lock_free(&slot->lock);
589
+ ruby_xfree(slot);
590
+ }
591
+
592
+ static size_t
593
+ tvar_memsize(const void *ptr)
594
+ {
595
+ return sizeof(struct tvar_slot);
596
+ }
597
+
598
+ static const rb_data_type_t tvar_data_type = {
599
+ "Thread::TVar",
600
+ {tvar_mark, tvar_free, tvar_memsize, NULL},
601
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE
602
+ };
603
+
604
+ static struct tvar_slot *
605
+ tvar_ptr(VALUE self)
606
+ {
607
+ struct tvar_slot *slot;
608
+ TypedData_Get_Struct(self, struct tvar_slot, &tvar_data_type, slot);
609
+ if (RB_UNLIKELY(slot == NULL)) {
610
+ rb_raise(rb_eTypeError, "uninitialized %"PRIsVALUE, rb_obj_class(self));
611
+ }
612
+ return slot;
613
+ }
614
+
615
+ static VALUE
616
+ tvar_new_(VALUE self, VALUE init)
617
+ {
618
+ // init should be shareable
619
+ if (RB_UNLIKELY(!rb_ractor_shareable_p(init))) {
620
+ rb_raise(rb_eArgError, "only shareable object are allowed");
621
+ }
622
+
623
+ struct tx_global *txg = tx_global_ptr();
624
+ struct tvar_slot *slot;
625
+ VALUE obj = TypedData_Make_Struct(rb_cRactorTVar, struct tvar_slot, &tvar_data_type, slot);
626
+ slot->version = 0;
627
+ slot->value = init;
628
+ slot->index = txg_next_index(txg);
629
+ tx_slot_lock_init(&slot->lock);
630
+
631
+ /* Only the slot changes, and it is not a Ruby object, so the TVar itself can
632
+ * be frozen and shared. Setting the flag by hand instead left it shareable
633
+ * but not frozen, which let the main Ractor go on attaching ivars to an
634
+ * object other Ractors were holding. */
635
+ rb_obj_freeze(obj);
636
+ rb_ractor_make_shareable(obj);
637
+
638
+ return obj;
639
+ }
640
+
641
+ static VALUE
642
+ tvar_new(int argc, VALUE *argv, VALUE self)
643
+ {
644
+ VALUE init = Qnil;
645
+ rb_scan_args(argc, argv, "01", &init);
646
+ return tvar_new_(self, init);
647
+ }
648
+
649
+ static VALUE
650
+ tvar_value(VALUE self)
651
+ {
652
+ struct tx_logs *tx = tx_logs();
653
+ struct tvar_slot *slot = tvar_ptr(self);
654
+
655
+ if (tx->enabled) {
656
+ return tx_get(tx, slot, self);
657
+ }
658
+ else {
659
+ // TODO: warn on multi-ractors?
660
+ return slot->value;
661
+ }
662
+ }
663
+
664
+ static VALUE
665
+ tvar_value_set(VALUE self, VALUE val)
666
+ {
667
+ if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) {
668
+ rb_raise(rb_eArgError, "only shareable object are allowed");
669
+ }
670
+
671
+ struct tx_logs *tx = tx_logs();
672
+ tx_check(tx);
673
+ struct tvar_slot *slot = tvar_ptr(self);
674
+ tx_set(tx, val, slot, self);
675
+ return val;
676
+ }
677
+
678
+ /* The sum only when it is still a Fixnum: rb_fix_plus_fix allocates a Bignum
679
+ * when it is not, and this runs with the slot's native mutex held. */
680
+ static VALUE
681
+ tvar_calc_inc(VALUE v, VALUE inc)
682
+ {
683
+ if (RB_LIKELY(FIXNUM_P(v) && FIXNUM_P(inc))) {
684
+ long x = FIX2LONG(v), y = FIX2LONG(inc);
685
+
686
+ if (y > 0 ? x > FIXNUM_MAX - y : x < FIXNUM_MIN - y) return Qundef;
687
+ return LONG2FIX(x + y);
688
+ }
689
+ else {
690
+ return Qundef;
691
+ }
692
+ }
693
+
694
+ static VALUE
695
+ tvar_value_increment_(VALUE self, VALUE inc)
696
+ {
697
+ struct tx_global *txg = tx_global_ptr();
698
+ struct tx_logs *tx = tx_logs();
699
+ VALUE recv, ret;
700
+ struct tvar_slot *slot = tvar_ptr(self);
701
+
702
+ if (!tx->enabled) {
703
+ tx_slot_lock(slot);
704
+ {
705
+ uint64_t new_version = txg_next_version(txg);
706
+ recv = slot->value;
707
+ ret = tvar_calc_inc(recv, inc);
708
+
709
+ if (RB_LIKELY(ret != Qundef)) {
710
+ slot->value = ret;
711
+ slot->version = new_version;
712
+ /* No store to txg->version here: txg_next_version already
713
+ * published it under version_lock. Re-storing without the lock
714
+ * could roll the clock back over a commit that advanced it, and
715
+ * a reused version number lets a stale read pass validation. */
716
+ }
717
+ }
718
+ tx_slot_unlock(slot);
719
+
720
+ if (RB_UNLIKELY(ret == Qundef)) {
721
+ // atomically{ self.value += inc }
722
+ ret = rb_funcall(self, rb_intern("__increment__"), 1, inc);
723
+ }
724
+ }
725
+ else {
726
+ recv = tx_get(tx, slot, self);
727
+ if (RB_UNLIKELY((ret = tvar_calc_inc(recv, inc)) == Qundef)) {
728
+ /* + can return anything; a TVar only ever holds shareable values, and
729
+ * outside a transaction this path is checked by #value=. */
730
+ ret = rb_funcall(recv, rb_intern("+"), 1, inc);
731
+ if (RB_UNLIKELY(!rb_ractor_shareable_p(ret))) {
732
+ rb_raise(rb_eArgError, "only shareable object are allowed");
733
+ }
734
+ }
735
+ tx_set(tx, ret, slot, self);
736
+ }
737
+
738
+ return ret;
739
+ }
740
+
741
+ static VALUE
742
+ tvar_value_increment(int argc, VALUE *argv, VALUE self)
743
+ {
744
+ switch (argc) {
745
+ case 0: return tvar_value_increment_(self, INT2FIX(1));
746
+ case 1: return tvar_value_increment_(self, argv[0]);
747
+ // todo: scan args
748
+ default: rb_raise(rb_eArgError, "2 or more arguments");
749
+ }
750
+ }
751
+
752
+ #if 0 // unused
753
+ static struct tvar_slot *
754
+ tvar_slot_ptr(VALUE v)
755
+ {
756
+ if (rb_typeddata_is_kind_of(v, &tvar_data_type)) {
757
+ return DATA_PTR(v);
758
+ }
759
+ else {
760
+ rb_raise(rb_eArgError, "TVar is needed");
761
+ }
762
+ }
763
+ #endif
764
+
765
+ static VALUE
766
+ tx_atomically_body2(VALUE txptr)
767
+ {
768
+ struct tx_logs *tx = (struct tx_logs *)txptr;
769
+
770
+ while (1) {
771
+ VALUE ret = rb_yield(Qnil);
772
+
773
+ if (tx_commit(tx)) {
774
+ return ret;
775
+ }
776
+ else {
777
+ tx_reset(tx);
778
+ }
779
+ }
780
+ }
781
+
782
+ static VALUE
783
+ tx_atomically_rescue(VALUE txptr, VALUE err)
784
+ {
785
+ struct tx_logs *tx = (struct tx_logs *)txptr;
786
+ tx_reset(tx);
787
+ return Qundef;
788
+ }
789
+
790
+ static VALUE
791
+ tx_atomically_body(VALUE txptr)
792
+ {
793
+ VALUE ret;
794
+
795
+ do {
796
+ ret = rb_rescue2(tx_atomically_body2, (VALUE)txptr,
797
+ tx_atomically_rescue, (VALUE)txptr,
798
+ rb_eTxRetry, 0);
799
+ } while (ret == Qundef);
800
+
801
+ return ret;
802
+ }
803
+
804
+ static VALUE
805
+ tx_atomically_ensure(VALUE txptr)
806
+ {
807
+ struct tx_logs *tx = (struct tx_logs *)txptr;
808
+ tx_end(tx);
809
+ return 0;
810
+ }
811
+
812
+ static VALUE
813
+ tx_atomically(VALUE self)
814
+ {
815
+ /* Pin the TxLogs for the whole transaction. Its only other reference is
816
+ * the thread local, which Ruby code can overwrite mid-transaction; without
817
+ * this pin that made the raw pointer below dangle once the GC ran. */
818
+ VALUE txobj = tx_logs_obj();
819
+ struct tx_logs *tx = tx_begin();
820
+ VALUE ret;
821
+
822
+ if (tx != NULL) {
823
+ ret = rb_ensure(tx_atomically_body, (VALUE)tx,
824
+ tx_atomically_ensure, (VALUE)tx);
825
+ }
826
+ else {
827
+ ret = rb_yield(Qnil);
828
+ }
829
+ RB_GC_GUARD(txobj);
830
+ return ret;
831
+ }
832
+
833
+ void
834
+ Init_tvar(void)
835
+ {
836
+ rb_ext_ractor_safe(true);
837
+
838
+ // initialixe tx_global
839
+ struct tx_global *txg = tx_global_ptr();
840
+ txg->slot_index = 0;
841
+ txg->version = 0;
842
+ rb_native_mutex_initialize(&txg->slot_index_lock);
843
+ rb_native_mutex_initialize(&txg->version_lock);
844
+
845
+ id_tx_logs = rb_intern("__ractor_tvar_tls__");
846
+
847
+ // errors
848
+ rb_eTxError = rb_define_class_under(rb_cRactor, "TransactionError", rb_eRuntimeError);
849
+ rb_eTxRetry = rb_define_class_under(rb_cRactor, "RetryTransaction", rb_eException);
850
+ rb_exc_tx_retry = rb_exc_new_cstr(rb_eTxRetry, "Thread::RetryTransaction");
851
+ rb_obj_freeze(rb_exc_tx_retry);
852
+ rb_gc_register_mark_object(rb_exc_tx_retry);
853
+
854
+ // TxLogs
855
+ rb_cRactorTxLogs = rb_define_class_under(rb_cRactor, "TxLogs", rb_cObject);
856
+ rb_undef_alloc_func(rb_cRactorTxLogs); /* internal; made only by tx_logs() */ // hidden object
857
+
858
+ // TVar APIs
859
+ rb_define_singleton_method(rb_cRactor, "atomically", tx_atomically, 0);
860
+
861
+ rb_cRactorTVar = rb_define_class_under(rb_cRactor, "TVar", rb_cObject);
862
+ /* .new makes the slot; an allocated-but-uninitialized TVar has none. */
863
+ rb_undef_alloc_func(rb_cRactorTVar);
864
+ rb_define_singleton_method(rb_cRactorTVar, "new", tvar_new, -1);
865
+ rb_define_method(rb_cRactorTVar, "value", tvar_value, 0);
866
+ rb_define_method(rb_cRactorTVar, "value=", tvar_value_set, 1);
867
+ rb_define_method(rb_cRactorTVar, "increment", tvar_value_increment, -1);
868
+ // rb_define_method(rb_cRactorTVar, "inspect", tvar_inspect, 0);
869
+ }