mini_racer 0.21.2 → 0.21.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70ca9828687a7fcdb44048eb1a3e9ad2fe3f38082492504fc22563fe94fa524a
4
- data.tar.gz: 61719146ed1430235269e80a21a60ef59b98f22f470cda13820074f9cecea818
3
+ metadata.gz: 3e370b2c6cdb60e5081d5f97840cff28489119f0225a3c9a74ed7f6859caf40e
4
+ data.tar.gz: 7405ed87278cbaa1d155bc72f8c99d8e8c744df743252eb4128b9ae9ff899d07
5
5
  SHA512:
6
- metadata.gz: 944e4a0e403b134e0206c6d6381efa3d9f120af9cfce18f61fc85c3eef5e6a077c64a457142d347041059a6b76f24990dbd15c17691bbcb3306ff1ea877983a4
7
- data.tar.gz: f3003a61146634c76ad28ab99be4d6e96b34490522edb13607b03fcf9354fce135844947af3d23f9d37cc012a26ba4c280c8d4b19d410d47bcebc753cdc6c34f
6
+ metadata.gz: 3c9f53c6a9052cca6a5129d023ecece59613c40c17f4c7666c728f8f6327200e34d707557ea4dcd9b626aed332b327c6c3e3f90e365c630adbec0b2d02c4b1c1
7
+ data.tar.gz: 4dc0348a006f7620c4c9688c0b877f2bd9500b7ece37fba4fa6e868006af1517e4279537693fd87b2f18c2b0602067084e2332e2bf9fa862504ebaab4ddbbcfc
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ - 0.21.3 - 18-06-2026
2
+ - Fix `:single_threaded` contexts inherited across `fork` by recovering idle reusable native runners in the child process without falling back to per-dispatch native thread spawning
3
+ - Avoid intermittent heap corruption during `:single_threaded` context finalization, especially when forked children exit normally after touching inherited contexts
4
+ - Avoid finalizer hangs when a forked child garbage-collects a non-idle inherited `:single_threaded` context
5
+ - Allow Ruby thread interrupts, process shutdown, and cross-thread `Context#dispose` to terminate busy `:single_threaded` JavaScript execution instead of hanging
6
+ - Make `Context#dispose` while an attached Ruby callback is active either terminate safely or raise instead of deadlocking
7
+
1
8
  - 0.21.2 - 11-06-2026
2
9
  - Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks
3
10
  - Fix native memory leaks in `Context#heap_snapshot`/`Context#write_heap_snapshot`; thanks to Pranjali Thakur from depthfirst.com
@@ -5,6 +5,7 @@
5
5
  #include <string.h>
6
6
  #include <pthread.h>
7
7
  #include <unistd.h>
8
+ #include <errno.h>
8
9
  #include <math.h>
9
10
 
10
11
  #if defined(__linux__) && !defined(__GLIBC__)
@@ -159,6 +160,7 @@ typedef struct Snapshot {
159
160
  } Snapshot;
160
161
 
161
162
  static void context_destroy(Context *c);
163
+ static void context_abandon(Context *c);
162
164
  static void context_free(void *arg);
163
165
  static void context_mark(void *arg);
164
166
  static size_t context_size(const void *arg);
@@ -227,6 +229,7 @@ struct rendezvous_nogvl
227
229
  {
228
230
  Context *context;
229
231
  Buf *req, *res;
232
+ atomic_int active;
230
233
  };
231
234
 
232
235
  struct rendezvous_des
@@ -870,8 +873,14 @@ void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
870
873
  {
871
874
  buf_reset(&c->req);
872
875
  pthread_cond_signal(&c->cv);
873
- while (!c->req.len)
876
+ while (!c->req.len && !atomic_load(&c->quit))
874
877
  pthread_cond_wait(&c->cv, &c->mtx);
878
+ if (!c->req.len && atomic_load(&c->quit)) {
879
+ static const uint8_t disposed[] = "edisposed context";
880
+ *p = disposed;
881
+ *n = sizeof(disposed) - 1;
882
+ return;
883
+ }
875
884
  buf_reset(&c->res);
876
885
  *p = c->req.buf;
877
886
  *n = c->req.len;
@@ -1007,6 +1016,33 @@ static void *single_threaded_runner(void *arg)
1007
1016
  return NULL;
1008
1017
  }
1009
1018
 
1019
+ static int single_threaded_recover_after_fork(Context *c)
1020
+ {
1021
+ pthread_condattr_t cattr;
1022
+ pid_t pid;
1023
+ int r;
1024
+
1025
+ pid = getpid();
1026
+ if (!c->single_threaded_thr_started || c->single_threaded_pid == pid)
1027
+ return 0;
1028
+ if (c->depth || c->req.len || c->res.len)
1029
+ return EBUSY;
1030
+
1031
+ if ((r = pthread_condattr_init(&cattr)))
1032
+ return r;
1033
+ #ifndef __APPLE__
1034
+ pthread_condattr_setclock(&cattr, CLOCK_MONOTONIC);
1035
+ #endif
1036
+ r = pthread_cond_init(&c->cv, &cattr);
1037
+ pthread_condattr_destroy(&cattr);
1038
+ if (r)
1039
+ return r;
1040
+
1041
+ c->single_threaded_thr_started = 0;
1042
+ c->single_threaded_pid = pid;
1043
+ return 0;
1044
+ }
1045
+
1010
1046
  static int single_threaded_runner_start(Context *c)
1011
1047
  {
1012
1048
  pid_t pid;
@@ -1031,7 +1067,10 @@ static inline void *rendezvous_nogvl(void *arg)
1031
1067
 
1032
1068
  a = arg;
1033
1069
  c = a->context;
1070
+ if (single_threaded && (r = single_threaded_recover_after_fork(c)))
1071
+ return (void *)(intptr_t)r;
1034
1072
  pthread_mutex_lock(&c->rr_mtx);
1073
+ atomic_store(&a->active, 1);
1035
1074
  if (c->depth > 0 && c->depth%50 == 0) { // TODO stop steep recursion
1036
1075
  fprintf(stderr, "mini_racer: deep js->ruby->js recursion, depth=%d\n", c->depth);
1037
1076
  fflush(stderr);
@@ -1039,6 +1078,14 @@ static inline void *rendezvous_nogvl(void *arg)
1039
1078
  c->depth++;
1040
1079
  next:
1041
1080
  pthread_mutex_lock(&c->mtx);
1081
+ if (atomic_load(&c->quit)) {
1082
+ buf_reset(a->req);
1083
+ pthread_mutex_unlock(&c->mtx);
1084
+ c->depth--;
1085
+ atomic_store(&a->active, 0);
1086
+ pthread_mutex_unlock(&c->rr_mtx);
1087
+ return (void *)(intptr_t)ECANCELED;
1088
+ }
1042
1089
  assert(c->req.len == 0);
1043
1090
  assert(c->res.len == 0);
1044
1091
  buf_move(a->req, &c->req); // v8 thread takes ownership of req
@@ -1048,6 +1095,7 @@ next:
1048
1095
  buf_move(&c->req, a->req);
1049
1096
  pthread_mutex_unlock(&c->mtx);
1050
1097
  c->depth--;
1098
+ atomic_store(&a->active, 0);
1051
1099
  pthread_mutex_unlock(&c->rr_mtx);
1052
1100
  return (void *)(intptr_t)r;
1053
1101
  }
@@ -1058,29 +1106,66 @@ next:
1058
1106
  do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
1059
1107
  }
1060
1108
  buf_move(&c->res, a->res);
1109
+ pthread_cond_broadcast(&c->cv);
1061
1110
  pthread_mutex_unlock(&c->mtx);
1062
1111
  if (*a->res->buf == 'c') { // js -> ruby callback?
1063
1112
  rb_thread_call_with_gvl(rendezvous_callback, a);
1064
1113
  buf_reset(a->res);
1114
+ if (atomic_load(&c->quit)) {
1115
+ buf_reset(a->req);
1116
+ c->depth--;
1117
+ atomic_store(&a->active, 0);
1118
+ pthread_mutex_unlock(&c->rr_mtx);
1119
+ return (void *)(intptr_t)ECANCELED;
1120
+ }
1065
1121
  goto next;
1066
1122
  }
1067
1123
  c->depth--;
1124
+ atomic_store(&a->active, 0);
1068
1125
  pthread_mutex_unlock(&c->rr_mtx);
1069
1126
  return NULL;
1070
1127
  }
1071
1128
 
1129
+ static void rendezvous_ubf(void *arg)
1130
+ {
1131
+ struct rendezvous_nogvl *a;
1132
+ Context *c;
1133
+
1134
+ a = arg;
1135
+ if (!atomic_load(&a->active))
1136
+ return;
1137
+ c = a->context;
1138
+ if (c->pst)
1139
+ v8_terminate_execution(c->pst);
1140
+ pthread_cond_broadcast(&c->cv);
1141
+ }
1142
+
1143
+ static void terminate_ubf(void *arg)
1144
+ {
1145
+ Context *c;
1146
+
1147
+ c = arg;
1148
+ if (c->pst)
1149
+ v8_terminate_execution(c->pst);
1150
+ pthread_cond_broadcast(&c->cv);
1151
+ }
1152
+
1072
1153
  static void rendezvous_no_des(Context *c, Buf *req, Buf *res)
1073
1154
  {
1074
1155
  void *r;
1156
+ struct rendezvous_nogvl a;
1075
1157
 
1076
1158
  if (atomic_load(&c->quit)) {
1077
1159
  buf_reset(req);
1078
1160
  rb_raise(context_disposed_error, "disposed context");
1079
1161
  }
1080
- r = rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res},
1081
- NULL, NULL, 0);
1162
+ a.context = c;
1163
+ a.req = req;
1164
+ a.res = res;
1165
+ atomic_init(&a.active, 0);
1166
+ r = rb_nogvl(rendezvous_nogvl, &a, rendezvous_ubf, &a, 0);
1082
1167
  if (r)
1083
- rb_raise(runtime_error, "pthread_create: %s", strerror((int)(intptr_t)r));
1168
+ rb_raise(runtime_error, "single-threaded runner: %s", strerror((int)(intptr_t)r));
1084
1169
  }
1085
1170
 
1086
1171
  // send request to & receive reply from v8 thread; takes ownership of |req|
@@ -1096,7 +1181,8 @@ static VALUE rendezvous1(Context *c, Buf *req, DesCtx *d)
1096
1181
  c->exception = Qnil;
1097
1182
  // if js land didn't handle exception from ruby callback, re-raise it now
1098
1183
  if (res.len == 1 && *res.buf == 'e') {
1099
- assert(!NIL_P(r));
1184
+ if (NIL_P(r))
1185
+ rb_raise(context_disposed_error, "disposed context");
1100
1186
  rb_exc_raise(r);
1101
1187
  }
1102
1188
  r = rb_protect(deserialize, (VALUE)&(struct rendezvous_des){d, &res}, &exc);
@@ -1251,11 +1337,19 @@ fail0:
1251
1337
  return Qnil; // pacify compiler
1252
1338
  }
1253
1339
 
1254
- static void *context_free_thread_do(void *arg)
1340
+ static void *context_free_do(void *arg)
1255
1341
  {
1256
1342
  Context *c;
1257
1343
 
1258
1344
  c = arg;
1345
+ if (single_threaded && single_threaded_recover_after_fork(c)) {
1346
+ // The child forked while this inherited context was not idle. There is
1347
+ // no live runner thread to join and the inherited V8/pthread state is
1348
+ // not safe to tear down. A finalizer must not hang here; let the OS
1349
+ // reclaim the abandoned V8 state when the child exits.
1350
+ context_abandon(c);
1351
+ return NULL;
1352
+ }
1259
1353
  if (single_threaded && c->single_threaded_thr_started && c->single_threaded_pid == getpid()) {
1260
1354
  pthread_mutex_lock(&c->mtx);
1261
1355
  atomic_store(&c->quit, 2);
@@ -1270,31 +1364,16 @@ static void *context_free_thread_do(void *arg)
1270
1364
  return NULL;
1271
1365
  }
1272
1366
 
1273
- static void context_free_thread(Context *c)
1274
- {
1275
- pthread_t thr;
1276
- int r;
1277
-
1278
- // dispose on another thread so we don't block when trying to
1279
- // enter an isolate that's in a stuck state; that *should* be
1280
- // impossible but apparently it happened regularly before the
1281
- // rewrite and I'm carrying it over out of an abundance of caution
1282
- if ((r = pthread_create(&thr, NULL, context_free_thread_do, c))) {
1283
- fprintf(stderr, "mini_racer: pthread_create: %s", strerror(r));
1284
- fflush(stderr);
1285
- context_free_thread_do(c);
1286
- } else {
1287
- pthread_detach(thr);
1288
- }
1289
- }
1290
-
1291
1367
  static void context_free(void *arg)
1292
1368
  {
1293
1369
  Context *c;
1294
1370
 
1295
1371
  c = arg;
1296
1372
  if (single_threaded) {
1297
- context_free_thread(c);
1373
+ // Free synchronously. A detached cleanup thread can race normal Ruby
1374
+ // process shutdown and trip glibc malloc corruption checks while V8 is
1375
+ // tearing down single-threaded contexts.
1376
+ context_free_do(c);
1298
1377
  } else {
1299
1378
  pthread_mutex_lock(&c->mtx);
1300
1379
  c->quit = 2; // 2 = v8 thread frees
@@ -1303,6 +1382,14 @@ static void context_free(void *arg)
1303
1382
  }
1304
1383
  }
1305
1384
 
1385
+ static void context_abandon(Context *c)
1386
+ {
1387
+ buf_reset(&c->snapshot);
1388
+ buf_reset(&c->req);
1389
+ buf_reset(&c->res);
1390
+ ruby_xfree(c);
1391
+ }
1392
+
1306
1393
  static void context_destroy(Context *c)
1307
1394
  {
1308
1395
  pthread_mutex_unlock(&c->mtx);
@@ -1360,8 +1447,25 @@ static VALUE context_attach(VALUE self, VALUE name, VALUE proc)
1360
1447
  static void *context_dispose_do(void *arg)
1361
1448
  {
1362
1449
  Context *c;
1450
+ int r;
1363
1451
 
1364
1452
  c = arg;
1453
+ if (single_threaded) {
1454
+ if ((r = single_threaded_recover_after_fork(c)))
1455
+ return (void *)(intptr_t)r;
1456
+ }
1457
+ if (c->depth > 0) {
1458
+ r = pthread_mutex_trylock(&c->rr_mtx);
1459
+ if (!r) {
1460
+ pthread_mutex_unlock(&c->rr_mtx);
1461
+ return (void *)(intptr_t)EBUSY;
1462
+ }
1463
+ if (r != EBUSY)
1464
+ return (void *)(intptr_t)r;
1465
+ if (c->pst)
1466
+ v8_terminate_execution(c->pst);
1467
+ pthread_cond_broadcast(&c->cv);
1468
+ }
1365
1469
  if (single_threaded) {
1366
1470
  pthread_mutex_lock(&c->mtx);
1367
1471
  while (c->req.len || c->res.len)
@@ -1389,9 +1493,12 @@ static void *context_dispose_do(void *arg)
1389
1493
  static VALUE context_dispose(VALUE self)
1390
1494
  {
1391
1495
  Context *c;
1496
+ void *r;
1392
1497
 
1393
1498
  TypedData_Get_Struct(self, Context, &context_type, c);
1394
- rb_thread_call_without_gvl(context_dispose_do, c, NULL, NULL);
1499
+ r = rb_thread_call_without_gvl(context_dispose_do, c, terminate_ubf, c);
1500
+ if (r)
1501
+ rb_raise(runtime_error, "context dispose: %s", strerror((int)(intptr_t)r));
1395
1502
  return Qnil;
1396
1503
  }
1397
1504
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.21.2"
4
+ VERSION = "0.21.3"
5
5
  LIBV8_NODE_VERSION = "~> 24.12.0.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.2
4
+ version: 0.21.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -136,9 +136,9 @@ licenses:
136
136
  - MIT
137
137
  metadata:
138
138
  bug_tracker_uri: https://github.com/discourse/mini_racer/issues
139
- changelog_uri: https://github.com/discourse/mini_racer/blob/v0.21.2/CHANGELOG
140
- documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.2
141
- source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.2
139
+ changelog_uri: https://github.com/discourse/mini_racer/blob/v0.21.3/CHANGELOG
140
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.3
141
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.3
142
142
  rdoc_options: []
143
143
  require_paths:
144
144
  - lib