polyphony 0.48.0 → 0.50.1

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: 3a699d313072b1beba17c900491dd2a4f454bc8faea69eb157634e98cba7ee90
4
- data.tar.gz: 3d8579b2c927c34876560ce878e960b9bc48a857ce43890dc069b4f0fa5ad75f
3
+ metadata.gz: 126431d6f1b6c547d57dd99834c5a2371b4f2b3399df5556f55fdf7ff8465702
4
+ data.tar.gz: bf4e8d8cbde0a0d25930996f21ca723ffb68c065688ea2423f96b2a1e8946a80
5
5
  SHA512:
6
- metadata.gz: d6efca5f42f21fe25ab5e6064675c2806dcad9d86e2e80c908987fcce855dd520ab51c4913e5a9001d3e4a84880a6606d124fa46dedff7c2e437a1ef6e156a82
7
- data.tar.gz: 73ad6c59d4f7e9f8f0d317f480be1ba33c1a70dcf130a717693c2bb5a9ef3e226bfff3bdb635461b505bd2e9bbec7a2174c6a206bee2a94709ba9739ab087956
6
+ metadata.gz: 9b6cb2b64ac16e70941195aa17c019abdb026bac2d440550841f839bae96d248b87609979e6e9d7c57f9905845209714d90f74caa4488d8ba66afa96c35aa425
7
+ data.tar.gz: '093360cb9c3f5e81d7d23f1e707bf6399608475822b56d0171438748e65752adb2784b1ab36ce850a26840c615f73ce1fcc74ca842018361019269f7d3c31aa9'
@@ -1,3 +1,33 @@
1
+ ## 0.50.1
2
+
3
+ - Set `IOSQE_ASYNC` flag in io_uring backend
4
+ - Fix error handling in `Backend#waitpid`
5
+ - Reimplement libev backend's `#waitpid` by using pidfd_open (in similar manner
6
+ to the io_uring backend)
7
+
8
+ ## 0.50.0
9
+
10
+ - Use `Process::CLOCK_MONOTONIC` in Timer
11
+ - Add `Timer#sleep`, `Timer#after`, `Timer#every`
12
+ - Prevent fiber from being resumed after terminating
13
+ - Add `Thread#fiber_index_of` method
14
+ - Use `Backend#wait_event` in `Fiber#await`
15
+
16
+ ## 0.49.2
17
+
18
+ - Fix hang with 100s or more child fibers when terminating
19
+ - Fix double pending_count increment in io_uring backend
20
+
21
+ ## 0.49.1
22
+
23
+ - Use `TCPSocket` instead of `Socket` in `Net.tcp_connect`
24
+ - Catch `Errno::ERSCH` in `Process.kill_and_await`
25
+ - Set io_uring queue size to 2048
26
+
27
+ ## 0.49.0
28
+
29
+ - Implement `Polyphony::Timer` for performant timeouts
30
+
1
31
  ## 0.48.0
2
32
 
3
33
  - Implement graceful shutdown
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.48.0)
4
+ polyphony (0.50.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Sharon Rosner
3
+ Copyright (c) 2021 Sharon Rosner
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/TODO.md CHANGED
@@ -1,4 +1,10 @@
1
- - Override stock `SizedQueue` impl with Queue with capacity
1
+ - Check segfault when resetting a `cancel_after` timeout lots of times at very high rate
2
+ - Check why `throttled_loop` inside of `move_on_after` fails to stop
3
+
4
+ - Commented out `io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);` in `io_uring_backend_defer_submit_and_await`:
5
+ - This flag should be set for I/O ops, not for other stuff
6
+
7
+ - Override stock `::SizedQueue` impl with Queue with capacity
2
8
 
3
9
  - Add support for `break` and `StopIteration` in all loops (with tests)
4
10
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ def process
7
+ p :b_start
8
+ sleep 1
9
+ p :b_stop
10
+ end
11
+
12
+ spin do
13
+ p :a_start
14
+ spin { process }
15
+ sleep 60
16
+ p :a_stop
17
+ end
18
+
19
+ p :main_start
20
+ sleep 120
21
+ p :main_stop
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ main = Fiber.current
7
+ spin do
8
+ sleep 0.1
9
+ main.schedule(:foo)
10
+ end
11
+
12
+ v = suspend
13
+ puts "v => #{v.inspect}"
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ main = Fiber.current
7
+ spin do
8
+ sleep 0.1
9
+ main.terminate
10
+ end
11
+
12
+ sleep
@@ -26,15 +26,13 @@ def write_response(socket)
26
26
  socket.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
27
27
  end
28
28
 
29
- server = TCPServer.open('0.0.0.0', 1234)
30
- puts "pid #{Process.pid} Polyphony (#{Thread.current.backend.kind}) listening on port 1234"
29
+ server = TCPServer.open('0.0.0.0', 4411)
30
+ puts "pid #{Process.pid} Polyphony (#{Thread.current.backend.kind}) listening on port 4411"
31
31
 
32
32
  spin_loop(interval: 10) do
33
33
  p Thread.current.fiber_scheduling_stats
34
34
  end
35
35
 
36
- GC.disable
37
-
38
36
  server.accept_loop do |c|
39
37
  spin { handle_client(c) }
40
38
  end
@@ -3,6 +3,14 @@
3
3
  #include "ruby.h"
4
4
  #include "ruby/io.h"
5
5
 
6
+ #ifndef __NR_pidfd_open
7
+ #define __NR_pidfd_open 434 /* System call # on most architectures */
8
+ #endif
9
+
10
+ static int pidfd_open(pid_t pid, unsigned int flags) {
11
+ return syscall(__NR_pidfd_open, pid, flags);
12
+ }
13
+
6
14
  //////////////////////////////////////////////////////////////////////
7
15
  //////////////////////////////////////////////////////////////////////
8
16
  // the following is copied verbatim from the Ruby source code (io.c)
@@ -114,7 +122,7 @@ inline double current_time() {
114
122
  struct timespec ts;
115
123
  clock_gettime(CLOCK_MONOTONIC, &ts);
116
124
  long long ns = ts.tv_sec;
117
- ns = ns * 1000000000 + ts.tv_nsec;
125
+ ns = ns * 1e9 + ts.tv_nsec;
118
126
  double t = ns;
119
127
  return t / 1e9;
120
128
  }
@@ -126,4 +134,16 @@ inline VALUE backend_timeout_exception(VALUE exception) {
126
134
  return rb_funcall(exception, ID_new, 0);
127
135
  else
128
136
  return rb_funcall(rb_eRuntimeError, ID_new, 1, exception);
129
- }
137
+ }
138
+
139
+ VALUE Backend_timeout_safe(VALUE arg) {
140
+ return rb_yield(arg);
141
+ }
142
+
143
+ VALUE Backend_timeout_rescue(VALUE arg, VALUE exception) {
144
+ return exception;
145
+ }
146
+
147
+ VALUE Backend_timeout_ensure_safe(VALUE arg) {
148
+ return rb_rescue2(Backend_timeout_safe, Qnil, Backend_timeout_rescue, Qnil, rb_eException, (VALUE)0);
149
+ }
@@ -19,14 +19,6 @@
19
19
  #include "ruby/thread.h"
20
20
  #include "backend_io_uring_context.h"
21
21
 
22
- #ifndef __NR_pidfd_open
23
- #define __NR_pidfd_open 434 /* System call # on most architectures */
24
- #endif
25
-
26
- static int pidfd_open(pid_t pid, unsigned int flags) {
27
- return syscall(__NR_pidfd_open, pid, flags);
28
- }
29
-
30
22
  VALUE SYM_io_uring;
31
23
 
32
24
  typedef struct Backend_t {
@@ -72,7 +64,7 @@ static VALUE Backend_initialize(VALUE self) {
72
64
  backend->pending_count = 0;
73
65
  backend->poll_no_wait_count = 0;
74
66
  backend->pending_sqes = 0;
75
- backend->prepared_limit = 256;
67
+ backend->prepared_limit = 2048;
76
68
 
77
69
  context_store_initialize(&backend->store);
78
70
  io_uring_queue_init(backend->prepared_limit, &backend->ring, 0);
@@ -260,12 +252,10 @@ int io_uring_backend_defer_submit_and_await(
260
252
  VALUE switchpoint_result = Qnil;
261
253
 
262
254
  io_uring_sqe_set_data(sqe, ctx);
263
- // io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);
255
+ io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);
264
256
  io_uring_backend_defer_submit(backend);
265
257
 
266
- backend->pending_count++;
267
258
  switchpoint_result = backend_await(backend);
268
- backend->pending_count--;
269
259
 
270
260
  if (!ctx->completed) {
271
261
  ctx->result = -ECANCELED;
@@ -813,18 +803,6 @@ VALUE Backend_timer_loop(VALUE self, VALUE interval) {
813
803
  }
814
804
  }
815
805
 
816
- VALUE Backend_timeout_safe(VALUE arg) {
817
- return rb_yield(arg);
818
- }
819
-
820
- VALUE Backend_timeout_rescue(VALUE arg, VALUE exception) {
821
- return exception;
822
- }
823
-
824
- VALUE Backend_timeout_ensure_safe(VALUE arg) {
825
- return rb_rescue2(Backend_timeout_safe, Qnil, Backend_timeout_rescue, Qnil, rb_eException, (VALUE)0);
826
- }
827
-
828
806
  struct Backend_timeout_ctx {
829
807
  Backend_t *backend;
830
808
  op_context_t *ctx;
@@ -863,7 +841,6 @@ VALUE Backend_timeout(int argc, VALUE *argv, VALUE self) {
863
841
  ctx->resume_value = timeout;
864
842
  io_uring_prep_timeout(sqe, &ts, 0, 0);
865
843
  io_uring_sqe_set_data(sqe, ctx);
866
- io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);
867
844
  io_uring_backend_defer_submit(backend);
868
845
 
869
846
  struct Backend_timeout_ctx timeout_ctx = {backend, ctx};
@@ -881,19 +858,28 @@ VALUE Backend_timeout(int argc, VALUE *argv, VALUE self) {
881
858
  }
882
859
 
883
860
  VALUE Backend_waitpid(VALUE self, VALUE pid) {
884
- Backend_t *backend;
885
861
  int pid_int = NUM2INT(pid);
886
862
  int fd = pidfd_open(pid_int, 0);
887
- GetBackend(self, backend);
888
-
889
- VALUE resume_value = io_uring_backend_wait_fd(backend, fd, 0);
890
- close(fd);
891
863
 
892
- RAISE_IF_EXCEPTION(resume_value);
893
- RB_GC_GUARD(resume_value);
864
+ if (fd >= 0) {
865
+ Backend_t *backend;
866
+ GetBackend(self, backend);
894
867
 
868
+ VALUE resume_value = io_uring_backend_wait_fd(backend, fd, 0);
869
+ close(fd);
870
+ RAISE_IF_EXCEPTION(resume_value);
871
+ RB_GC_GUARD(resume_value);
872
+ }
873
+
895
874
  int status;
896
875
  pid_t ret = waitpid(pid_int, &status, WNOHANG);
876
+ if (ret < 0) {
877
+ int e = errno;
878
+ if (e == ECHILD)
879
+ ret = pid_int;
880
+ else
881
+ rb_syserr_fail(e, strerror(e));
882
+ }
897
883
  return rb_ary_new_from_args(2, INT2NUM(ret), INT2NUM(WEXITSTATUS(status)));
898
884
  }
899
885
 
@@ -8,6 +8,8 @@
8
8
  #include <netinet/in.h>
9
9
  #include <arpa/inet.h>
10
10
  #include <stdnoreturn.h>
11
+ #include <sys/types.h>
12
+ #include <sys/wait.h>
11
13
 
12
14
  #include "polyphony.h"
13
15
  #include "../libev/ev.h"
@@ -685,26 +687,12 @@ noreturn VALUE Backend_timer_loop(VALUE self, VALUE interval) {
685
687
  RB_GC_GUARD(switchpoint_result);
686
688
 
687
689
  rb_yield(Qnil);
688
-
689
- while (1) {
690
+ do {
690
691
  next_time += interval_d;
691
- if (next_time > now) break;
692
- }
692
+ } while (next_time <= now);
693
693
  }
694
694
  }
695
695
 
696
- VALUE Backend_timeout_safe(VALUE arg) {
697
- return rb_yield(arg);
698
- }
699
-
700
- VALUE Backend_timeout_rescue(VALUE arg, VALUE exception) {
701
- return exception;
702
- }
703
-
704
- VALUE Backend_timeout_ensure_safe(VALUE arg) {
705
- return rb_rescue2(Backend_timeout_safe, Qnil, Backend_timeout_rescue, Qnil, rb_eException, (VALUE)0);
706
- }
707
-
708
696
  struct libev_timeout {
709
697
  struct ev_timer timer;
710
698
  VALUE fiber;
@@ -759,38 +747,29 @@ VALUE Backend_timeout(int argc,VALUE *argv, VALUE self) {
759
747
  return result;
760
748
  }
761
749
 
762
- struct libev_child {
763
- struct ev_child child;
764
- VALUE fiber;
765
- };
766
-
767
- void Backend_child_callback(EV_P_ ev_child *w, int revents)
768
- {
769
- struct libev_child *watcher = (struct libev_child *)w;
770
- int exit_status = WEXITSTATUS(w->rstatus);
771
- VALUE status;
772
-
773
- status = rb_ary_new_from_args(2, INT2NUM(w->rpid), INT2NUM(exit_status));
774
- Fiber_make_runnable(watcher->fiber, status);
775
- }
776
-
777
750
  VALUE Backend_waitpid(VALUE self, VALUE pid) {
778
- Backend_t *backend;
779
- struct libev_child watcher;
780
- VALUE switchpoint_result = Qnil;
781
- GetBackend(self, backend);
782
-
783
- watcher.fiber = rb_fiber_current();
784
- ev_child_init(&watcher.child, Backend_child_callback, NUM2INT(pid), 0);
785
- ev_child_start(backend->ev_loop, &watcher.child);
786
-
787
- switchpoint_result = backend_await(backend);
751
+ int pid_int = NUM2INT(pid);
752
+ int fd = pidfd_open(pid_int, 0);
753
+ if (fd >= 0) {
754
+ Backend_t *backend;
755
+ GetBackend(self, backend);
756
+
757
+ VALUE resume_value = libev_wait_fd(backend, fd, EV_READ, 0);
758
+ close(fd);
759
+ RAISE_IF_EXCEPTION(resume_value);
760
+ RB_GC_GUARD(resume_value);
761
+ }
788
762
 
789
- ev_child_stop(backend->ev_loop, &watcher.child);
790
- RAISE_IF_EXCEPTION(switchpoint_result);
791
- RB_GC_GUARD(watcher.fiber);
792
- RB_GC_GUARD(switchpoint_result);
793
- return switchpoint_result;
763
+ int status = 0;
764
+ pid_t ret = waitpid(pid_int, &status, WNOHANG);
765
+ if (ret < 0) {
766
+ int e = errno;
767
+ if (e == ECHILD)
768
+ ret = pid_int;
769
+ else
770
+ rb_syserr_fail(e, strerror(e));
771
+ }
772
+ return rb_ary_new_from_args(2, INT2NUM(ret), INT2NUM(WEXITSTATUS(status)));
794
773
  }
795
774
 
796
775
  void Backend_async_callback(EV_P_ ev_async *w, int revents) { }
@@ -101,7 +101,8 @@ VALUE Fiber_await(VALUE self) {
101
101
  }
102
102
  rb_hash_aset(waiting_fibers, fiber, Qtrue);
103
103
 
104
- result = Thread_switch_fiber(rb_thread_current());
104
+ VALUE backend = rb_ivar_get(rb_thread_current(), ID_ivar_backend);
105
+ result = Backend_wait_event(backend, Qnil);
105
106
 
106
107
  rb_hash_delete(waiting_fibers, fiber);
107
108
  RAISE_IF_EXCEPTION(result);
@@ -84,6 +84,7 @@ void Runqueue_push(VALUE self, VALUE fiber, VALUE value, int reschedule);
84
84
  void Runqueue_unshift(VALUE self, VALUE fiber, VALUE value, int reschedule);
85
85
  runqueue_entry Runqueue_shift(VALUE self);
86
86
  void Runqueue_delete(VALUE self, VALUE fiber);
87
+ int Runqueue_index_of(VALUE self, VALUE fiber);
87
88
  void Runqueue_clear(VALUE self);
88
89
  long Runqueue_len(VALUE self);
89
90
  int Runqueue_empty_p(VALUE self);
@@ -74,6 +74,12 @@ void Runqueue_delete(VALUE self, VALUE fiber) {
74
74
  runqueue_ring_buffer_delete(&runqueue->entries, fiber);
75
75
  }
76
76
 
77
+ int Runqueue_index_of(VALUE self, VALUE fiber) {
78
+ Runqueue_t *runqueue;
79
+ GetRunqueue(self, runqueue);
80
+ return runqueue_ring_buffer_index_of(&runqueue->entries, fiber);
81
+ }
82
+
77
83
  void Runqueue_clear(VALUE self) {
78
84
  Runqueue_t *runqueue;
79
85
  GetRunqueue(self, runqueue);
@@ -80,6 +80,15 @@ void runqueue_ring_buffer_delete(runqueue_ring_buffer *buffer, VALUE fiber) {
80
80
  }
81
81
  }
82
82
 
83
+ int runqueue_ring_buffer_index_of(runqueue_ring_buffer *buffer, VALUE fiber) {
84
+ for (unsigned int i = 0; i < buffer->count; i++) {
85
+ unsigned int idx = (buffer->head + i) % buffer->size;
86
+ if (buffer->entries[idx].fiber == fiber)
87
+ return i;
88
+ }
89
+ return -1;
90
+ }
91
+
83
92
  void runqueue_ring_buffer_clear(runqueue_ring_buffer *buffer) {
84
93
  buffer->count = buffer->head = buffer->tail = 0;
85
94
  }
@@ -27,5 +27,6 @@ void runqueue_ring_buffer_unshift(runqueue_ring_buffer *buffer, VALUE fiber, VAL
27
27
  void runqueue_ring_buffer_push(runqueue_ring_buffer *buffer, VALUE fiber, VALUE value);
28
28
 
29
29
  void runqueue_ring_buffer_delete(runqueue_ring_buffer *buffer, VALUE fiber);
30
+ int runqueue_ring_buffer_index_of(runqueue_ring_buffer *buffer, VALUE fiber);
30
31
 
31
32
  #endif /* RUNQUEUE_RING_BUFFER_H */
@@ -59,6 +59,18 @@ void schedule_fiber(VALUE self, VALUE fiber, VALUE value, int prioritize) {
59
59
  }
60
60
  }
61
61
 
62
+ VALUE Thread_fiber_scheduling_index(VALUE self, VALUE fiber) {
63
+ VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
64
+
65
+ return INT2NUM(Runqueue_index_of(runqueue, fiber));
66
+ }
67
+
68
+ VALUE Thread_fiber_unschedule(VALUE self, VALUE fiber) {
69
+ VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
70
+ Runqueue_delete(runqueue, fiber);
71
+ return self;
72
+ }
73
+
62
74
  VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
63
75
  schedule_fiber(self, fiber, value, 0);
64
76
  return self;
@@ -135,6 +147,8 @@ void Init_Thread() {
135
147
  rb_define_method(rb_cThread, "schedule_fiber_with_priority",
136
148
  Thread_schedule_fiber_with_priority, 2);
137
149
  rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
150
+ rb_define_method(rb_cThread, "fiber_scheduling_index", Thread_fiber_scheduling_index, 1);
151
+ rb_define_method(rb_cThread, "fiber_unschedule", Thread_fiber_unschedule, 1);
138
152
 
139
153
  rb_define_method(rb_cThread, "debug!", Thread_debug, 0);
140
154
 
@@ -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
 
@@ -43,7 +44,7 @@ module Polyphony
43
44
  rescue SystemExit
44
45
  # fall through to ensure
45
46
  rescue Exception => e
46
- warn e.full_message
47
+ STDERR << e.full_message
47
48
  exit!
48
49
  ensure
49
50
  exit_forked_process
@@ -24,6 +24,8 @@ module Polyphony
24
24
  def kill_and_await(sig, pid)
25
25
  ::Process.kill(sig, pid)
26
26
  Thread.current.backend.waitpid(pid)
27
+ rescue Errno::ERSCH
28
+ # ignore
27
29
  end
28
30
  end
29
31
  end
@@ -13,6 +13,7 @@ module Polyphony
13
13
  def initialize(value = nil)
14
14
  @caller_backtrace = caller
15
15
  @value = value
16
+ super
16
17
  end
17
18
  end
18
19
 
@@ -29,7 +29,7 @@ module Polyphony
29
29
  spin do
30
30
  sleep interval
31
31
  exception = cancel_exception(with_exception)
32
- exception.__raising_fiber__ = nil
32
+ exception.raising_fiber = nil
33
33
  fiber.schedule exception
34
34
  end
35
35
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyphony
4
+ # Implements a common timer for running multiple timeouts
5
+ class Timer
6
+ def initialize(resolution:)
7
+ @fiber = spin_loop(interval: resolution) { update }
8
+ @timeouts = {}
9
+ end
10
+
11
+ def stop
12
+ @fiber.stop
13
+ end
14
+
15
+ def sleep(duration)
16
+ fiber = Fiber.current
17
+ @timeouts[fiber] = {
18
+ interval: duration,
19
+ target_stamp: now + duration
20
+ }
21
+ Thread.current.backend.wait_event(true)
22
+ ensure
23
+ @timeouts.delete(fiber)
24
+ end
25
+
26
+ def after(interval, &block)
27
+ spin do
28
+ self.sleep interval
29
+ block.()
30
+ end
31
+ end
32
+
33
+ def every(interval)
34
+ fiber = Fiber.current
35
+ @timeouts[fiber] = {
36
+ interval: interval,
37
+ target_stamp: now + interval,
38
+ recurring: true
39
+ }
40
+ while true
41
+ Thread.current.backend.wait_event(true)
42
+ yield
43
+ end
44
+ ensure
45
+ @timeouts.delete(fiber)
46
+ end
47
+
48
+ def cancel_after(interval, with_exception: Polyphony::Cancel)
49
+ fiber = Fiber.current
50
+ @timeouts[fiber] = {
51
+ interval: interval,
52
+ target_stamp: now + interval,
53
+ exception: with_exception
54
+ }
55
+ yield
56
+ ensure
57
+ @timeouts.delete(fiber)
58
+ end
59
+
60
+ def move_on_after(interval, with_value: nil)
61
+ fiber = Fiber.current
62
+ @timeouts[fiber] = {
63
+ interval: interval,
64
+ target_stamp: now + interval,
65
+ exception: [Polyphony::MoveOn, with_value]
66
+ }
67
+ yield
68
+ rescue Polyphony::MoveOn => e
69
+ e.value
70
+ ensure
71
+ @timeouts.delete(fiber)
72
+ end
73
+
74
+ def reset
75
+ record = @timeouts[Fiber.current]
76
+ return unless record
77
+
78
+ record[:target_stamp] = now + record[:interval]
79
+ end
80
+
81
+ private
82
+
83
+ def now
84
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
85
+ end
86
+
87
+ def timeout_exception(record)
88
+ case (exception = record[:exception])
89
+ when Array
90
+ exception[0].new(exception[1])
91
+ when Class
92
+ exception.new
93
+ else
94
+ RuntimeError.new(exception)
95
+ end
96
+ end
97
+
98
+ def update
99
+ return if @timeouts.empty?
100
+
101
+ @timeouts.each do |fiber, record|
102
+ next if record[:target_stamp] > now
103
+
104
+ value = record[:exception] ? timeout_exception(record) : record[:value]
105
+ fiber.schedule value
106
+
107
+ next unless record[:recurring]
108
+
109
+ while record[:target_stamp] <= now
110
+ record[:target_stamp] += record[:interval]
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -12,11 +12,11 @@ class ::Exception
12
12
  attr_accessor :__disable_sanitized_backtrace__
13
13
  end
14
14
 
15
- attr_accessor :source_fiber, :__raising_fiber__
15
+ attr_accessor :source_fiber, :raising_fiber
16
16
 
17
17
  alias_method :orig_initialize, :initialize
18
18
  def initialize(*args)
19
- @__raising_fiber__ = Fiber.current
19
+ @raising_fiber = Fiber.current
20
20
  orig_initialize(*args)
21
21
  end
22
22
 
@@ -31,10 +31,10 @@ class ::Exception
31
31
  end
32
32
 
33
33
  def sanitized_backtrace
34
- return sanitize(orig_backtrace) unless @__raising_fiber__
34
+ return sanitize(orig_backtrace) unless @raising_fiber
35
35
 
36
36
  backtrace = orig_backtrace || []
37
- sanitize(backtrace + @__raising_fiber__.caller)
37
+ sanitize(backtrace + @raising_fiber.caller)
38
38
  end
39
39
 
40
40
  POLYPHONY_DIR = File.expand_path(File.join(__dir__, '..'))
@@ -245,8 +245,12 @@ module Polyphony
245
245
  end
246
246
 
247
247
  def shutdown_all_children(graceful = false)
248
- terminate_all_children(graceful)
249
- await_all_children
248
+ return unless @children
249
+
250
+ @children.keys.each do |c|
251
+ c.terminate(graceful)
252
+ c.await
253
+ end
250
254
  end
251
255
  end
252
256
 
@@ -312,6 +316,8 @@ module Polyphony
312
316
  @running = false
313
317
  inform_dependants(result, uncaught_exception)
314
318
  ensure
319
+ # Prevent fiber from being resumed after terminating
320
+ @thread.fiber_unschedule(self)
315
321
  Thread.current.switch_fiber
316
322
  end
317
323
 
@@ -319,13 +325,10 @@ module Polyphony
319
325
  # the children are shut down, it is returned along with the uncaught_exception
320
326
  # flag set. Otherwise, it returns the given arguments.
321
327
  def finalize_children(result, uncaught_exception)
322
- begin
323
- shutdown_all_children
324
- rescue Exception => e
325
- result = e
326
- uncaught_exception = true
327
- end
328
+ shutdown_all_children
328
329
  [result, uncaught_exception]
330
+ rescue Exception => e
331
+ [e, true]
329
332
  end
330
333
 
331
334
  def inform_dependants(result, uncaught_exception)
@@ -41,8 +41,7 @@ class ::Thread
41
41
 
42
42
  def finalize(result)
43
43
  unless Fiber.current.children.empty?
44
- Fiber.current.terminate_all_children
45
- Fiber.current.await_all_children
44
+ Fiber.current.shutdown_all_children
46
45
  end
47
46
  @finalization_mutex.synchronize do
48
47
  @terminated = true
@@ -8,10 +8,7 @@ module Polyphony
8
8
  module Net
9
9
  class << self
10
10
  def tcp_connect(host, port, opts = {})
11
- socket = ::Socket.new(:INET, :STREAM).tap do |s|
12
- addr = ::Socket.sockaddr_in(port, host)
13
- s.connect(addr)
14
- end
11
+ socket = TCPSocket.new(host, port)
15
12
  if opts[:secure_context] || opts[:secure]
16
13
  secure_socket(socket, opts[:secure_context], opts.merge(host: host))
17
14
  else
@@ -23,7 +20,7 @@ module Polyphony
23
20
  host ||= '0.0.0.0'
24
21
  raise 'Port number not specified' unless port
25
22
 
26
- socket = socket_from_options(host, port, opts)
23
+ socket = listening_socket_from_options(host, port, opts)
27
24
  if opts[:secure_context] || opts[:secure]
28
25
  secure_server(socket, opts[:secure_context], opts)
29
26
  else
@@ -31,7 +28,7 @@ module Polyphony
31
28
  end
32
29
  end
33
30
 
34
- def socket_from_options(host, port, opts)
31
+ def listening_socket_from_options(host, port, opts)
35
32
  ::Socket.new(:INET, :STREAM).tap do |s|
36
33
  s.reuse_addr if opts[:reuse_addr]
37
34
  s.dont_linger if opts[:dont_linger]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.48.0'
4
+ VERSION = '0.50.1'
5
5
  end
@@ -57,8 +57,7 @@ class MiniTest::Test
57
57
 
58
58
  def teardown
59
59
  # trace "* teardown #{self.name}"
60
- Fiber.current.terminate_all_children
61
- Fiber.current.await_all_children
60
+ Fiber.current.shutdown_all_children
62
61
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
63
62
  rescue => e
64
63
  puts e
@@ -100,6 +100,31 @@ class BackendTest < MiniTest::Test
100
100
  assert_equal [:ready, 'foo', 'bar', :done], buf
101
101
  end
102
102
 
103
+ def test_read_loop_terminate
104
+ i, o = IO.pipe
105
+
106
+ buf = []
107
+ parent = spin do
108
+ f = spin do
109
+ buf << :ready
110
+ @backend.read_loop(i) { |d| buf << d }
111
+ buf << :done
112
+ end
113
+ suspend
114
+ end
115
+
116
+ # writing always causes snoozing
117
+ o << 'foo'
118
+ sleep 0.01
119
+ o << 'bar'
120
+ sleep 0.01
121
+
122
+ parent.stop
123
+
124
+ parent.await
125
+ assert_equal [:ready, 'foo', 'bar'], buf
126
+ end
127
+
103
128
  def test_accept_loop
104
129
  server = TCPServer.new('127.0.0.1', 1234)
105
130
 
@@ -349,6 +349,22 @@ class FiberTest < MiniTest::Test
349
349
  assert_equal [:foo, :terminate], buffer
350
350
  end
351
351
 
352
+ CMD_TERMINATE_MAIN_FIBER = <<~BASH
353
+ ruby -rbundler/setup -rpolyphony -e"spin { sleep 0.1; Thread.current.main_fiber.terminate }; begin; sleep; rescue Polyphony::Terminate; STDOUT << 'terminated'; end" 2>&1
354
+ BASH
355
+
356
+ CMD_TERMINATE_CHILD_FIBER = <<~BASH
357
+ ruby -rbundler/setup -rpolyphony -e"f = spin { sleep }; spin { sleep 0.1; f.terminate }; f.await" 2>&1
358
+ BASH
359
+
360
+ def test_terminate_main_fiber
361
+ output = `#{CMD_TERMINATE_CHILD_FIBER}`
362
+ assert_equal '', output
363
+
364
+ output = `#{CMD_TERMINATE_MAIN_FIBER}`
365
+ assert_equal 'terminated', output
366
+ end
367
+
352
368
  def test_interrupt_timer
353
369
  result = []
354
370
  f = Fiber.current.spin do
@@ -639,7 +655,6 @@ class FiberTest < MiniTest::Test
639
655
  i.close
640
656
  f.await
641
657
  rescue Exception => e
642
- trace e
643
658
  o << e.class.name
644
659
  o.close
645
660
  end
@@ -1039,6 +1054,21 @@ class RestartTest < MiniTest::Test
1039
1054
  end
1040
1055
  end
1041
1056
 
1057
+ class ChildrenTerminationTest < MiniTest::Test
1058
+ def test_shutdown_all_children
1059
+ f = spin do
1060
+ 1000.times { spin { suspend } }
1061
+ suspend
1062
+ end
1063
+
1064
+ snooze
1065
+ assert_equal 1000, f.children.size
1066
+
1067
+ f.shutdown_all_children
1068
+ assert_equal 0, f.children.size
1069
+ end
1070
+ end
1071
+
1042
1072
  class GracefulTerminationTest < MiniTest::Test
1043
1073
  def test_graceful_termination
1044
1074
  buffer = []
@@ -110,7 +110,7 @@ class IOTest < MiniTest::Test
110
110
  assert_equal [], buf
111
111
 
112
112
  o << "ulous\n"
113
- 10.times { snooze }
113
+ 15.times { snooze }
114
114
  assert_equal ["fabulous\n"], buf
115
115
 
116
116
  o.close
@@ -46,8 +46,7 @@ class SignalTrapTest < Minitest::Test
46
46
  end.await
47
47
  rescue Interrupt
48
48
  o.puts "3 - interrupted"
49
- Fiber.current.terminate_all_children
50
- Fiber.current.await_all_children
49
+ Fiber.current.shutdown_all_children
51
50
  ensure
52
51
  o.close
53
52
  end
@@ -63,14 +63,15 @@ class HTTPClientTest < MiniTest::Test
63
63
  require 'json'
64
64
 
65
65
  def test_http
66
- res = HTTParty.get('http://worldtimeapi.org/api/timezone/Europe/Paris')
66
+ res = HTTParty.get('http://ipinfo.io/')
67
+
67
68
  response = JSON.load(res.body)
68
- assert_equal "CET", response['abbreviation']
69
+ assert_equal 'https://ipinfo.io/missingauth', response['readme']
69
70
  end
70
71
 
71
72
  def test_https
72
- res = HTTParty.get('https://worldtimeapi.org/api/timezone/Europe/Paris')
73
+ res = HTTParty.get('https://ipinfo.io/')
73
74
  response = JSON.load(res.body)
74
- assert_equal "CET", response['abbreviation']
75
+ assert_equal 'https://ipinfo.io/missingauth', response['readme']
75
76
  end
76
77
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class TimerMoveOnAfterTest < MiniTest::Test
6
+ def setup
7
+ @timer = Polyphony::Timer.new(resolution: 0.01)
8
+ end
9
+
10
+ def teardown
11
+ @timer.stop
12
+ end
13
+
14
+ def test_timer_move_on_after
15
+ t0 = Time.now
16
+ v = @timer.move_on_after(0.1) do
17
+ sleep 1
18
+ :foo
19
+ end
20
+ t1 = Time.now
21
+
22
+ assert_in_range 0.1..0.15, t1 - t0
23
+ assert_nil v
24
+ end
25
+
26
+ def test_timer_move_on_after_with_value
27
+ t0 = Time.now
28
+ v = @timer.move_on_after(0.01, with_value: :bar) do
29
+ sleep 1
30
+ :foo
31
+ end
32
+ t1 = Time.now
33
+
34
+ assert_in_range 0.01..0.025, t1 - t0
35
+ assert_equal :bar, v
36
+ end
37
+
38
+ def test_timer_move_on_after_with_reset
39
+ t0 = Time.now
40
+ v = @timer.move_on_after(0.01, with_value: :moved_on) do
41
+ sleep 0.007
42
+ @timer.reset
43
+ sleep 0.007
44
+ @timer.reset
45
+ sleep 0.007
46
+ nil
47
+ end
48
+ t1 = Time.now
49
+
50
+ assert_nil v
51
+ assert_in_range 0.015..0.03, t1 - t0
52
+ end
53
+ end
54
+
55
+ class TimerCancelAfterTest < MiniTest::Test
56
+ def setup
57
+ @timer = Polyphony::Timer.new(resolution: 0.01)
58
+ end
59
+
60
+ def teardown
61
+ @timer.stop
62
+ end
63
+
64
+ def test_timer_cancel_after
65
+ t0 = Time.now
66
+
67
+ assert_raises Polyphony::Cancel do
68
+ @timer.cancel_after(0.01) do
69
+ sleep 1
70
+ :foo
71
+ end
72
+ end
73
+ t1 = Time.now
74
+ assert_in_range 0.01..0.03, t1 - t0
75
+ end
76
+
77
+ def test_timer_cancel_after_with_reset
78
+ t0 = Time.now
79
+ @timer.cancel_after(0.01) do
80
+ sleep 0.007
81
+ @timer.reset
82
+ sleep 0.007
83
+ end
84
+ t1 = Time.now
85
+ assert_in_range 0.013..0.024, t1 - t0
86
+ end
87
+
88
+ class CustomException < Exception
89
+ end
90
+
91
+ def test_timer_cancel_after_with_custom_exception
92
+ assert_raises CustomException do
93
+ @timer.cancel_after(0.01, with_exception: CustomException) do
94
+ sleep 1
95
+ :foo
96
+ end
97
+ end
98
+
99
+ begin
100
+ err = nil
101
+ @timer.cancel_after(0.01, with_exception: [CustomException, 'custom message']) do
102
+ sleep 1
103
+ :foo
104
+ end
105
+ rescue Exception => err
106
+ ensure
107
+ assert_kind_of CustomException, err
108
+ assert_equal 'custom message', err.message
109
+ end
110
+
111
+
112
+ begin
113
+ e = nil
114
+ @timer.cancel_after(0.01, with_exception: 'foo') do
115
+ sleep 1
116
+ :foo
117
+ end
118
+ rescue => e
119
+ ensure
120
+ assert_kind_of RuntimeError, e
121
+ assert_equal 'foo', e.message
122
+ end
123
+ end
124
+ end
125
+
126
+ class TimerMiscTest < MiniTest::Test
127
+ def setup
128
+ @timer = Polyphony::Timer.new(resolution: 0.001)
129
+ sleep 0
130
+ end
131
+
132
+ def teardown
133
+ @timer.stop
134
+ end
135
+
136
+ def test_timer_after
137
+ buffer = []
138
+ f = @timer.after(0.01) { buffer << 2 }
139
+ assert_kind_of Fiber, f
140
+ snooze
141
+ assert_equal [], buffer
142
+ sleep 0.1
143
+ p :post_sleep
144
+ assert_equal [2], buffer
145
+ end
146
+
147
+ def test_timer_every
148
+ buffer = []
149
+ t0 = Time.now
150
+ f = spin do
151
+ @timer.every(0.01) { buffer << 1 }
152
+ end
153
+ sleep 0.05
154
+ f.stop
155
+ assert_in_range 4..6, buffer.size
156
+ end
157
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyphony
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.48.0
4
+ version: 0.50.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-05 00:00:00.000000000 Z
11
+ date: 2021-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -353,6 +353,7 @@ files:
353
353
  - examples/core/forking.rb
354
354
  - examples/core/handling-signals.rb
355
355
  - examples/core/interrupt.rb
356
+ - examples/core/nested.rb
356
357
  - examples/core/pingpong.rb
357
358
  - examples/core/recurrent-timer.rb
358
359
  - examples/core/resource_delegate.rb
@@ -360,6 +361,8 @@ files:
360
361
  - examples/core/spin_error_backtrace.rb
361
362
  - examples/core/supervise-process.rb
362
363
  - examples/core/supervisor.rb
364
+ - examples/core/suspend.rb
365
+ - examples/core/terminate_main_fiber.rb
363
366
  - examples/core/thread-sleep.rb
364
367
  - examples/core/thread_pool.rb
365
368
  - examples/core/throttling.rb
@@ -476,6 +479,7 @@ files:
476
479
  - lib/polyphony/core/sync.rb
477
480
  - lib/polyphony/core/thread_pool.rb
478
481
  - lib/polyphony/core/throttler.rb
482
+ - lib/polyphony/core/timer.rb
479
483
  - lib/polyphony/extensions/core.rb
480
484
  - lib/polyphony/extensions/debug.rb
481
485
  - lib/polyphony/extensions/fiber.rb
@@ -510,6 +514,7 @@ files:
510
514
  - test/test_thread.rb
511
515
  - test/test_thread_pool.rb
512
516
  - test/test_throttler.rb
517
+ - test/test_timer.rb
513
518
  - test/test_trace.rb
514
519
  homepage: https://digital-fabric.github.io/polyphony
515
520
  licenses: