mini_racer 0.21.3 → 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: 3e370b2c6cdb60e5081d5f97840cff28489119f0225a3c9a74ed7f6859caf40e
4
- data.tar.gz: 7405ed87278cbaa1d155bc72f8c99d8e8c744df743252eb4128b9ae9ff899d07
3
+ metadata.gz: f2e290a77bd6b38286dc4e4224d71579a1e8e41bbb8fd70029584f518383ba7f
4
+ data.tar.gz: 479d9793ade9c46075986dd53c0a7ebacf62e1afb199fc6983e5b7c4fb4e0faa
5
5
  SHA512:
6
- metadata.gz: 3c9f53c6a9052cca6a5129d023ecece59613c40c17f4c7666c728f8f6327200e34d707557ea4dcd9b626aed332b327c6c3e3f90e365c630adbec0b2d02c4b1c1
7
- data.tar.gz: 4dc0348a006f7620c4c9688c0b877f2bd9500b7ece37fba4fa6e868006af1517e4279537693fd87b2f18c2b0602067084e2332e2bf9fa862504ebaab4ddbbcfc
6
+ metadata.gz: dc69216098ec6076dc90cf731fc5fd229526a4c21d3383fdb94448aace72068f36fc6bf6910cb09d8f42bf57c6926bf1527a2fd0871c11c5b08df820643cc23e
7
+ data.tar.gz: b7b4b851b4cbc9ad35c56e39db1d5c7ccc19b23dabd9dd87f084ba37f0a59fb584c833128827d58ecdb0c99f75969383333d3b304d00f816ba544e83e5436639
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
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
+
1
6
  - 0.21.3 - 18-06-2026
2
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
3
8
  - Avoid intermittent heap corruption during `:single_threaded` context finalization, especially when forked children exit normally after touching inherited contexts
@@ -137,6 +137,8 @@ typedef struct Context
137
137
  VALUE procs; // array of js -> ruby callbacks
138
138
  VALUE exception; // pending exception or Qnil
139
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
140
142
  Buf snapshot;
141
143
  pthread_t single_threaded_thr;
142
144
  pid_t single_threaded_pid;
@@ -230,6 +232,8 @@ struct rendezvous_nogvl
230
232
  Context *context;
231
233
  Buf *req, *res;
232
234
  atomic_int active;
235
+ atomic_int interrupted;
236
+ int started, finished, has_rr_mtx;
233
237
  };
234
238
 
235
239
  struct rendezvous_des
@@ -822,11 +826,28 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
822
826
  fflush(stderr);
823
827
  }
824
828
 
825
- static void dispatch(Context *c)
829
+ static void dispatch_buf(Context *c, Buf *req)
826
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);
827
838
  buf_reset(&c->res);
828
- dispatch1(c, c->req.buf, c->req.len);
829
- 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);
830
851
  }
831
852
 
832
853
  // called by v8_isolate_and_context
@@ -859,19 +880,22 @@ void v8_thread_main(Context *c, struct State *pst)
859
880
  }
860
881
  }
861
882
 
862
- // called by v8_thread_main and from mini_racer_v8.cc,
863
- // in all cases with Context.mtx held
883
+ // called by v8_thread_main and from mini_racer_v8.cc
864
884
  void v8_dispatch(Context *c)
865
885
  {
866
- dispatch1(c, c->req.buf, c->req.len);
867
- buf_reset(&c->req);
886
+ pthread_mutex_lock(&c->mtx);
887
+ dispatch_buf(c, &c->v8_req);
888
+ pthread_mutex_unlock(&c->mtx);
868
889
  }
869
890
 
870
- // called from mini_racer_v8.cc with Context.mtx held
871
891
  // only called when inside v8_call, v8_eval, or v8_pump_message_loop
872
892
  void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
873
893
  {
894
+ pthread_mutex_lock(&c->mtx);
895
+ buf_reset(&c->v8_req);
874
896
  buf_reset(&c->req);
897
+ if (c->res.len)
898
+ c->res_ready = 1;
875
899
  pthread_cond_signal(&c->cv);
876
900
  while (!c->req.len && !atomic_load(&c->quit))
877
901
  pthread_cond_wait(&c->cv, &c->mtx);
@@ -879,17 +903,22 @@ void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
879
903
  static const uint8_t disposed[] = "edisposed context";
880
904
  *p = disposed;
881
905
  *n = sizeof(disposed) - 1;
906
+ pthread_mutex_unlock(&c->mtx);
882
907
  return;
883
908
  }
884
909
  buf_reset(&c->res);
885
- *p = c->req.buf;
886
- *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);
887
915
  }
888
916
 
889
- // called from mini_racer_v8.cc with Context.mtx held
890
917
  void v8_reply(Context *c, const uint8_t *p, size_t n)
891
918
  {
919
+ pthread_mutex_lock(&c->mtx);
892
920
  buf_put(&c->res, p, n);
921
+ pthread_mutex_unlock(&c->mtx);
893
922
  }
894
923
 
895
924
  static void v8_once_init(void)
@@ -1059,6 +1088,19 @@ static int single_threaded_runner_start(Context *c)
1059
1088
  return r;
1060
1089
  }
1061
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
+
1062
1104
  static inline void *rendezvous_nogvl(void *arg)
1063
1105
  {
1064
1106
  struct rendezvous_nogvl *a;
@@ -1067,62 +1109,77 @@ static inline void *rendezvous_nogvl(void *arg)
1067
1109
 
1068
1110
  a = arg;
1069
1111
  c = a->context;
1070
- if (single_threaded && (r = single_threaded_recover_after_fork(c)))
1071
- return (void *)(intptr_t)r;
1072
- pthread_mutex_lock(&c->rr_mtx);
1073
- atomic_store(&a->active, 1);
1074
- if (c->depth > 0 && c->depth%50 == 0) { // TODO stop steep recursion
1075
- fprintf(stderr, "mini_racer: deep js->ruby->js recursion, depth=%d\n", c->depth);
1076
- 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;
1077
1123
  }
1078
- c->depth++;
1124
+
1079
1125
  next:
1126
+ atomic_store(&a->active, 1);
1080
1127
  pthread_mutex_lock(&c->mtx);
1081
1128
  if (atomic_load(&c->quit)) {
1082
1129
  buf_reset(a->req);
1083
1130
  pthread_mutex_unlock(&c->mtx);
1084
- c->depth--;
1085
- atomic_store(&a->active, 0);
1086
- pthread_mutex_unlock(&c->rr_mtx);
1131
+ a->finished = 1;
1132
+ rendezvous_release(a);
1087
1133
  return (void *)(intptr_t)ECANCELED;
1088
1134
  }
1089
- assert(c->req.len == 0);
1090
- assert(c->res.len == 0);
1091
- buf_move(a->req, &c->req); // v8 thread takes ownership of req
1092
- if (single_threaded) {
1093
- r = single_threaded_runner_start(c);
1094
- if (r) {
1095
- buf_move(&c->req, a->req);
1096
- pthread_mutex_unlock(&c->mtx);
1097
- c->depth--;
1098
- atomic_store(&a->active, 0);
1099
- pthread_mutex_unlock(&c->rr_mtx);
1100
- return (void *)(intptr_t)r;
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
+ }
1101
1148
  }
1102
1149
  pthread_cond_signal(&c->cv);
1103
- do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
1104
- } else {
1105
- pthread_cond_signal(&c->cv);
1106
- 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;
1107
1164
  }
1108
1165
  buf_move(&c->res, a->res);
1166
+ c->res_ready = 0;
1109
1167
  pthread_cond_broadcast(&c->cv);
1110
1168
  pthread_mutex_unlock(&c->mtx);
1169
+ atomic_store(&a->active, 0);
1111
1170
  if (*a->res->buf == 'c') { // js -> ruby callback?
1112
1171
  rb_thread_call_with_gvl(rendezvous_callback, a);
1113
1172
  buf_reset(a->res);
1114
1173
  if (atomic_load(&c->quit)) {
1115
1174
  buf_reset(a->req);
1116
- c->depth--;
1117
- atomic_store(&a->active, 0);
1118
- pthread_mutex_unlock(&c->rr_mtx);
1175
+ a->finished = 1;
1176
+ rendezvous_release(a);
1119
1177
  return (void *)(intptr_t)ECANCELED;
1120
1178
  }
1121
1179
  goto next;
1122
1180
  }
1123
- c->depth--;
1124
- atomic_store(&a->active, 0);
1125
- pthread_mutex_unlock(&c->rr_mtx);
1181
+ a->finished = 1;
1182
+ rendezvous_release(a);
1126
1183
  return NULL;
1127
1184
  }
1128
1185
 
@@ -1134,9 +1191,8 @@ static void rendezvous_ubf(void *arg)
1134
1191
  a = arg;
1135
1192
  if (!atomic_load(&a->active))
1136
1193
  return;
1194
+ atomic_store(&a->interrupted, 1);
1137
1195
  c = a->context;
1138
- if (c->pst)
1139
- v8_terminate_execution(c->pst);
1140
1196
  pthread_cond_broadcast(&c->cv);
1141
1197
  }
1142
1198
 
@@ -1150,9 +1206,82 @@ static void terminate_ubf(void *arg)
1150
1206
  pthread_cond_broadcast(&c->cv);
1151
1207
  }
1152
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);
1247
+ return NULL;
1248
+ }
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
+
1153
1281
  static void rendezvous_no_des(Context *c, Buf *req, Buf *res)
1154
1282
  {
1155
1283
  void *r;
1284
+ VALUE rv;
1156
1285
  struct rendezvous_nogvl a;
1157
1286
 
1158
1287
  if (atomic_load(&c->quit)) {
@@ -1163,7 +1292,15 @@ static void rendezvous_no_des(Context *c, Buf *req, Buf *res)
1163
1292
  a.req = req;
1164
1293
  a.res = res;
1165
1294
  atomic_init(&a.active, 0);
1166
- r = rb_nogvl(rendezvous_nogvl, &a, rendezvous_ubf, &a, 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");
1167
1304
  if (r)
1168
1305
  rb_raise(runtime_error, "single-threaded runner: %s", strerror((int)(intptr_t)r));
1169
1306
  }
@@ -1283,6 +1420,7 @@ static VALUE context_alloc(VALUE klass)
1283
1420
  buf_init(&c->snapshot);
1284
1421
  buf_init(&c->req);
1285
1422
  buf_init(&c->res);
1423
+ buf_init(&c->v8_req);
1286
1424
  cause = "pthread_condattr_init";
1287
1425
  if ((r = pthread_condattr_init(&cattr)))
1288
1426
  goto fail0;
@@ -1387,6 +1525,7 @@ static void context_abandon(Context *c)
1387
1525
  buf_reset(&c->snapshot);
1388
1526
  buf_reset(&c->req);
1389
1527
  buf_reset(&c->res);
1528
+ buf_reset(&c->v8_req);
1390
1529
  ruby_xfree(c);
1391
1530
  }
1392
1531
 
@@ -1402,6 +1541,7 @@ static void context_destroy(Context *c)
1402
1541
  buf_reset(&c->snapshot);
1403
1542
  buf_reset(&c->req);
1404
1543
  buf_reset(&c->res);
1544
+ buf_reset(&c->v8_req);
1405
1545
  ruby_xfree(c);
1406
1546
  }
1407
1547
 
@@ -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.3"
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.3
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.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
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