polyphony 0.47.3 → 0.49.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +318 -295
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE +1 -1
  5. data/TODO.md +40 -27
  6. data/examples/core/supervisor.rb +3 -3
  7. data/examples/core/worker-thread.rb +3 -4
  8. data/examples/io/tcp_proxy.rb +32 -0
  9. data/examples/io/unix_socket.rb +26 -0
  10. data/examples/performance/line_splitting.rb +34 -0
  11. data/examples/performance/loop.rb +32 -0
  12. data/examples/performance/thread-vs-fiber/polyphony_server.rb +6 -0
  13. data/ext/polyphony/backend_common.h +2 -2
  14. data/ext/polyphony/backend_io_uring.c +42 -83
  15. data/ext/polyphony/backend_libev.c +32 -77
  16. data/ext/polyphony/event.c +1 -1
  17. data/ext/polyphony/polyphony.c +0 -2
  18. data/ext/polyphony/polyphony.h +5 -4
  19. data/ext/polyphony/queue.c +2 -2
  20. data/ext/polyphony/thread.c +9 -28
  21. data/lib/polyphony.rb +1 -0
  22. data/lib/polyphony/adapters/postgres.rb +3 -3
  23. data/lib/polyphony/core/global_api.rb +14 -2
  24. data/lib/polyphony/core/thread_pool.rb +3 -1
  25. data/lib/polyphony/core/throttler.rb +1 -1
  26. data/lib/polyphony/core/timer.rb +72 -0
  27. data/lib/polyphony/extensions/fiber.rb +32 -8
  28. data/lib/polyphony/extensions/io.rb +8 -14
  29. data/lib/polyphony/extensions/openssl.rb +4 -4
  30. data/lib/polyphony/extensions/socket.rb +68 -16
  31. data/lib/polyphony/version.rb +1 -1
  32. data/polyphony.gemspec +1 -1
  33. data/test/helper.rb +1 -0
  34. data/test/test_backend.rb +1 -1
  35. data/test/test_fiber.rb +64 -1
  36. data/test/test_global_api.rb +30 -0
  37. data/test/test_io.rb +26 -0
  38. data/test/test_socket.rb +32 -6
  39. data/test/test_supervise.rb +2 -1
  40. data/test/test_timer.rb +122 -0
  41. metadata +9 -4
  42. data/ext/polyphony/backend.h +0 -26
@@ -13,7 +13,6 @@
13
13
  #include "../libev/ev.h"
14
14
  #include "ruby/io.h"
15
15
 
16
- VALUE cTCPSocket;
17
16
  VALUE SYM_libev;
18
17
 
19
18
  ID ID_ivar_is_nonblocking;
@@ -41,11 +40,14 @@ inline void io_set_nonblock(rb_io_t *fptr, VALUE io) {
41
40
  }
42
41
 
43
42
  typedef struct Backend_t {
43
+ // common fields
44
+ unsigned int currently_polling;
45
+ unsigned int pending_count;
46
+ unsigned int poll_no_wait_count;
47
+
48
+ // implementation-specific fields
44
49
  struct ev_loop *ev_loop;
45
50
  struct ev_async break_async;
46
- int running;
47
- int ref_count;
48
- int run_no_wait_count;
49
51
  } Backend_t;
50
52
 
51
53
  static size_t Backend_size(const void *ptr) {
@@ -84,9 +86,9 @@ static VALUE Backend_initialize(VALUE self) {
84
86
  ev_async_start(backend->ev_loop, &backend->break_async);
85
87
  ev_unref(backend->ev_loop); // don't count the break_async watcher
86
88
 
87
- backend->running = 0;
88
- backend->ref_count = 0;
89
- backend->run_no_wait_count = 0;
89
+ backend->currently_polling = 0;
90
+ backend->pending_count = 0;
91
+ backend->poll_no_wait_count = 0;
90
92
 
91
93
  return Qnil;
92
94
  }
@@ -117,42 +119,11 @@ VALUE Backend_post_fork(VALUE self) {
117
119
  return self;
118
120
  }
119
121
 
120
- VALUE Backend_ref(VALUE self) {
122
+ unsigned int Backend_pending_count(VALUE self) {
121
123
  Backend_t *backend;
122
124
  GetBackend(self, backend);
123
125
 
124
- backend->ref_count++;
125
- return self;
126
- }
127
-
128
- VALUE Backend_unref(VALUE self) {
129
- Backend_t *backend;
130
- GetBackend(self, backend);
131
-
132
- backend->ref_count--;
133
- return self;
134
- }
135
-
136
- int Backend_ref_count(VALUE self) {
137
- Backend_t *backend;
138
- GetBackend(self, backend);
139
-
140
- return backend->ref_count;
141
- }
142
-
143
- void Backend_reset_ref_count(VALUE self) {
144
- Backend_t *backend;
145
- GetBackend(self, backend);
146
-
147
- backend->ref_count = 0;
148
- }
149
-
150
- VALUE Backend_pending_count(VALUE self) {
151
- int count;
152
- Backend_t *backend;
153
- GetBackend(self, backend);
154
- count = ev_pending_count(backend->ev_loop);
155
- return INT2NUM(count);
126
+ return backend->pending_count;
156
127
  }
157
128
 
158
129
  VALUE Backend_poll(VALUE self, VALUE nowait, VALUE current_fiber, VALUE runqueue) {
@@ -161,19 +132,19 @@ VALUE Backend_poll(VALUE self, VALUE nowait, VALUE current_fiber, VALUE runqueue
161
132
  GetBackend(self, backend);
162
133
 
163
134
  if (is_nowait) {
164
- backend->run_no_wait_count++;
165
- if (backend->run_no_wait_count < 10) return self;
135
+ backend->poll_no_wait_count++;
136
+ if (backend->poll_no_wait_count < 10) return self;
166
137
 
167
138
  long runnable_count = Runqueue_len(runqueue);
168
- if (backend->run_no_wait_count < runnable_count) return self;
139
+ if (backend->poll_no_wait_count < runnable_count) return self;
169
140
  }
170
141
 
171
- backend->run_no_wait_count = 0;
142
+ backend->poll_no_wait_count = 0;
172
143
 
173
144
  COND_TRACE(2, SYM_fiber_event_poll_enter, current_fiber);
174
- backend->running = 1;
145
+ backend->currently_polling = 1;
175
146
  ev_run(backend->ev_loop, is_nowait ? EVRUN_NOWAIT : EVRUN_ONCE);
176
- backend->running = 0;
147
+ backend->currently_polling = 0;
177
148
  COND_TRACE(2, SYM_fiber_event_poll_leave, current_fiber);
178
149
 
179
150
  return self;
@@ -183,7 +154,7 @@ VALUE Backend_wakeup(VALUE self) {
183
154
  Backend_t *backend;
184
155
  GetBackend(self, backend);
185
156
 
186
- if (backend->running) {
157
+ if (backend->currently_polling) {
187
158
  // Since the loop will run until at least one event has occurred, we signal
188
159
  // the selector's associated async watcher, which will cause the ev loop to
189
160
  // return. In contrast to using `ev_break` to break out of the loop, which
@@ -494,7 +465,7 @@ VALUE Backend_write_m(int argc, VALUE *argv, VALUE self) {
494
465
  Backend_writev(self, argv[0], argc - 1, argv + 1);
495
466
  }
496
467
 
497
- VALUE Backend_accept(VALUE self, VALUE sock) {
468
+ VALUE Backend_accept(VALUE self, VALUE server_socket, VALUE socket_class) {
498
469
  Backend_t *backend;
499
470
  struct libev_io watcher;
500
471
  rb_io_t *fptr;
@@ -502,12 +473,12 @@ VALUE Backend_accept(VALUE self, VALUE sock) {
502
473
  struct sockaddr addr;
503
474
  socklen_t len = (socklen_t)sizeof addr;
504
475
  VALUE switchpoint_result = Qnil;
505
- VALUE underlying_sock = rb_ivar_get(sock, ID_ivar_io);
506
- if (underlying_sock != Qnil) sock = underlying_sock;
476
+ VALUE underlying_sock = rb_ivar_get(server_socket, ID_ivar_io);
477
+ if (underlying_sock != Qnil) server_socket = underlying_sock;
507
478
 
508
479
  GetBackend(self, backend);
509
- GetOpenFile(sock, fptr);
510
- io_set_nonblock(fptr, sock);
480
+ GetOpenFile(server_socket, fptr);
481
+ io_set_nonblock(fptr, server_socket);
511
482
  watcher.fiber = Qnil;
512
483
  while (1) {
513
484
  fd = accept(fptr->fd, &addr, &len);
@@ -529,7 +500,7 @@ VALUE Backend_accept(VALUE self, VALUE sock) {
529
500
  goto error;
530
501
  }
531
502
 
532
- socket = rb_obj_alloc(cTCPSocket);
503
+ socket = rb_obj_alloc(socket_class);
533
504
  MakeOpenFile(socket, fp);
534
505
  rb_update_max_fd(fd);
535
506
  fp->fd = fd;
@@ -550,7 +521,7 @@ error:
550
521
  return RAISE_EXCEPTION(switchpoint_result);
551
522
  }
552
523
 
553
- VALUE Backend_accept_loop(VALUE self, VALUE sock) {
524
+ VALUE Backend_accept_loop(VALUE self, VALUE server_socket, VALUE socket_class) {
554
525
  Backend_t *backend;
555
526
  struct libev_io watcher;
556
527
  rb_io_t *fptr;
@@ -559,12 +530,12 @@ VALUE Backend_accept_loop(VALUE self, VALUE sock) {
559
530
  socklen_t len = (socklen_t)sizeof addr;
560
531
  VALUE switchpoint_result = Qnil;
561
532
  VALUE socket = Qnil;
562
- VALUE underlying_sock = rb_ivar_get(sock, ID_ivar_io);
563
- if (underlying_sock != Qnil) sock = underlying_sock;
533
+ VALUE underlying_sock = rb_ivar_get(server_socket, ID_ivar_io);
534
+ if (underlying_sock != Qnil) server_socket = underlying_sock;
564
535
 
565
536
  GetBackend(self, backend);
566
- GetOpenFile(sock, fptr);
567
- io_set_nonblock(fptr, sock);
537
+ GetOpenFile(server_socket, fptr);
538
+ io_set_nonblock(fptr, server_socket);
568
539
  watcher.fiber = Qnil;
569
540
 
570
541
  while (1) {
@@ -586,7 +557,7 @@ VALUE Backend_accept_loop(VALUE self, VALUE sock) {
586
557
  goto error;
587
558
  }
588
559
 
589
- socket = rb_obj_alloc(cTCPSocket);
560
+ socket = rb_obj_alloc(socket_class);
590
561
  MakeOpenFile(socket, fp);
591
562
  rb_update_max_fd(fd);
592
563
  fp->fd = fd;
@@ -849,19 +820,12 @@ VALUE Backend_kind(VALUE self) {
849
820
  void Init_Backend() {
850
821
  ev_set_allocator(xrealloc);
851
822
 
852
- rb_require("socket");
853
- cTCPSocket = rb_const_get(rb_cObject, rb_intern("TCPSocket"));
854
-
855
823
  VALUE cBackend = rb_define_class_under(mPolyphony, "Backend", rb_cData);
856
824
  rb_define_alloc_func(cBackend, Backend_allocate);
857
825
 
858
826
  rb_define_method(cBackend, "initialize", Backend_initialize, 0);
859
827
  rb_define_method(cBackend, "finalize", Backend_finalize, 0);
860
828
  rb_define_method(cBackend, "post_fork", Backend_post_fork, 0);
861
- rb_define_method(cBackend, "pending_count", Backend_pending_count, 0);
862
-
863
- rb_define_method(cBackend, "ref", Backend_ref, 0);
864
- rb_define_method(cBackend, "unref", Backend_unref, 0);
865
829
 
866
830
  rb_define_method(cBackend, "poll", Backend_poll, 3);
867
831
  rb_define_method(cBackend, "break", Backend_wakeup, 0);
@@ -869,8 +833,8 @@ void Init_Backend() {
869
833
  rb_define_method(cBackend, "read", Backend_read, 4);
870
834
  rb_define_method(cBackend, "read_loop", Backend_read_loop, 1);
871
835
  rb_define_method(cBackend, "write", Backend_write_m, -1);
872
- rb_define_method(cBackend, "accept", Backend_accept, 1);
873
- rb_define_method(cBackend, "accept_loop", Backend_accept_loop, 1);
836
+ rb_define_method(cBackend, "accept", Backend_accept, 2);
837
+ rb_define_method(cBackend, "accept_loop", Backend_accept_loop, 2);
874
838
  rb_define_method(cBackend, "connect", Backend_connect, 3);
875
839
  rb_define_method(cBackend, "recv", Backend_recv, 3);
876
840
  rb_define_method(cBackend, "recv_loop", Backend_read_loop, 1);
@@ -886,15 +850,6 @@ void Init_Backend() {
886
850
 
887
851
  ID_ivar_is_nonblocking = rb_intern("@is_nonblocking");
888
852
  SYM_libev = ID2SYM(rb_intern("libev"));
889
-
890
- __BACKEND__.pending_count = Backend_pending_count;
891
- __BACKEND__.poll = Backend_poll;
892
- __BACKEND__.ref = Backend_ref;
893
- __BACKEND__.ref_count = Backend_ref_count;
894
- __BACKEND__.reset_ref_count = Backend_reset_ref_count;
895
- __BACKEND__.unref = Backend_unref;
896
- __BACKEND__.wait_event = Backend_wait_event;
897
- __BACKEND__.wakeup = Backend_wakeup;
898
853
  }
899
854
 
900
855
  #endif // POLYPHONY_BACKEND_LIBEV
@@ -66,7 +66,7 @@ VALUE Event_await(VALUE self) {
66
66
 
67
67
  VALUE backend = rb_ivar_get(rb_thread_current(), ID_ivar_backend);
68
68
  event->waiting_fiber = rb_fiber_current();
69
- VALUE switchpoint_result = __BACKEND__.wait_event(backend, Qnil);
69
+ VALUE switchpoint_result = Backend_wait_event(backend, Qnil);
70
70
  event->waiting_fiber = Qnil;
71
71
 
72
72
  RAISE_IF_EXCEPTION(switchpoint_result);
@@ -22,8 +22,6 @@ ID ID_R;
22
22
  ID ID_W;
23
23
  ID ID_RW;
24
24
 
25
- backend_interface_t backend_interface;
26
-
27
25
  VALUE Polyphony_snooze(VALUE self) {
28
26
  VALUE ret;
29
27
  VALUE fiber = rb_fiber_current();
@@ -4,7 +4,6 @@
4
4
  #include <execinfo.h>
5
5
 
6
6
  #include "ruby.h"
7
- #include "backend.h"
8
7
  #include "runqueue_ring_buffer.h"
9
8
 
10
9
  // debugging
@@ -32,9 +31,6 @@
32
31
  // Fiber#transfer
33
32
  #define FIBER_TRANSFER(fiber, value) rb_funcall(fiber, ID_transfer, 1, value)
34
33
 
35
- extern backend_interface_t backend_interface;
36
- #define __BACKEND__ (backend_interface)
37
-
38
34
  extern VALUE mPolyphony;
39
35
  extern VALUE cQueue;
40
36
  extern VALUE cEvent;
@@ -92,6 +88,11 @@ void Runqueue_clear(VALUE self);
92
88
  long Runqueue_len(VALUE self);
93
89
  int Runqueue_empty_p(VALUE self);
94
90
 
91
+ unsigned int Backend_pending_count(VALUE self);
92
+ VALUE Backend_poll(VALUE self, VALUE nowait, VALUE current_fiber, VALUE runqueue);
93
+ VALUE Backend_wait_event(VALUE self, VALUE raise_on_exception);
94
+ VALUE Backend_wakeup(VALUE self);
95
+
95
96
  VALUE Thread_schedule_fiber(VALUE thread, VALUE fiber, VALUE value);
96
97
  VALUE Thread_schedule_fiber_with_priority(VALUE thread, VALUE fiber, VALUE value);
97
98
  VALUE Thread_switch_fiber(VALUE thread);
@@ -86,7 +86,7 @@ inline void capped_queue_block_push(Queue_t *queue) {
86
86
  if (queue->capacity > queue->values.count) Fiber_make_runnable(fiber, Qnil);
87
87
 
88
88
  ring_buffer_push(&queue->push_queue, fiber);
89
- switchpoint_result = __BACKEND__.wait_event(backend, Qnil);
89
+ switchpoint_result = Backend_wait_event(backend, Qnil);
90
90
  ring_buffer_delete(&queue->push_queue, fiber);
91
91
 
92
92
  RAISE_IF_EXCEPTION(switchpoint_result);
@@ -131,7 +131,7 @@ VALUE Queue_shift(VALUE self) {
131
131
  if (queue->values.count) Fiber_make_runnable(fiber, Qnil);
132
132
 
133
133
  ring_buffer_push(&queue->shift_queue, fiber);
134
- VALUE switchpoint_result = __BACKEND__.wait_event(backend, Qnil);
134
+ VALUE switchpoint_result = Backend_wait_event(backend, Qnil);
135
135
  ring_buffer_delete(&queue->shift_queue, fiber);
136
136
 
137
137
  RAISE_IF_EXCEPTION(switchpoint_result);
@@ -17,16 +17,6 @@ static VALUE Thread_setup_fiber_scheduling(VALUE self) {
17
17
  return self;
18
18
  }
19
19
 
20
- int Thread_fiber_ref_count(VALUE self) {
21
- VALUE backend = rb_ivar_get(self, ID_ivar_backend);
22
- return NUM2INT(__BACKEND__.ref_count(backend));
23
- }
24
-
25
- inline void Thread_fiber_reset_ref_count(VALUE self) {
26
- VALUE backend = rb_ivar_get(self, ID_ivar_backend);
27
- __BACKEND__.reset_ref_count(backend);
28
- }
29
-
30
20
  static VALUE SYM_scheduled_fibers;
31
21
  static VALUE SYM_pending_watchers;
32
22
 
@@ -39,7 +29,7 @@ static VALUE Thread_fiber_scheduling_stats(VALUE self) {
39
29
  long scheduled_count = Runqueue_len(runqueue);
40
30
  rb_hash_aset(stats, SYM_scheduled_fibers, INT2NUM(scheduled_count));
41
31
 
42
- pending_count = __BACKEND__.pending_count(backend);
32
+ pending_count = Backend_pending_count(backend);
43
33
  rb_hash_aset(stats, SYM_pending_watchers, INT2NUM(pending_count));
44
34
 
45
35
  return stats;
@@ -64,7 +54,7 @@ void schedule_fiber(VALUE self, VALUE fiber, VALUE value, int prioritize) {
64
54
  // happen, not knowing that it there's already a fiber ready to run in its
65
55
  // run queue.
66
56
  VALUE backend = rb_ivar_get(self,ID_ivar_backend);
67
- __BACKEND__.wakeup(backend);
57
+ Backend_wakeup(backend);
68
58
  }
69
59
  }
70
60
  }
@@ -84,25 +74,24 @@ VALUE Thread_switch_fiber(VALUE self) {
84
74
  VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
85
75
  runqueue_entry next;
86
76
  VALUE backend = rb_ivar_get(self, ID_ivar_backend);
87
- int ref_count;
88
- int backend_was_polled = 0;
77
+ unsigned int pending_count = Backend_pending_count(backend);
78
+ unsigned int backend_was_polled = 0;
89
79
 
90
80
  if (__tracing_enabled__ && (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse))
91
81
  TRACE(2, SYM_fiber_switchpoint, current_fiber);
92
82
 
93
- ref_count = __BACKEND__.ref_count(backend);
94
83
  while (1) {
95
84
  next = Runqueue_shift(runqueue);
96
85
  if (next.fiber != Qnil) {
97
- if (backend_was_polled == 0 && ref_count > 0) {
86
+ if (!backend_was_polled && pending_count) {
98
87
  // this prevents event starvation in case the run queue never empties
99
- __BACKEND__.poll(backend, Qtrue, current_fiber, runqueue);
88
+ Backend_poll(backend, Qtrue, current_fiber, runqueue);
100
89
  }
101
90
  break;
102
91
  }
103
- if (ref_count == 0) break;
92
+ if (pending_count == 0) break;
104
93
 
105
- __BACKEND__.poll(backend, Qnil, current_fiber, runqueue);
94
+ Backend_poll(backend, Qnil, current_fiber, runqueue);
106
95
  backend_was_polled = 1;
107
96
  }
108
97
 
@@ -118,20 +107,13 @@ VALUE Thread_switch_fiber(VALUE self) {
118
107
  next.value : FIBER_TRANSFER(next.fiber, next.value);
119
108
  }
120
109
 
121
- VALUE Thread_reset_fiber_scheduling(VALUE self) {
122
- VALUE queue = rb_ivar_get(self, ID_ivar_runqueue);
123
- Runqueue_clear(queue);
124
- Thread_fiber_reset_ref_count(self);
125
- return self;
126
- }
127
-
128
110
  VALUE Thread_fiber_schedule_and_wakeup(VALUE self, VALUE fiber, VALUE resume_obj) {
129
111
  VALUE backend = rb_ivar_get(self, ID_ivar_backend);
130
112
  if (fiber != Qnil) {
131
113
  Thread_schedule_fiber_with_priority(self, fiber, resume_obj);
132
114
  }
133
115
 
134
- if (__BACKEND__.wakeup(backend) == Qnil) {
116
+ if (Backend_wakeup(backend) == Qnil) {
135
117
  // we're not inside the ev_loop, so we just do a switchpoint
136
118
  Thread_switch_fiber(self);
137
119
  }
@@ -146,7 +128,6 @@ VALUE Thread_debug(VALUE self) {
146
128
 
147
129
  void Init_Thread() {
148
130
  rb_define_method(rb_cThread, "setup_fiber_scheduling", Thread_setup_fiber_scheduling, 0);
149
- rb_define_method(rb_cThread, "reset_fiber_scheduling", Thread_reset_fiber_scheduling, 0);
150
131
  rb_define_method(rb_cThread, "fiber_scheduling_stats", Thread_fiber_scheduling_stats, 0);
151
132
  rb_define_method(rb_cThread, "schedule_and_wakeup", Thread_fiber_schedule_and_wakeup, 2);
152
133
 
@@ -14,6 +14,7 @@ Thread.current.backend = Polyphony::Backend.new
14
14
  require_relative './polyphony/core/global_api'
15
15
  require_relative './polyphony/core/resource_pool'
16
16
  require_relative './polyphony/core/sync'
17
+ require_relative './polyphony/core/timer'
17
18
  require_relative './polyphony/net'
18
19
  require_relative './polyphony/adapters/process'
19
20
 
@@ -11,7 +11,7 @@ module ::PG
11
11
 
12
12
  def self.connect_async(conn)
13
13
  socket_io = conn.socket_io
14
- loop do
14
+ while true
15
15
  res = conn.connect_poll
16
16
  case res
17
17
  when PGRES_POLLING_FAILED then raise Error, conn.error_message
@@ -23,7 +23,7 @@ module ::PG
23
23
  end
24
24
 
25
25
  def self.connect_sync(conn)
26
- loop do
26
+ while true
27
27
  res = conn.connect_poll
28
28
  case res
29
29
  when PGRES_POLLING_FAILED
@@ -96,7 +96,7 @@ class ::PG::Connection
96
96
  def wait_for_notify(timeout = nil, &block)
97
97
  return move_on_after(timeout) { wait_for_notify(&block) } if timeout
98
98
 
99
- loop do
99
+ while true
100
100
  Thread.current.backend.wait_io(socket_io, false)
101
101
  consume_input
102
102
  notice = notifies
@@ -59,7 +59,15 @@ module Polyphony
59
59
  throttled_loop(rate: rate, interval: interval, &block)
60
60
  end
61
61
  else
62
- Fiber.current.spin(tag, caller) { loop(&block) }
62
+ spin_looped_block(tag, caller, block)
63
+ end
64
+ end
65
+
66
+ def spin_looped_block(tag, caller, block)
67
+ Fiber.current.spin(tag, caller) do
68
+ block.call while true
69
+ rescue LocalJumpError, StopIteration
70
+ # break called or StopIteration raised
63
71
  end
64
72
  end
65
73
 
@@ -133,8 +141,12 @@ module Polyphony
133
141
  if opts[:count]
134
142
  opts[:count].times { |_i| throttler.(&block) }
135
143
  else
136
- loop { throttler.(&block) }
144
+ while true
145
+ throttler.(&block)
146
+ end
137
147
  end
148
+ rescue LocalJumpError, StopIteration
149
+ # break called or StopIteration raised
138
150
  ensure
139
151
  throttler&.stop
140
152
  end