polyphony 0.43.11 → 0.44.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +5 -1
- data/TODO.md +11 -8
- data/examples/adapters/sequel_mysql.rb +23 -0
- data/examples/adapters/sequel_mysql_pool.rb +33 -0
- data/ext/polyphony/libev_agent.c +16 -18
- data/ext/polyphony/polyphony.c +2 -0
- data/ext/polyphony/polyphony.h +11 -4
- data/ext/polyphony/thread.c +13 -9
- data/lib/polyphony.rb +11 -25
- data/lib/polyphony/adapters/mysql2.rb +19 -0
- data/lib/polyphony/adapters/sequel.rb +45 -0
- data/lib/polyphony/core/exceptions.rb +11 -0
- data/lib/polyphony/core/resource_pool.rb +18 -5
- data/lib/polyphony/extensions/core.rb +15 -8
- data/lib/polyphony/extensions/fiber.rb +4 -0
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +2 -0
- data/test/test_fiber.rb +16 -0
- data/test/test_resource_pool.rb +29 -4
- data/test/test_signal.rb +2 -26
- metadata +34 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04c1c1221d46b1b9169aa04c5896c8d68c20fa11d64554e76b7452710423f142
|
4
|
+
data.tar.gz: 519528e0a3dfd323620869d4a7e43e4769cf3c1b5f0ea405cf341efc6fd534f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de8ac460dd05b051057ffab1c155c6b2d675cc623b98549333ba1defbc99f4efb738ff4a34a9f8c14bd3cb9ab0cab5356aa34a2308daa73273a6ef8719565bc4
|
7
|
+
data.tar.gz: c26b254559e484774574555dc895cf072bda8061b92fed26d8e61e7172523fe0faf2750cd2f924db3a25b59583b673ebe5f14749fd6f1279ca2e7e7b7745490b
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## 0.44.0 2020-07-25
|
2
|
+
|
3
|
+
* Fix reentrant `ResourcePool` (#38)
|
4
|
+
* Add `ResourcePool#discard!` (#35)
|
5
|
+
* Add `Mysql2::Client` and `Sequel::ConnectionPool` adapters (#35)
|
6
|
+
* Reimplement `Kernel.trap` using `Fiber#interject`
|
7
|
+
* Add `Fiber#interject` for running arbitrary code on arbitrary fibers (#39)
|
8
|
+
|
1
9
|
## 0.43.11 2020-07-24
|
2
10
|
|
3
11
|
* Dump uncaught exception info for forked process (#36)
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
polyphony (0.
|
4
|
+
polyphony (0.44.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -72,6 +72,7 @@ GEM
|
|
72
72
|
minitest (>= 5.0)
|
73
73
|
ruby-progressbar
|
74
74
|
multi_xml (0.6.0)
|
75
|
+
mysql2 (0.5.3)
|
75
76
|
parallel (1.19.1)
|
76
77
|
parser (2.7.0.2)
|
77
78
|
ast (~> 2.4.0)
|
@@ -109,6 +110,7 @@ GEM
|
|
109
110
|
sass-listen (4.0.0)
|
110
111
|
rb-fsevent (~> 0.9, >= 0.9.4)
|
111
112
|
rb-inotify (~> 0.9, >= 0.9.7)
|
113
|
+
sequel (5.34.0)
|
112
114
|
simplecov (0.17.1)
|
113
115
|
docile (~> 1.1)
|
114
116
|
json (>= 1.8, < 3)
|
@@ -130,11 +132,13 @@ DEPENDENCIES
|
|
130
132
|
localhost (= 1.1.4)
|
131
133
|
minitest (= 5.13.0)
|
132
134
|
minitest-reporters (= 1.4.2)
|
135
|
+
mysql2 (= 0.5.3)
|
133
136
|
pg (= 1.1.4)
|
134
137
|
polyphony!
|
135
138
|
rake-compiler (= 1.0.5)
|
136
139
|
redis (= 4.1.0)
|
137
140
|
rubocop (= 0.85.1)
|
141
|
+
sequel (= 5.34.0)
|
138
142
|
simplecov (= 0.17.1)
|
139
143
|
|
140
144
|
BUNDLED WITH
|
data/TODO.md
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
+
- Adapter for Pry, or maybe just an adapter for readline
|
2
|
+
- Rename Agent to Backend
|
3
|
+
- Review all code
|
4
|
+
- Cleanup C code
|
5
|
+
- Cleanup Ruby code (use Rubocop)
|
6
|
+
- Cleanup and annotate examples (and remove all the examples used for
|
7
|
+
debugging). Focus on examples that serve as "how-to".
|
8
|
+
|
1
9
|
- Debugging
|
2
10
|
- Eat your own dogfood: need a good tool to check what's going on when some
|
3
11
|
test fails
|
4
|
-
- Needs to work with Pry (can write perhaps an extension for pry)
|
5
12
|
- First impl in Ruby using `TracePoint` API
|
6
13
|
- Mode of operation:
|
7
14
|
- Two parts: tracer and controller
|
@@ -111,7 +118,7 @@
|
|
111
118
|
- discuss using `snooze` for ensuring responsiveness when executing CPU-bound work
|
112
119
|
|
113
120
|
|
114
|
-
## 0.
|
121
|
+
## 0.45
|
115
122
|
|
116
123
|
### Some more API work, more docs
|
117
124
|
|
@@ -124,13 +131,13 @@
|
|
124
131
|
- proceed from there
|
125
132
|
|
126
133
|
|
127
|
-
## 0.
|
134
|
+
## 0.46
|
128
135
|
|
129
136
|
### Sinatra / Sidekiq
|
130
137
|
|
131
138
|
- Pull out redis/postgres code, put into new `polyphony-xxx` gems
|
132
139
|
|
133
|
-
## 0.
|
140
|
+
## 0.47
|
134
141
|
|
135
142
|
### Testing && Docs
|
136
143
|
|
@@ -142,10 +149,6 @@
|
|
142
149
|
- `IO.foreach`
|
143
150
|
- `Process.waitpid`
|
144
151
|
|
145
|
-
## 0.47
|
146
|
-
|
147
|
-
### Real IO#gets and IO#read
|
148
|
-
|
149
152
|
## 0.48 DNS
|
150
153
|
|
151
154
|
### DNS client
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/adapters/sequel'
|
5
|
+
require 'polyphony/adapters/mysql2'
|
6
|
+
|
7
|
+
time_printer = spin do
|
8
|
+
last = Time.now
|
9
|
+
throttled_loop(10) do
|
10
|
+
now = Time.now
|
11
|
+
puts now - last
|
12
|
+
last = now
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
db = Sequel.connect('mysql2://localhost/test')
|
17
|
+
|
18
|
+
x = 10_000
|
19
|
+
t0 = Time.now
|
20
|
+
x.times { db.execute('select 1 as test') }
|
21
|
+
puts "query rate: #{x / (Time.now - t0)} reqs/s"
|
22
|
+
|
23
|
+
time_printer.stop
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/adapters/sequel'
|
5
|
+
require 'polyphony/adapters/mysql2'
|
6
|
+
|
7
|
+
CONCURRENCY = ARGV.first ? ARGV.first.to_i : 1000
|
8
|
+
puts "concurrency: #{CONCURRENCY}"
|
9
|
+
|
10
|
+
db = Sequel.connect(
|
11
|
+
'mysql2://localhost/test',
|
12
|
+
max_connections: 100,
|
13
|
+
preconnect: true
|
14
|
+
)
|
15
|
+
|
16
|
+
t0 = Time.now
|
17
|
+
count = 0
|
18
|
+
|
19
|
+
fibers = Array.new(CONCURRENCY) do
|
20
|
+
spin do
|
21
|
+
loop do
|
22
|
+
db.execute('select sleep(0.001) as test')
|
23
|
+
count += 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
sleep 0.1
|
29
|
+
fibers.first.terminate # Interrupt mid-query
|
30
|
+
|
31
|
+
sleep 2
|
32
|
+
puts "query rate: #{count / (Time.now - t0)} reqs/s"
|
33
|
+
fibers.each(&:interrupt)
|
data/ext/polyphony/libev_agent.c
CHANGED
@@ -77,15 +77,13 @@ VALUE LibevAgent_post_fork(VALUE self) {
|
|
77
77
|
LibevAgent_t *agent;
|
78
78
|
GetLibevAgent(self, agent);
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
ev_loop_fork(agent->ev_loop);
|
80
|
+
// After fork there may be some watchers still active left over from the
|
81
|
+
// parent, so we destroy the loop, even if it's the default one, then use the
|
82
|
+
// default one, as post_fork is called only from the main thread of the forked
|
83
|
+
// process. That way we don't need to call ev_loop_fork, since the loop is
|
84
|
+
// always a fresh one.
|
85
|
+
ev_loop_destroy(agent->ev_loop);
|
86
|
+
agent->ev_loop = EV_DEFAULT;
|
89
87
|
|
90
88
|
return self;
|
91
89
|
}
|
@@ -142,11 +140,11 @@ VALUE LibevAgent_poll(VALUE self, VALUE nowait, VALUE current_fiber, VALUE queue
|
|
142
140
|
|
143
141
|
agent->run_no_wait_count = 0;
|
144
142
|
|
145
|
-
|
143
|
+
COND_TRACE(2, SYM_fiber_ev_loop_enter, current_fiber);
|
146
144
|
agent->running = 1;
|
147
145
|
ev_run(agent->ev_loop, is_nowait ? EVRUN_NOWAIT : EVRUN_ONCE);
|
148
146
|
agent->running = 0;
|
149
|
-
|
147
|
+
COND_TRACE(2, SYM_fiber_ev_loop_leave, current_fiber);
|
150
148
|
|
151
149
|
return self;
|
152
150
|
}
|
@@ -376,7 +374,7 @@ VALUE LibevAgent_read(VALUE self, VALUE io, VALUE str, VALUE length, VALUE to_eo
|
|
376
374
|
|
377
375
|
return str;
|
378
376
|
error:
|
379
|
-
return
|
377
|
+
return RAISE_EXCEPTION(switchpoint_result);
|
380
378
|
}
|
381
379
|
|
382
380
|
VALUE LibevAgent_read_loop(VALUE self, VALUE io) {
|
@@ -456,7 +454,7 @@ VALUE LibevAgent_read_loop(VALUE self, VALUE io) {
|
|
456
454
|
|
457
455
|
return io;
|
458
456
|
error:
|
459
|
-
return
|
457
|
+
return RAISE_EXCEPTION(switchpoint_result);
|
460
458
|
}
|
461
459
|
|
462
460
|
VALUE LibevAgent_write(VALUE self, VALUE io, VALUE str) {
|
@@ -500,7 +498,7 @@ VALUE LibevAgent_write(VALUE self, VALUE io, VALUE str) {
|
|
500
498
|
|
501
499
|
return INT2NUM(len);
|
502
500
|
error:
|
503
|
-
return
|
501
|
+
return RAISE_EXCEPTION(switchpoint_result);
|
504
502
|
}
|
505
503
|
|
506
504
|
VALUE LibevAgent_writev(VALUE self, VALUE io, int argc, VALUE *argv) {
|
@@ -570,7 +568,7 @@ VALUE LibevAgent_writev(VALUE self, VALUE io, int argc, VALUE *argv) {
|
|
570
568
|
return INT2NUM(total_written);
|
571
569
|
error:
|
572
570
|
free(iov);
|
573
|
-
return
|
571
|
+
return RAISE_EXCEPTION(switchpoint_result);
|
574
572
|
}
|
575
573
|
|
576
574
|
VALUE LibevAgent_write_m(int argc, VALUE *argv, VALUE self) {
|
@@ -636,7 +634,7 @@ VALUE LibevAgent_accept(VALUE self, VALUE sock) {
|
|
636
634
|
RB_GC_GUARD(switchpoint_result);
|
637
635
|
return Qnil;
|
638
636
|
error:
|
639
|
-
return
|
637
|
+
return RAISE_EXCEPTION(switchpoint_result);
|
640
638
|
}
|
641
639
|
|
642
640
|
VALUE LibevAgent_accept_loop(VALUE self, VALUE sock) {
|
@@ -692,7 +690,7 @@ VALUE LibevAgent_accept_loop(VALUE self, VALUE sock) {
|
|
692
690
|
RB_GC_GUARD(switchpoint_result);
|
693
691
|
return Qnil;
|
694
692
|
error:
|
695
|
-
return
|
693
|
+
return RAISE_EXCEPTION(switchpoint_result);
|
696
694
|
}
|
697
695
|
|
698
696
|
VALUE LibevAgent_connect(VALUE self, VALUE sock, VALUE host, VALUE port) {
|
@@ -728,7 +726,7 @@ VALUE LibevAgent_connect(VALUE self, VALUE sock, VALUE host, VALUE port) {
|
|
728
726
|
RB_GC_GUARD(switchpoint_result);
|
729
727
|
return sock;
|
730
728
|
error:
|
731
|
-
return
|
729
|
+
return RAISE_EXCEPTION(switchpoint_result);
|
732
730
|
}
|
733
731
|
|
734
732
|
VALUE libev_wait_fd(LibevAgent_t *agent, int fd, int events, int raise_exception) {
|
data/ext/polyphony/polyphony.c
CHANGED
@@ -7,6 +7,7 @@ ID ID_caller;
|
|
7
7
|
ID ID_clear;
|
8
8
|
ID ID_each;
|
9
9
|
ID ID_inspect;
|
10
|
+
ID ID_invoke;
|
10
11
|
ID ID_new;
|
11
12
|
ID ID_raise;
|
12
13
|
ID ID_ivar_running;
|
@@ -60,6 +61,7 @@ void Init_Polyphony() {
|
|
60
61
|
ID_clear = rb_intern("clear");
|
61
62
|
ID_each = rb_intern("each");
|
62
63
|
ID_inspect = rb_intern("inspect");
|
64
|
+
ID_invoke = rb_intern("invoke");
|
63
65
|
ID_ivar_running = rb_intern("@running");
|
64
66
|
ID_ivar_thread = rb_intern("@thread");
|
65
67
|
ID_new = rb_intern("new");
|
data/ext/polyphony/polyphony.h
CHANGED
@@ -8,17 +8,23 @@
|
|
8
8
|
|
9
9
|
// debugging
|
10
10
|
#define OBJ_ID(obj) (NUM2LONG(rb_funcall(obj, rb_intern("object_id"), 0)))
|
11
|
-
#define INSPECT(str, obj) { printf(str); VALUE s = rb_funcall(obj, rb_intern("inspect"), 0); printf("%s\n", StringValueCStr(s));}
|
12
|
-
#define
|
13
|
-
|
11
|
+
#define INSPECT(str, obj) { printf(str); VALUE s = rb_funcall(obj, rb_intern("inspect"), 0); printf("%s\n", StringValueCStr(s)); }
|
12
|
+
#define TRACE_CALLER() { VALUE c = rb_funcall(rb_mKernel, rb_intern("caller"), 0); INSPECT("caller: ", c); }
|
13
|
+
|
14
|
+
// tracing
|
15
|
+
#define TRACE(...) rb_funcall(rb_cObject, ID_fiber_trace, __VA_ARGS__)
|
16
|
+
#define COND_TRACE(...) if (__tracing_enabled__) { \
|
17
|
+
TRACE(__VA_ARGS__); \
|
14
18
|
}
|
15
19
|
|
16
20
|
#define TEST_EXCEPTION(ret) (RTEST(rb_obj_is_kind_of(ret, rb_eException)))
|
17
21
|
|
22
|
+
#define RAISE_EXCEPTION(e) rb_funcall(e, ID_invoke, 0);
|
18
23
|
#define TEST_RESUME_EXCEPTION(ret) if (RTEST(rb_obj_is_kind_of(ret, rb_eException))) { \
|
19
|
-
return
|
24
|
+
return RAISE_EXCEPTION(ret); \
|
20
25
|
}
|
21
26
|
|
27
|
+
|
22
28
|
extern agent_interface_t agent_interface;
|
23
29
|
#define __AGENT__ (agent_interface)
|
24
30
|
|
@@ -32,6 +38,7 @@ extern ID ID_clear;
|
|
32
38
|
extern ID ID_each;
|
33
39
|
extern ID ID_fiber_trace;
|
34
40
|
extern ID ID_inspect;
|
41
|
+
extern ID ID_invoke;
|
35
42
|
extern ID ID_ivar_agent;
|
36
43
|
extern ID ID_ivar_running;
|
37
44
|
extern ID ID_ivar_thread;
|
data/ext/polyphony/thread.c
CHANGED
@@ -64,7 +64,7 @@ VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
|
|
64
64
|
}
|
65
65
|
|
66
66
|
rb_ivar_set(fiber, ID_runnable_value, value);
|
67
|
-
|
67
|
+
COND_TRACE(3, SYM_fiber_schedule, fiber, value);
|
68
68
|
|
69
69
|
if (!already_runnable) {
|
70
70
|
queue = rb_ivar_get(self, ID_run_queue);
|
@@ -89,7 +89,7 @@ VALUE Thread_schedule_fiber_with_priority(VALUE self, VALUE fiber, VALUE value)
|
|
89
89
|
|
90
90
|
if (rb_fiber_alive_p(fiber) != Qtrue) return self;
|
91
91
|
|
92
|
-
|
92
|
+
COND_TRACE(3, SYM_fiber_schedule, fiber, value);
|
93
93
|
rb_ivar_set(fiber, ID_runnable_value, value);
|
94
94
|
|
95
95
|
queue = rb_ivar_get(self, ID_run_queue);
|
@@ -123,13 +123,10 @@ VALUE Thread_switch_fiber(VALUE self) {
|
|
123
123
|
VALUE value;
|
124
124
|
VALUE agent = rb_ivar_get(self, ID_ivar_agent);
|
125
125
|
int ref_count;
|
126
|
-
int agent_was_polled = 0;
|
126
|
+
int agent_was_polled = 0;
|
127
127
|
|
128
|
-
if (__tracing_enabled__)
|
129
|
-
|
130
|
-
rb_funcall(rb_cObject, ID_fiber_trace, 2, SYM_fiber_switchpoint, current_fiber);
|
131
|
-
}
|
132
|
-
}
|
128
|
+
if (__tracing_enabled__ && (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse))
|
129
|
+
TRACE(2, SYM_fiber_switchpoint, current_fiber);
|
133
130
|
|
134
131
|
ref_count = __AGENT__.ref_count(agent);
|
135
132
|
while (1) {
|
@@ -152,7 +149,7 @@ VALUE Thread_switch_fiber(VALUE self) {
|
|
152
149
|
|
153
150
|
// run next fiber
|
154
151
|
value = rb_ivar_get(next_fiber, ID_runnable_value);
|
155
|
-
|
152
|
+
COND_TRACE(3, SYM_fiber_run, next_fiber, value);
|
156
153
|
|
157
154
|
rb_ivar_set(next_fiber, ID_runnable, Qnil);
|
158
155
|
RB_GC_GUARD(next_fiber);
|
@@ -188,6 +185,11 @@ VALUE Thread_fiber_break_out_of_ev_loop(VALUE self, VALUE fiber, VALUE resume_ob
|
|
188
185
|
return self;
|
189
186
|
}
|
190
187
|
|
188
|
+
VALUE Thread_debug(VALUE self) {
|
189
|
+
rb_ivar_set(self, rb_intern("@__debug__"), Qtrue);
|
190
|
+
return self;
|
191
|
+
}
|
192
|
+
|
191
193
|
void Init_Thread() {
|
192
194
|
rb_define_method(rb_cThread, "setup_fiber_scheduling", Thread_setup_fiber_scheduling, 0);
|
193
195
|
rb_define_method(rb_cThread, "reset_fiber_scheduling", Thread_reset_fiber_scheduling, 0);
|
@@ -200,6 +202,8 @@ void Init_Thread() {
|
|
200
202
|
rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
|
201
203
|
rb_define_method(rb_cThread, "run_queue_trace", Thread_run_queue_trace, 0);
|
202
204
|
|
205
|
+
rb_define_method(rb_cThread, "debug!", Thread_debug, 0);
|
206
|
+
|
203
207
|
ID_deactivate_all_watchers_post_fork = rb_intern("deactivate_all_watchers_post_fork");
|
204
208
|
ID_ivar_agent = rb_intern("@agent");
|
205
209
|
ID_ivar_join_wait_queue = rb_intern("@join_wait_queue");
|
data/lib/polyphony.rb
CHANGED
@@ -27,26 +27,19 @@ require_relative './polyphony/adapters/process'
|
|
27
27
|
# Main Polyphony API
|
28
28
|
module Polyphony
|
29
29
|
class << self
|
30
|
-
def wait_for_signal(sig)
|
31
|
-
raise "should be reimplemented"
|
32
|
-
|
33
|
-
fiber = Fiber.current
|
34
|
-
# Polyphony.ref
|
35
|
-
old_trap = trap(sig) do
|
36
|
-
# Polyphony.unref
|
37
|
-
fiber.schedule(sig)
|
38
|
-
trap(sig, old_trap)
|
39
|
-
end
|
40
|
-
suspend
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
30
|
def fork(&block)
|
45
31
|
Kernel.fork do
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
32
|
+
# A race condition can arise if a TERM or INT signal is received before
|
33
|
+
# the forked process has finished initializing. To prevent this we restore
|
34
|
+
# the default signal handlers, and then reinstall the custom Polyphony
|
35
|
+
# handlers just before running the given block.
|
36
|
+
trap('SIGTERM', 'DEFAULT')
|
37
|
+
trap('SIGINT', 'DEFAULT')
|
38
|
+
|
39
|
+
# Since the fiber doing the fork will become the main fiber of the
|
40
|
+
# forked process, we leave it behind by transferring to a new fiber
|
41
|
+
# created in the context of the forked process, which rescues *all*
|
42
|
+
# exceptions, including Interrupt and SystemExit.
|
50
43
|
spin_forked_block(&block).transfer
|
51
44
|
end
|
52
45
|
end
|
@@ -65,13 +58,6 @@ module Polyphony
|
|
65
58
|
end
|
66
59
|
|
67
60
|
def run_forked_block(&block)
|
68
|
-
# A race condition can arise if a TERM or INT signal is received before
|
69
|
-
# the forked process has finished initializing. To prevent this we restore
|
70
|
-
# the default signal handlers, and then reinstall the custom Polyphony
|
71
|
-
# handlers just before running the given block.
|
72
|
-
trap('SIGTERM', 'DEFAULT')
|
73
|
-
trap('SIGINT', 'DEFAULT')
|
74
|
-
|
75
61
|
Thread.current.setup
|
76
62
|
Fiber.current.setup_main_fiber
|
77
63
|
Thread.current.agent.post_fork
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../polyphony'
|
4
|
+
require 'mysql2/client'
|
5
|
+
|
6
|
+
# Mysql2::Client overrides
|
7
|
+
Mysql2::Client.prepend(Module.new do
|
8
|
+
def initialize(config)
|
9
|
+
config[:async] = true
|
10
|
+
super
|
11
|
+
@io = ::IO.for_fd(socket)
|
12
|
+
end
|
13
|
+
|
14
|
+
def query(sql, **options)
|
15
|
+
super
|
16
|
+
Thread.current.agent.wait_io(@io, false)
|
17
|
+
async_result
|
18
|
+
end
|
19
|
+
end)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../polyphony'
|
4
|
+
require 'sequel'
|
5
|
+
|
6
|
+
module Polyphony
|
7
|
+
# Sequel ConnectionPool that delegates to Polyphony::ResourcePool.
|
8
|
+
class FiberConnectionPool < Sequel::ConnectionPool
|
9
|
+
def initialize(db, opts = OPTS)
|
10
|
+
super
|
11
|
+
max_size = Integer(opts[:max_connections] || 4)
|
12
|
+
@pool = Polyphony::ResourcePool.new(limit: max_size) { make_new(:default) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def hold(_server = nil)
|
16
|
+
@pool.acquire do |conn|
|
17
|
+
yield conn
|
18
|
+
rescue Polyphony::BaseException
|
19
|
+
# The connection may be in an unrecoverable state if interrupted,
|
20
|
+
# discard the connection from the pool so it isn't reused.
|
21
|
+
@pool.discard!
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def size
|
27
|
+
@pool.size
|
28
|
+
end
|
29
|
+
|
30
|
+
def max_size
|
31
|
+
@pool.limit
|
32
|
+
end
|
33
|
+
|
34
|
+
def preconnect(_concurrent = false)
|
35
|
+
@pool.preheat!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Override Sequel::Database to use FiberConnectionPool by default.
|
40
|
+
Sequel::Database.prepend(Module.new do
|
41
|
+
def connection_pool_default_options
|
42
|
+
{pool_class: FiberConnectionPool}
|
43
|
+
end
|
44
|
+
end)
|
45
|
+
end
|
@@ -33,4 +33,15 @@ module Polyphony
|
|
33
33
|
|
34
34
|
# Restart is used to restart a fiber
|
35
35
|
class Restart < BaseException; end
|
36
|
+
|
37
|
+
# Interjection is used to run arbitrary code on arbitrary fibers at any point
|
38
|
+
class Interjection < BaseException
|
39
|
+
def initialize(proc)
|
40
|
+
@proc = proc
|
41
|
+
end
|
42
|
+
|
43
|
+
def invoke
|
44
|
+
@proc.call
|
45
|
+
end
|
46
|
+
end
|
36
47
|
end
|
@@ -20,17 +20,23 @@ module Polyphony
|
|
20
20
|
@stock.size
|
21
21
|
end
|
22
22
|
|
23
|
-
def acquire
|
23
|
+
def acquire(&block)
|
24
24
|
fiber = Fiber.current
|
25
|
-
return @acquired_resources[fiber] if @acquired_resources[fiber]
|
25
|
+
return yield @acquired_resources[fiber] if @acquired_resources[fiber]
|
26
26
|
|
27
|
+
acquire_from_stock(fiber, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def acquire_from_stock(fiber)
|
27
31
|
add_to_stock if (@stock.empty? || @stock.pending?) && @size < @limit
|
28
32
|
resource = @stock.shift
|
29
33
|
@acquired_resources[fiber] = resource
|
30
34
|
yield resource
|
31
35
|
ensure
|
32
|
-
@acquired_resources
|
33
|
-
|
36
|
+
if resource && @acquired_resources[fiber] == resource
|
37
|
+
@acquired_resources.delete(fiber)
|
38
|
+
@stock.push resource
|
39
|
+
end
|
34
40
|
end
|
35
41
|
|
36
42
|
def method_missing(sym, *args, &block)
|
@@ -45,7 +51,14 @@ module Polyphony
|
|
45
51
|
# @return [any] allocated resource
|
46
52
|
def add_to_stock
|
47
53
|
@size += 1
|
48
|
-
|
54
|
+
resource = @allocator.call
|
55
|
+
@stock << resource
|
56
|
+
end
|
57
|
+
|
58
|
+
# Discards the currently-acquired resource
|
59
|
+
# instead of returning it to the pool when done.
|
60
|
+
def discard!
|
61
|
+
@size -= 1 if @acquired_resources.delete(Fiber.current)
|
49
62
|
end
|
50
63
|
|
51
64
|
def preheat!
|
@@ -46,6 +46,10 @@ class ::Exception
|
|
46
46
|
|
47
47
|
backtrace.reject { |l| l[POLYPHONY_DIR] }
|
48
48
|
end
|
49
|
+
|
50
|
+
def invoke
|
51
|
+
Kernel.raise(self)
|
52
|
+
end
|
49
53
|
end
|
50
54
|
|
51
55
|
# Overrides for Process
|
@@ -140,8 +144,16 @@ module ::Kernel
|
|
140
144
|
def trap(sig, command = nil, &block)
|
141
145
|
return orig_trap(sig, command) if command.is_a? String
|
142
146
|
|
143
|
-
block = command if command.respond_to?(:call)
|
144
|
-
|
147
|
+
block = command if !block && command.respond_to?(:call)
|
148
|
+
if block
|
149
|
+
exception = Polyphony::Interjection.new(block)
|
150
|
+
else
|
151
|
+
exception = command.is_a?(Class) && command.new
|
152
|
+
end
|
153
|
+
|
154
|
+
unless exception
|
155
|
+
raise ArgumentError, "Must supply block or exception or callable object"
|
156
|
+
end
|
145
157
|
|
146
158
|
# The signal trap can be invoked at any time, including while the system
|
147
159
|
# agent is blocking while polling for events. In order to deal with this
|
@@ -152,12 +164,7 @@ module ::Kernel
|
|
152
164
|
# If the command argument is an exception class however, it will be raised
|
153
165
|
# directly in the context of the main fiber.
|
154
166
|
orig_trap(sig) do
|
155
|
-
|
156
|
-
Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
|
157
|
-
else
|
158
|
-
fiber = spin { snooze; block.call }
|
159
|
-
Thread.current.break_out_of_ev_loop(fiber, nil)
|
160
|
-
end
|
167
|
+
Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
|
161
168
|
end
|
162
169
|
end
|
163
170
|
end
|
data/lib/polyphony/version.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -32,6 +32,8 @@ Gem::Specification.new do |s|
|
|
32
32
|
s.add_development_dependency 'redis', '4.1.0'
|
33
33
|
s.add_development_dependency 'hiredis', '0.6.3'
|
34
34
|
s.add_development_dependency 'http_parser.rb', '~>0.6.0'
|
35
|
+
s.add_development_dependency 'mysql2', '0.5.3'
|
36
|
+
s.add_development_dependency 'sequel', '5.34.0'
|
35
37
|
|
36
38
|
s.add_development_dependency 'jekyll', '~>3.8.6'
|
37
39
|
s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
|
data/test/test_fiber.rb
CHANGED
@@ -413,6 +413,22 @@ class FiberTest < MiniTest::Test
|
|
413
413
|
f2&.stop
|
414
414
|
end
|
415
415
|
|
416
|
+
def test_interject
|
417
|
+
buf = []
|
418
|
+
f = spin_loop { sleep }
|
419
|
+
snooze
|
420
|
+
f.interject { buf << Fiber.current }
|
421
|
+
snooze
|
422
|
+
assert_equal [f], buf
|
423
|
+
assert_equal :waiting, f.state
|
424
|
+
|
425
|
+
f.interject { buf << :foo; raise Polyphony::MoveOn }
|
426
|
+
snooze
|
427
|
+
|
428
|
+
assert_equal [f, :foo], buf
|
429
|
+
assert_equal :dead, f.state
|
430
|
+
end
|
431
|
+
|
416
432
|
def test_state
|
417
433
|
counter = 0
|
418
434
|
f = spin do
|
data/test/test_resource_pool.rb
CHANGED
@@ -27,6 +27,27 @@ class ResourcePoolTest < MiniTest::Test
|
|
27
27
|
assert_equal ['a', 'b', 'a', 'b'], results
|
28
28
|
end
|
29
29
|
|
30
|
+
def test_discard
|
31
|
+
resources = [+'a', +'b']
|
32
|
+
pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
|
33
|
+
|
34
|
+
results = []
|
35
|
+
4.times {
|
36
|
+
spin {
|
37
|
+
snooze
|
38
|
+
pool.acquire { |resource|
|
39
|
+
results << resource
|
40
|
+
pool.discard! if resource == 'b'
|
41
|
+
snooze
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
Fiber.current.await_all_children
|
46
|
+
|
47
|
+
assert_equal ['a', 'b', 'a', 'a'], results
|
48
|
+
assert_equal 1, pool.size
|
49
|
+
end
|
50
|
+
|
30
51
|
def test_single_resource_limit
|
31
52
|
resources = [+'a', +'b']
|
32
53
|
pool = Polyphony::ResourcePool.new(limit: 1) { resources.shift }
|
@@ -41,7 +62,7 @@ class ResourcePoolTest < MiniTest::Test
|
|
41
62
|
}
|
42
63
|
}
|
43
64
|
}
|
44
|
-
|
65
|
+
Fiber.current.await_all_children
|
45
66
|
|
46
67
|
assert_equal ['a'] * 10, results
|
47
68
|
end
|
@@ -75,12 +96,16 @@ class ResourcePoolTest < MiniTest::Test
|
|
75
96
|
resources = [+'a', +'b']
|
76
97
|
pool = Polyphony::ResourcePool.new(limit: 1) { resources.shift }
|
77
98
|
|
99
|
+
results = []
|
78
100
|
pool.acquire do |r|
|
79
|
-
|
80
|
-
|
81
|
-
|
101
|
+
results << r
|
102
|
+
2.times do
|
103
|
+
pool.acquire do |r|
|
104
|
+
results << r
|
105
|
+
end
|
82
106
|
end
|
83
107
|
end
|
108
|
+
assert_equal ['a']*3, results
|
84
109
|
end
|
85
110
|
|
86
111
|
def test_overloaded_resource_pool
|
data/test/test_signal.rb
CHANGED
@@ -56,35 +56,11 @@ class SignalTrapTest < Minitest::Test
|
|
56
56
|
assert_equal "3 - interrupted\n2 - terminated\n1 - terminated\n", buffer
|
57
57
|
end
|
58
58
|
|
59
|
-
def
|
59
|
+
def test_interrupt_signal_scheduling
|
60
60
|
i, o = IO.pipe
|
61
61
|
pid = Polyphony.fork do
|
62
62
|
i.close
|
63
|
-
|
64
|
-
f2 = spin do
|
65
|
-
# this fiber will try to create a race condition by
|
66
|
-
# - being scheduled before f1 is scheduled with the Interrupt exception
|
67
|
-
# - scheduling f1 without an exception
|
68
|
-
suspend
|
69
|
-
f1.schedule
|
70
|
-
rescue ::Interrupt => e
|
71
|
-
o.puts '2-interrupt'
|
72
|
-
raise e
|
73
|
-
end
|
74
|
-
f1 = spin do
|
75
|
-
# this fiber is the one that will be current when the
|
76
|
-
# signal is trapped
|
77
|
-
sleep 1
|
78
|
-
o << 'boom'
|
79
|
-
rescue ::Interrupt => e
|
80
|
-
o.puts '1-interrupt'
|
81
|
-
raise e
|
82
|
-
end
|
83
|
-
old_trap = trap('INT') do
|
84
|
-
f2.schedule
|
85
|
-
old_trap.()
|
86
|
-
end
|
87
|
-
Fiber.current.await_all_children
|
63
|
+
sleep
|
88
64
|
rescue ::Interrupt => e
|
89
65
|
o.puts '3-interrupt'
|
90
66
|
ensure
|
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.
|
4
|
+
version: 0.44.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-07-
|
11
|
+
date: 2020-07-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -164,6 +164,34 @@ dependencies:
|
|
164
164
|
- - "~>"
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: 0.6.0
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: mysql2
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - '='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: 0.5.3
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - '='
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: 0.5.3
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: sequel
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - '='
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: 5.34.0
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - '='
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: 5.34.0
|
167
195
|
- !ruby/object:Gem::Dependency
|
168
196
|
name: jekyll
|
169
197
|
requirement: !ruby/object:Gem::Requirement
|
@@ -293,6 +321,8 @@ files:
|
|
293
321
|
- examples/adapters/redis_client.rb
|
294
322
|
- examples/adapters/redis_pubsub.rb
|
295
323
|
- examples/adapters/redis_pubsub_perf.rb
|
324
|
+
- examples/adapters/sequel_mysql.rb
|
325
|
+
- examples/adapters/sequel_mysql_pool.rb
|
296
326
|
- examples/core/01-spinning-up-fibers.rb
|
297
327
|
- examples/core/02-awaiting-fibers.rb
|
298
328
|
- examples/core/03-interrupting.rb
|
@@ -407,9 +437,11 @@ files:
|
|
407
437
|
- lib/polyphony.rb
|
408
438
|
- lib/polyphony/adapters/fs.rb
|
409
439
|
- lib/polyphony/adapters/irb.rb
|
440
|
+
- lib/polyphony/adapters/mysql2.rb
|
410
441
|
- lib/polyphony/adapters/postgres.rb
|
411
442
|
- lib/polyphony/adapters/process.rb
|
412
443
|
- lib/polyphony/adapters/redis.rb
|
444
|
+
- lib/polyphony/adapters/sequel.rb
|
413
445
|
- lib/polyphony/adapters/trace.rb
|
414
446
|
- lib/polyphony/core/channel.rb
|
415
447
|
- lib/polyphony/core/exceptions.rb
|