mini_racer 0.21.2 → 0.21.4

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: f2e290a77bd6b38286dc4e4224d71579a1e8e41bbb8fd70029584f518383ba7f
4
+ data.tar.gz: 479d9793ade9c46075986dd53c0a7ebacf62e1afb199fc6983e5b7c4fb4e0faa
5
5
  SHA512:
6
- metadata.gz: 944e4a0e403b134e0206c6d6381efa3d9f120af9cfce18f61fc85c3eef5e6a077c64a457142d347041059a6b76f24990dbd15c17691bbcb3306ff1ea877983a4
7
- data.tar.gz: f3003a61146634c76ad28ab99be4d6e96b34490522edb13607b03fcf9354fce135844947af3d23f9d37cc012a26ba4c280c8d4b19d410d47bcebc753cdc6c34f
6
+ metadata.gz: dc69216098ec6076dc90cf731fc5fd229526a4c21d3383fdb94448aace72068f36fc6bf6910cb09d8f42bf57c6926bf1527a2fd0871c11c5b08df820643cc23e
7
+ data.tar.gz: b7b4b851b4cbc9ad35c56e39db1d5c7ccc19b23dabd9dd87f084ba37f0a59fb584c833128827d58ecdb0c99f75969383333d3b304d00f816ba544e83e5436639
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ - 0.21.4 - 24-06-2026
2
+ - Fix stale V8 termination state after interrupts/timeouts so contexts remain usable after cancelled evaluations
3
+ - Let Ruby interrupts wake MiniRacer calls without immediately terminating V8, allowing signal traps and nested callbacks to unwind safely
4
+ - Add benchmark suite covering eval, serialization/deserialization, and transpilation workloads
5
+
6
+ - 0.21.3 - 18-06-2026
7
+ - 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
8
+ - Avoid intermittent heap corruption during `:single_threaded` context finalization, especially when forked children exit normally after touching inherited contexts
9
+ - Avoid finalizer hangs when a forked child garbage-collects a non-idle inherited `:single_threaded` context
10
+ - Allow Ruby thread interrupts, process shutdown, and cross-thread `Context#dispose` to terminate busy `:single_threaded` JavaScript execution instead of hanging
11
+ - Make `Context#dispose` while an attached Ruby callback is active either terminate safely or raise instead of deadlocking
12
+
1
13
  - 0.21.2 - 11-06-2026
2
14
  - Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks
3
15
  - 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__)
@@ -136,6 +137,8 @@ typedef struct Context
136
137
  VALUE procs; // array of js -> ruby callbacks
137
138
  VALUE exception; // pending exception or Qnil
138
139
  Buf req, res; // ruby->v8 request/response, mediated by |mtx| and |cv|
140
+ Buf v8_req; // stable v8-side copy of a request returned by v8_roundtrip
141
+ int res_ready; // protected by |mtx|; response may be filled before ready
139
142
  Buf snapshot;
140
143
  pthread_t single_threaded_thr;
141
144
  pid_t single_threaded_pid;
@@ -159,6 +162,7 @@ typedef struct Snapshot {
159
162
  } Snapshot;
160
163
 
161
164
  static void context_destroy(Context *c);
165
+ static void context_abandon(Context *c);
162
166
  static void context_free(void *arg);
163
167
  static void context_mark(void *arg);
164
168
  static size_t context_size(const void *arg);
@@ -227,6 +231,9 @@ struct rendezvous_nogvl
227
231
  {
228
232
  Context *context;
229
233
  Buf *req, *res;
234
+ atomic_int active;
235
+ atomic_int interrupted;
236
+ int started, finished, has_rr_mtx;
230
237
  };
231
238
 
232
239
  struct rendezvous_des
@@ -819,11 +826,28 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
819
826
  fflush(stderr);
820
827
  }
821
828
 
822
- static void dispatch(Context *c)
829
+ static void dispatch_buf(Context *c, Buf *req)
823
830
  {
831
+ Buf local_req;
832
+
833
+ // Called with Context.mtx held. Release it while V8 runs so an rb_nogvl
834
+ // UBF can wake the Ruby thread to process interrupts with
835
+ // rb_thread_check_ints(), without forcing termination first. Move the
836
+ // request out first so cancellation cannot free a buffer V8 is reading.
837
+ buf_move(req, &local_req);
824
838
  buf_reset(&c->res);
825
- dispatch1(c, c->req.buf, c->req.len);
826
- buf_reset(&c->req);
839
+ c->res_ready = 0;
840
+ pthread_mutex_unlock(&c->mtx);
841
+ dispatch1(c, local_req.buf, local_req.len);
842
+ pthread_mutex_lock(&c->mtx);
843
+ buf_reset(&local_req);
844
+ c->res_ready = 1;
845
+ pthread_cond_signal(&c->cv);
846
+ }
847
+
848
+ static void dispatch(Context *c)
849
+ {
850
+ dispatch_buf(c, &c->req);
827
851
  }
828
852
 
829
853
  // called by v8_isolate_and_context
@@ -856,31 +880,45 @@ void v8_thread_main(Context *c, struct State *pst)
856
880
  }
857
881
  }
858
882
 
859
- // called by v8_thread_main and from mini_racer_v8.cc,
860
- // in all cases with Context.mtx held
883
+ // called by v8_thread_main and from mini_racer_v8.cc
861
884
  void v8_dispatch(Context *c)
862
885
  {
863
- dispatch1(c, c->req.buf, c->req.len);
864
- buf_reset(&c->req);
886
+ pthread_mutex_lock(&c->mtx);
887
+ dispatch_buf(c, &c->v8_req);
888
+ pthread_mutex_unlock(&c->mtx);
865
889
  }
866
890
 
867
- // called from mini_racer_v8.cc with Context.mtx held
868
891
  // only called when inside v8_call, v8_eval, or v8_pump_message_loop
869
892
  void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
870
893
  {
894
+ pthread_mutex_lock(&c->mtx);
895
+ buf_reset(&c->v8_req);
871
896
  buf_reset(&c->req);
897
+ if (c->res.len)
898
+ c->res_ready = 1;
872
899
  pthread_cond_signal(&c->cv);
873
- while (!c->req.len)
900
+ while (!c->req.len && !atomic_load(&c->quit))
874
901
  pthread_cond_wait(&c->cv, &c->mtx);
902
+ if (!c->req.len && atomic_load(&c->quit)) {
903
+ static const uint8_t disposed[] = "edisposed context";
904
+ *p = disposed;
905
+ *n = sizeof(disposed) - 1;
906
+ pthread_mutex_unlock(&c->mtx);
907
+ return;
908
+ }
875
909
  buf_reset(&c->res);
876
- *p = c->req.buf;
877
- *n = c->req.len;
910
+ c->res_ready = 0;
911
+ buf_move(&c->req, &c->v8_req);
912
+ *p = c->v8_req.buf;
913
+ *n = c->v8_req.len;
914
+ pthread_mutex_unlock(&c->mtx);
878
915
  }
879
916
 
880
- // called from mini_racer_v8.cc with Context.mtx held
881
917
  void v8_reply(Context *c, const uint8_t *p, size_t n)
882
918
  {
919
+ pthread_mutex_lock(&c->mtx);
883
920
  buf_put(&c->res, p, n);
921
+ pthread_mutex_unlock(&c->mtx);
884
922
  }
885
923
 
886
924
  static void v8_once_init(void)
@@ -1007,6 +1045,33 @@ static void *single_threaded_runner(void *arg)
1007
1045
  return NULL;
1008
1046
  }
1009
1047
 
1048
+ static int single_threaded_recover_after_fork(Context *c)
1049
+ {
1050
+ pthread_condattr_t cattr;
1051
+ pid_t pid;
1052
+ int r;
1053
+
1054
+ pid = getpid();
1055
+ if (!c->single_threaded_thr_started || c->single_threaded_pid == pid)
1056
+ return 0;
1057
+ if (c->depth || c->req.len || c->res.len)
1058
+ return EBUSY;
1059
+
1060
+ if ((r = pthread_condattr_init(&cattr)))
1061
+ return r;
1062
+ #ifndef __APPLE__
1063
+ pthread_condattr_setclock(&cattr, CLOCK_MONOTONIC);
1064
+ #endif
1065
+ r = pthread_cond_init(&c->cv, &cattr);
1066
+ pthread_condattr_destroy(&cattr);
1067
+ if (r)
1068
+ return r;
1069
+
1070
+ c->single_threaded_thr_started = 0;
1071
+ c->single_threaded_pid = pid;
1072
+ return 0;
1073
+ }
1074
+
1010
1075
  static int single_threaded_runner_start(Context *c)
1011
1076
  {
1012
1077
  pid_t pid;
@@ -1023,6 +1088,19 @@ static int single_threaded_runner_start(Context *c)
1023
1088
  return r;
1024
1089
  }
1025
1090
 
1091
+ static void rendezvous_release(struct rendezvous_nogvl *a)
1092
+ {
1093
+ Context *c;
1094
+
1095
+ atomic_store(&a->active, 0);
1096
+ if (!a->has_rr_mtx)
1097
+ return;
1098
+ c = a->context;
1099
+ c->depth--;
1100
+ a->has_rr_mtx = 0;
1101
+ pthread_mutex_unlock(&c->rr_mtx);
1102
+ }
1103
+
1026
1104
  static inline void *rendezvous_nogvl(void *arg)
1027
1105
  {
1028
1106
  struct rendezvous_nogvl *a;
@@ -1031,56 +1109,200 @@ static inline void *rendezvous_nogvl(void *arg)
1031
1109
 
1032
1110
  a = arg;
1033
1111
  c = a->context;
1034
- pthread_mutex_lock(&c->rr_mtx);
1035
- if (c->depth > 0 && c->depth%50 == 0) { // TODO stop steep recursion
1036
- fprintf(stderr, "mini_racer: deep js->ruby->js recursion, depth=%d\n", c->depth);
1037
- fflush(stderr);
1112
+ if (!a->started) {
1113
+ if (single_threaded && (r = single_threaded_recover_after_fork(c)))
1114
+ return (void *)(intptr_t)r;
1115
+ pthread_mutex_lock(&c->rr_mtx);
1116
+ a->has_rr_mtx = 1;
1117
+ if (c->depth > 0 && c->depth%50 == 0) { // TODO stop steep recursion
1118
+ fprintf(stderr, "mini_racer: deep js->ruby->js recursion, depth=%d\n", c->depth);
1119
+ fflush(stderr);
1120
+ }
1121
+ c->depth++;
1122
+ a->started = 1;
1038
1123
  }
1039
- c->depth++;
1124
+
1040
1125
  next:
1126
+ atomic_store(&a->active, 1);
1041
1127
  pthread_mutex_lock(&c->mtx);
1042
- assert(c->req.len == 0);
1043
- assert(c->res.len == 0);
1044
- buf_move(a->req, &c->req); // v8 thread takes ownership of req
1045
- if (single_threaded) {
1046
- r = single_threaded_runner_start(c);
1047
- if (r) {
1048
- buf_move(&c->req, a->req);
1049
- pthread_mutex_unlock(&c->mtx);
1050
- c->depth--;
1051
- pthread_mutex_unlock(&c->rr_mtx);
1052
- return (void *)(intptr_t)r;
1128
+ if (atomic_load(&c->quit)) {
1129
+ buf_reset(a->req);
1130
+ pthread_mutex_unlock(&c->mtx);
1131
+ a->finished = 1;
1132
+ rendezvous_release(a);
1133
+ return (void *)(intptr_t)ECANCELED;
1134
+ }
1135
+ if (a->req->len) {
1136
+ assert(c->req.len == 0);
1137
+ assert(!c->res_ready);
1138
+ buf_move(a->req, &c->req); // v8 thread takes ownership of req
1139
+ if (single_threaded) {
1140
+ r = single_threaded_runner_start(c);
1141
+ if (r) {
1142
+ buf_move(&c->req, a->req);
1143
+ pthread_mutex_unlock(&c->mtx);
1144
+ a->finished = 1;
1145
+ rendezvous_release(a);
1146
+ return (void *)(intptr_t)r;
1147
+ }
1053
1148
  }
1054
1149
  pthread_cond_signal(&c->cv);
1055
- do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
1056
- } else {
1057
- pthread_cond_signal(&c->cv);
1058
- do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
1150
+ }
1151
+ while (!c->res_ready && !atomic_load(&a->interrupted) && !atomic_load(&c->quit))
1152
+ pthread_cond_wait(&c->cv, &c->mtx);
1153
+ if (!c->res_ready && atomic_load(&a->interrupted)) {
1154
+ atomic_store(&a->active, 0);
1155
+ pthread_mutex_unlock(&c->mtx);
1156
+ return (void *)(intptr_t)EINTR;
1157
+ }
1158
+ if (!c->res_ready && atomic_load(&c->quit)) {
1159
+ buf_reset(a->req);
1160
+ pthread_mutex_unlock(&c->mtx);
1161
+ a->finished = 1;
1162
+ rendezvous_release(a);
1163
+ return (void *)(intptr_t)ECANCELED;
1059
1164
  }
1060
1165
  buf_move(&c->res, a->res);
1166
+ c->res_ready = 0;
1167
+ pthread_cond_broadcast(&c->cv);
1061
1168
  pthread_mutex_unlock(&c->mtx);
1169
+ atomic_store(&a->active, 0);
1062
1170
  if (*a->res->buf == 'c') { // js -> ruby callback?
1063
1171
  rb_thread_call_with_gvl(rendezvous_callback, a);
1064
1172
  buf_reset(a->res);
1173
+ if (atomic_load(&c->quit)) {
1174
+ buf_reset(a->req);
1175
+ a->finished = 1;
1176
+ rendezvous_release(a);
1177
+ return (void *)(intptr_t)ECANCELED;
1178
+ }
1065
1179
  goto next;
1066
1180
  }
1067
- c->depth--;
1068
- pthread_mutex_unlock(&c->rr_mtx);
1181
+ a->finished = 1;
1182
+ rendezvous_release(a);
1183
+ return NULL;
1184
+ }
1185
+
1186
+ static void rendezvous_ubf(void *arg)
1187
+ {
1188
+ struct rendezvous_nogvl *a;
1189
+ Context *c;
1190
+
1191
+ a = arg;
1192
+ if (!atomic_load(&a->active))
1193
+ return;
1194
+ atomic_store(&a->interrupted, 1);
1195
+ c = a->context;
1196
+ pthread_cond_broadcast(&c->cv);
1197
+ }
1198
+
1199
+ static void terminate_ubf(void *arg)
1200
+ {
1201
+ Context *c;
1202
+
1203
+ c = arg;
1204
+ if (c->pst)
1205
+ v8_terminate_execution(c->pst);
1206
+ pthread_cond_broadcast(&c->cv);
1207
+ }
1208
+
1209
+ static void *rendezvous_cancel_nogvl(void *arg)
1210
+ {
1211
+ // Reply to any pending JS->Ruby callback with an 'e' marker plus message
1212
+ // so V8 throws, unwinds, and reaches its normal termination cleanup.
1213
+ static const uint8_t terminated[] = "eterminated";
1214
+ struct rendezvous_nogvl *a;
1215
+ Context *c;
1216
+
1217
+ a = arg;
1218
+ c = a->context;
1219
+ atomic_store(&a->active, 0);
1220
+ if (c->pst)
1221
+ v8_terminate_execution(c->pst);
1222
+ pthread_mutex_lock(&c->mtx);
1223
+ pthread_cond_broadcast(&c->cv);
1224
+ while (!atomic_load(&c->quit)) {
1225
+ while (!c->res_ready && !atomic_load(&c->quit))
1226
+ pthread_cond_wait(&c->cv, &c->mtx);
1227
+ if (!c->res_ready)
1228
+ break;
1229
+ if (c->res.len && *c->res.buf != 'c')
1230
+ break;
1231
+ buf_reset(&c->res);
1232
+ c->res_ready = 0;
1233
+ buf_reset(&c->req);
1234
+ buf_put(&c->req, terminated, sizeof(terminated) - 1);
1235
+ pthread_cond_signal(&c->cv);
1236
+ }
1237
+ buf_reset(&c->req);
1238
+ buf_reset(&c->res);
1239
+ buf_reset(&c->v8_req);
1240
+ c->res_ready = 0;
1241
+ pthread_cond_broadcast(&c->cv);
1242
+ pthread_mutex_unlock(&c->mtx);
1243
+ if (c->pst)
1244
+ v8_cancel_terminate_execution(c->pst);
1245
+ a->finished = 1;
1246
+ rendezvous_release(a);
1069
1247
  return NULL;
1070
1248
  }
1071
1249
 
1250
+ static VALUE rendezvous_no_des_body(VALUE arg)
1251
+ {
1252
+ struct rendezvous_nogvl *a;
1253
+ void *r;
1254
+
1255
+ a = (void *)arg;
1256
+ for (;;) {
1257
+ // Let the UBF wake this wait without deciding why Ruby interrupted it.
1258
+ // Back under the GVL, rb_thread_check_ints() runs signal traps and
1259
+ // raises real asynchronous exceptions (Timeout, Thread#raise, kill).
1260
+ atomic_store(&a->interrupted, 0);
1261
+ atomic_store(&a->active, 1);
1262
+ r = rb_nogvl(rendezvous_nogvl, a, rendezvous_ubf, a, RB_NOGVL_INTR_FAIL);
1263
+ atomic_store(&a->active, 0);
1264
+ if (a->finished || (r && (int)(intptr_t)r != EINTR))
1265
+ return LONG2NUM((long)(intptr_t)r);
1266
+ rb_thread_check_ints();
1267
+ }
1268
+ }
1269
+
1270
+ static VALUE rendezvous_no_des_ensure(VALUE arg)
1271
+ {
1272
+ struct rendezvous_nogvl *a;
1273
+
1274
+ a = (void *)arg;
1275
+ if (a->started && !a->finished)
1276
+ rb_nogvl(rendezvous_cancel_nogvl, a, NULL, NULL, 0);
1277
+ buf_reset(a->req);
1278
+ return Qnil;
1279
+ }
1280
+
1072
1281
  static void rendezvous_no_des(Context *c, Buf *req, Buf *res)
1073
1282
  {
1074
1283
  void *r;
1284
+ VALUE rv;
1285
+ struct rendezvous_nogvl a;
1075
1286
 
1076
1287
  if (atomic_load(&c->quit)) {
1077
1288
  buf_reset(req);
1078
1289
  rb_raise(context_disposed_error, "disposed context");
1079
1290
  }
1080
- r = rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res},
1081
- NULL, NULL, 0);
1291
+ a.context = c;
1292
+ a.req = req;
1293
+ a.res = res;
1294
+ atomic_init(&a.active, 0);
1295
+ atomic_init(&a.interrupted, 0);
1296
+ a.started = 0;
1297
+ a.finished = 0;
1298
+ a.has_rr_mtx = 0;
1299
+ rv = rb_ensure(rendezvous_no_des_body, (VALUE)&a,
1300
+ rendezvous_no_des_ensure, (VALUE)&a);
1301
+ r = (void *)(intptr_t)NUM2LONG(rv);
1302
+ if ((int)(intptr_t)r == ECANCELED)
1303
+ rb_raise(context_disposed_error, "disposed context");
1082
1304
  if (r)
1083
- rb_raise(runtime_error, "pthread_create: %s", strerror((int)(intptr_t)r));
1305
+ rb_raise(runtime_error, "single-threaded runner: %s", strerror((int)(intptr_t)r));
1084
1306
  }
1085
1307
 
1086
1308
  // send request to & receive reply from v8 thread; takes ownership of |req|
@@ -1096,7 +1318,8 @@ static VALUE rendezvous1(Context *c, Buf *req, DesCtx *d)
1096
1318
  c->exception = Qnil;
1097
1319
  // if js land didn't handle exception from ruby callback, re-raise it now
1098
1320
  if (res.len == 1 && *res.buf == 'e') {
1099
- assert(!NIL_P(r));
1321
+ if (NIL_P(r))
1322
+ rb_raise(context_disposed_error, "disposed context");
1100
1323
  rb_exc_raise(r);
1101
1324
  }
1102
1325
  r = rb_protect(deserialize, (VALUE)&(struct rendezvous_des){d, &res}, &exc);
@@ -1197,6 +1420,7 @@ static VALUE context_alloc(VALUE klass)
1197
1420
  buf_init(&c->snapshot);
1198
1421
  buf_init(&c->req);
1199
1422
  buf_init(&c->res);
1423
+ buf_init(&c->v8_req);
1200
1424
  cause = "pthread_condattr_init";
1201
1425
  if ((r = pthread_condattr_init(&cattr)))
1202
1426
  goto fail0;
@@ -1251,11 +1475,19 @@ fail0:
1251
1475
  return Qnil; // pacify compiler
1252
1476
  }
1253
1477
 
1254
- static void *context_free_thread_do(void *arg)
1478
+ static void *context_free_do(void *arg)
1255
1479
  {
1256
1480
  Context *c;
1257
1481
 
1258
1482
  c = arg;
1483
+ if (single_threaded && single_threaded_recover_after_fork(c)) {
1484
+ // The child forked while this inherited context was not idle. There is
1485
+ // no live runner thread to join and the inherited V8/pthread state is
1486
+ // not safe to tear down. A finalizer must not hang here; let the OS
1487
+ // reclaim the abandoned V8 state when the child exits.
1488
+ context_abandon(c);
1489
+ return NULL;
1490
+ }
1259
1491
  if (single_threaded && c->single_threaded_thr_started && c->single_threaded_pid == getpid()) {
1260
1492
  pthread_mutex_lock(&c->mtx);
1261
1493
  atomic_store(&c->quit, 2);
@@ -1270,31 +1502,16 @@ static void *context_free_thread_do(void *arg)
1270
1502
  return NULL;
1271
1503
  }
1272
1504
 
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
1505
  static void context_free(void *arg)
1292
1506
  {
1293
1507
  Context *c;
1294
1508
 
1295
1509
  c = arg;
1296
1510
  if (single_threaded) {
1297
- context_free_thread(c);
1511
+ // Free synchronously. A detached cleanup thread can race normal Ruby
1512
+ // process shutdown and trip glibc malloc corruption checks while V8 is
1513
+ // tearing down single-threaded contexts.
1514
+ context_free_do(c);
1298
1515
  } else {
1299
1516
  pthread_mutex_lock(&c->mtx);
1300
1517
  c->quit = 2; // 2 = v8 thread frees
@@ -1303,6 +1520,15 @@ static void context_free(void *arg)
1303
1520
  }
1304
1521
  }
1305
1522
 
1523
+ static void context_abandon(Context *c)
1524
+ {
1525
+ buf_reset(&c->snapshot);
1526
+ buf_reset(&c->req);
1527
+ buf_reset(&c->res);
1528
+ buf_reset(&c->v8_req);
1529
+ ruby_xfree(c);
1530
+ }
1531
+
1306
1532
  static void context_destroy(Context *c)
1307
1533
  {
1308
1534
  pthread_mutex_unlock(&c->mtx);
@@ -1315,6 +1541,7 @@ static void context_destroy(Context *c)
1315
1541
  buf_reset(&c->snapshot);
1316
1542
  buf_reset(&c->req);
1317
1543
  buf_reset(&c->res);
1544
+ buf_reset(&c->v8_req);
1318
1545
  ruby_xfree(c);
1319
1546
  }
1320
1547
 
@@ -1360,8 +1587,25 @@ static VALUE context_attach(VALUE self, VALUE name, VALUE proc)
1360
1587
  static void *context_dispose_do(void *arg)
1361
1588
  {
1362
1589
  Context *c;
1590
+ int r;
1363
1591
 
1364
1592
  c = arg;
1593
+ if (single_threaded) {
1594
+ if ((r = single_threaded_recover_after_fork(c)))
1595
+ return (void *)(intptr_t)r;
1596
+ }
1597
+ if (c->depth > 0) {
1598
+ r = pthread_mutex_trylock(&c->rr_mtx);
1599
+ if (!r) {
1600
+ pthread_mutex_unlock(&c->rr_mtx);
1601
+ return (void *)(intptr_t)EBUSY;
1602
+ }
1603
+ if (r != EBUSY)
1604
+ return (void *)(intptr_t)r;
1605
+ if (c->pst)
1606
+ v8_terminate_execution(c->pst);
1607
+ pthread_cond_broadcast(&c->cv);
1608
+ }
1365
1609
  if (single_threaded) {
1366
1610
  pthread_mutex_lock(&c->mtx);
1367
1611
  while (c->req.len || c->res.len)
@@ -1389,9 +1633,12 @@ static void *context_dispose_do(void *arg)
1389
1633
  static VALUE context_dispose(VALUE self)
1390
1634
  {
1391
1635
  Context *c;
1636
+ void *r;
1392
1637
 
1393
1638
  TypedData_Get_Struct(self, Context, &context_type, c);
1394
- rb_thread_call_without_gvl(context_dispose_do, c, NULL, NULL);
1639
+ r = rb_thread_call_without_gvl(context_dispose_do, c, terminate_ubf, c);
1640
+ if (r)
1641
+ rb_raise(runtime_error, "context dispose: %s", strerror((int)(intptr_t)r));
1395
1642
  return Qnil;
1396
1643
  }
1397
1644
 
@@ -1014,6 +1014,14 @@ extern "C" void v8_terminate_execution(State *pst)
1014
1014
  pst->isolate->TerminateExecution();
1015
1015
  }
1016
1016
 
1017
+ // called from ruby thread
1018
+ extern "C" void v8_cancel_terminate_execution(State *pst)
1019
+ {
1020
+ // TerminateExecution can race with V8 completing and queue a termination
1021
+ // for the next entry without IsExecutionTerminating() becoming true.
1022
+ pst->isolate->CancelTerminateExecution();
1023
+ }
1024
+
1017
1025
  extern "C" void v8_single_threaded_enter(State *pst, Context *c, void (*f)(Context *c))
1018
1026
  {
1019
1027
  State& st = *pst;
@@ -48,6 +48,7 @@ void v8_snapshot(struct State *pst, const uint8_t *p, size_t n);
48
48
  void v8_warmup(struct State *pst, const uint8_t *p, size_t n);
49
49
  void v8_low_memory_notification(struct State *pst);
50
50
  void v8_terminate_execution(struct State *pst); // called from ruby or watchdog thread
51
+ void v8_cancel_terminate_execution(struct State *pst); // called from ruby thread
51
52
  void v8_single_threaded_enter(struct State *pst, struct Context *c, void (*f)(struct Context *c));
52
53
  void v8_single_threaded_dispose(struct State *pst);
53
54
 
@@ -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.4"
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.4
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.4/CHANGELOG
140
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.4
141
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.4
142
142
  rdoc_options: []
143
143
  require_paths:
144
144
  - lib