rbgo 0.2.0 → 0.2.5

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: 5bccb96948128dee0adbfb357a273c55af918c35148099b90280e09c4af29a88
4
- data.tar.gz: 753b306467acaf5b46882b57a42a38d05c64a65496d6d285dca08e99330dc197
3
+ metadata.gz: d77329adc2e8773f5ffb22ef40da51ad2f4451f2032804e684a4e7287df6c387
4
+ data.tar.gz: dd92c1cd5c9ec8f0fa307cc1d8ae6731d51cb70e39d02fe9641aba0dd8d598f9
5
5
  SHA512:
6
- metadata.gz: '090379aa03738950d87b5b399a83f5e11b3aef62e6189e94b9df633f6ad7db41f5b5b53dd871b73831b6c1e09d210d71221d9702ecd1d844687d9d6d37128902'
7
- data.tar.gz: b1689789770582e853220b084157b3477e7f095e1e0ea4725ef9b7f8b46572e0e71dc4c55c8d78b76cc5adeb12e5dbacd972813728ae8b0dcc66fa9a18e9629b
6
+ metadata.gz: b5519621d6a4c7f2354037157901a86d2a060ca71d459ca9ac42f692e6c9f8dab32138dfeb571ea3a562f640f4118bcbaa720a79175724200259a1cb51a8ab78
7
+ data.tar.gz: e41d09d68fbe4d61222408d6899625627edebf56f2141b6c13a1e5e45a4ad181149520361b8a27a7f236b283875e8d0042dc989a059511e205c0e7814f9e5f00
data/README.md CHANGED
@@ -130,19 +130,111 @@ actor = Rbgo::Actor.new do|msg, actor|
130
130
  actor.send_msg :msg1 #won't block
131
131
 
132
132
  ```
133
+ # IOMachine
134
+ IOMachine wrap nio4r to do IO operation asynchronously.
135
+
136
+ support platforms: MRI, JRuby.
137
+
138
+ *Not supported platform: Truffleruby*
139
+
140
+
141
+ ```ruby
142
+ require 'rbgo'
143
+
144
+ io_r, io_w = IO.pipe
145
+ machine = Rbgo::IOMachine.new
146
+
147
+ receipt1 = machine.do_read(io_r, length: 100) # when length > 0, result nil if have not read anything yet when read complete.
148
+ receipt2 = machine.do_read(io_r, length: 100) # when length >0, result data bytes length up to 100 if have read some when read complete.
149
+ receipt3 = machine.do_read(io_r, length: 0) # when length == 0, result ""
150
+ receipt4 = machine.do_read(io_r, length: nil) # when length == nil, read until EOF. return "" if have not read anything.
151
+
152
+ io_w.write("a"*100)
153
+ io_w.write("b"*100)
154
+ io_w.write("c"*100)
155
+ io_w.close # cause EOF
156
+
157
+ # if the same io object, and the same read/write operation, operations will complete in sequence.
158
+ # so receipt1 complete first, and the receipt2 ...
159
+ # if the same io object, but not the same read/write operation,
160
+ # or not the same io object, operations will complete in arbitrary order.
161
+ receipt1.wait
162
+ p receipt1.res # aaa...
163
+ receipt2.wait
164
+ p receipt2.res # bbb...
165
+ receipt3.wait
166
+ p receipt3.res # ""
167
+ receipt4.wait
168
+ p receipt4.res # ccc...
169
+
170
+ io_r, io_w = IO.pipe
171
+ receipt1 = machine.do_write(io_w, str: "hello world!")
172
+
173
+ receipt1.wait
174
+ p receipt1.res # number of bytes written, may be less than str.bytesize if exception raised
175
+
176
+ ```
177
+
178
+ # yield_read / yield_write
179
+ use IOMachine to do IO operation asynchronously, but write code in a sequential way.
180
+
181
+ No callback and another callback...
182
+ ```ruby
183
+ require 'rbgo'
184
+ using Rbgo::CoRunExtensions
185
+
186
+ io_r, io_w = IO.pipe
187
+ go do
188
+ data = io_r.yield_read(100) # have blocking semantics, but execute asynchronously
189
+ # this operation will *NOT* block the current thread
190
+ # when io operation complete, thread will resume to execute from this point.
191
+ p data
192
+ end
193
+
194
+ sleep 2
195
+
196
+ io_w.write("haha I'm crazy.")
197
+ io_w.close
198
+
199
+ # NOTICE yield_read / yield_write will do yield only in CoRun::Routine.new(*args, new_thread: false, &blk) block
200
+ # in other place yield_read / yield_write will do normal IO#read / IO#write
201
+ # for example:
202
+ # go do
203
+ # io_r.yield_read # will do yield
204
+ # end
205
+ #
206
+ # go do
207
+ # fiber = Fiber.new do
208
+ # io_r.yield_read # will not do yield. just do normal IO#read
209
+ # end
210
+ # fiber.resume
211
+ # end
212
+ #
213
+ # go! do
214
+ # io_r.yield_read # will not do yield. just do normal IO#read
215
+ # end
216
+
217
+ ```
218
+
219
+
133
220
  # NetworkService
134
221
 
135
222
  open TCP or UDP service
136
223
 
137
224
  Because service handles network request in async mode, it can handle many requests concurrently. If use some Non-GIL ruby implementations such as TruffleRuby or JRuby, it can utilize all your CPU cores.
138
225
 
226
+ Use IO#yield_read IO#yield_write to do IO operations
139
227
 
140
228
  ```ruby
141
229
  require 'rbgo'
142
230
 
231
+ using Rbgo::CoRunExtensions
232
+
143
233
  #localhost, port 3000
144
234
  tcp_service = Rbgo::NetworkServiceFactory.open_tcp_service(3000) do|sock, clientAddrInfo|
145
235
  p [sock, clientAddrInfo]
236
+ p sock.yield_read
237
+ sock.yield_write("hello")
146
238
  sock.close #SHOULD CLOSE SOCK MANUALLY since version 0.2.0
147
239
  end
148
240
 
data/lib/rbgo/actor.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  require 'thread'
2
- require_relative 'corun'
3
2
 
4
- using Rbgo::CoRunExtensions
5
3
 
6
4
  module Rbgo
7
5
  class Actor
@@ -37,7 +35,7 @@ module Rbgo
37
35
  private
38
36
 
39
37
  def start_msg_loop
40
- go! do
38
+ CoRun::Routine.new(new_thread: true) do
41
39
  while msg = mail_box.deq
42
40
  handler.call(msg, self) rescue nil
43
41
  end
data/lib/rbgo/corun.rb CHANGED
@@ -2,9 +2,35 @@ require 'thread'
2
2
  require 'fiber'
3
3
  require 'system'
4
4
  require 'singleton'
5
+ require_relative 'io_machine'
5
6
 
6
7
  module Rbgo
7
8
  module CoRun
9
+ IS_CORUN_FIBER = :is_corun_fiber_bbc0f70e
10
+ YIELD_IO_OPERATION = :yield_bbc0f70e
11
+
12
+ def self.is_in_corun_fiber?
13
+ !!Thread.current[IS_CORUN_FIBER]
14
+ end
15
+
16
+ def self.read_from(io, length: nil)
17
+ if is_in_corun_fiber?
18
+ return "" if length == 0
19
+ receipt = Scheduler.instance.io_machine.do_read(io, length: length)
20
+ Fiber.yield [YIELD_IO_OPERATION, receipt]
21
+ else
22
+ io.read(length)
23
+ end
24
+ end
25
+
26
+ def self.write_to(io, str:)
27
+ if is_in_corun_fiber?
28
+ receipt = Scheduler.instance.io_machine.do_write(io, str: str)
29
+ Fiber.yield [YIELD_IO_OPERATION, receipt]
30
+ else
31
+ io.write(str)
32
+ end
33
+ end
8
34
 
9
35
  class Routine
10
36
  attr_accessor :error
@@ -16,7 +42,7 @@ module Rbgo
16
42
 
17
43
  private
18
44
 
19
- attr_accessor :args, :blk, :fiber
45
+ attr_accessor :args, :blk, :fiber, :io_receipt
20
46
 
21
47
  def initialize(*args, new_thread: false, &blk)
22
48
  self.args = args
@@ -33,6 +59,7 @@ module Rbgo
33
59
  end
34
60
  rescue Exception => ex
35
61
  self.error = ex
62
+ STDERR.puts ex
36
63
  end
37
64
  end
38
65
 
@@ -43,11 +70,28 @@ module Rbgo
43
70
 
44
71
  def perform
45
72
  self.fiber = Fiber.new do |args|
73
+ Thread.current[IS_CORUN_FIBER] = true
46
74
  blk.call(*args)
47
75
  end if fiber.nil?
48
76
 
49
77
  if fiber.alive?
50
- fiber.resume(*args)
78
+ if io_receipt.nil?
79
+ obj = fiber.resume(*args)
80
+ else
81
+ if io_receipt.done_flag
82
+ obj = fiber.resume(io_receipt.res)
83
+ else
84
+ return nil
85
+ end
86
+ end
87
+ if obj.is_a?(Array) &&
88
+ obj.size == 2 &&
89
+ obj[0] == YIELD_IO_OPERATION &&
90
+ obj[1].is_a?(IOReceipt)
91
+ self.io_receipt = obj[1]
92
+ else
93
+ self.io_receipt = nil
94
+ end
51
95
  end
52
96
  nil
53
97
  end
@@ -56,7 +100,7 @@ module Rbgo
56
100
  class Scheduler
57
101
 
58
102
  include Singleton
59
- attr_accessor :num_thread, :check_interval
103
+ attr_accessor :num_thread, :check_interval, :io_machine
60
104
 
61
105
  private
62
106
 
@@ -66,7 +110,7 @@ module Rbgo
66
110
  attr_accessor :supervisor_thread
67
111
 
68
112
  def initialize
69
- self.num_thread = System::CPU.count rescue 8
113
+ self.num_thread = System::CPU.count rescue 8
70
114
  self.thread_pool = []
71
115
 
72
116
  self.msg_queue = Queue.new
@@ -74,6 +118,8 @@ module Rbgo
74
118
 
75
119
  self.check_interval = 0.1
76
120
 
121
+ self.io_machine = IOMachine.new
122
+
77
123
  msg_queue << :init
78
124
  create_supervisor_thread
79
125
  generate_check_msg
@@ -85,22 +131,38 @@ module Rbgo
85
131
  thread_pool << Thread.new do
86
132
  Thread.current.report_on_exception = false
87
133
  begin
88
- should_exit = false
89
- yield_task_queue = Queue.new
90
- local_task_queue = Queue.new
134
+ should_exit = false
135
+ yield_task_queue = Queue.new
136
+ pending_io_task_queue = Queue.new
137
+ local_task_queue = Queue.new
91
138
  loop do
92
139
  task = nil
93
140
  if local_task_queue.empty?
94
141
  task = task_queue.deq(true) rescue nil
95
142
  if task.nil?
96
143
  task = yield_task_queue.deq unless yield_task_queue.empty?
97
- task = task_queue.deq if task.nil?
98
- local_task_queue << task
99
- else
100
- local_task_queue << task
101
- local_task_queue << yield_task_queue.deq unless yield_task_queue.empty?
144
+ if task.nil?
145
+ task = pending_io_task_queue.deq unless pending_io_task_queue.empty?
146
+ if task.nil?
147
+ task = task_queue.deq
148
+ else
149
+ sleep 0.1 # only pending io tasks in queue
150
+ end
151
+ else
152
+ receipt = task.send(:io_receipt)
153
+ if receipt
154
+ pending_io_task_queue << task unless receipt.done_flag
155
+ next
156
+ end
157
+ end
102
158
  end
159
+
160
+ local_task_queue << task
161
+ local_task_queue << yield_task_queue.deq unless yield_task_queue.empty?
162
+ local_task_queue << pending_io_task_queue.deq unless pending_io_task_queue.empty?
163
+
103
164
  end
165
+
104
166
  task = local_task_queue.deq
105
167
 
106
168
  begin
@@ -108,6 +170,7 @@ module Rbgo
108
170
  task.send :perform
109
171
  rescue Exception => ex
110
172
  task.error = ex
173
+ STDERR.puts(ex)
111
174
  next
112
175
  ensure
113
176
  Thread.current.thread_variable_set(:performing, false)
@@ -119,6 +182,7 @@ module Rbgo
119
182
 
120
183
  should_exit = Thread.current.thread_variable_get(:should_exit) &&
121
184
  yield_task_queue.empty? &&
185
+ pending_io_task_queue.empty? &&
122
186
  local_task_queue.empty?
123
187
  break if should_exit
124
188
  end
@@ -202,6 +266,8 @@ module Rbgo
202
266
  end
203
267
  end
204
268
 
269
+ CoRun.freeze
270
+
205
271
  module CoRunExtensions
206
272
  refine Object do
207
273
  def go(*args, &blk)
@@ -212,5 +278,15 @@ module Rbgo
212
278
  CoRun::Routine.new(*args, new_thread: true, &blk)
213
279
  end
214
280
  end
281
+
282
+ refine IO do
283
+ def yield_read(len = nil)
284
+ CoRun.read_from(self, length: len)
285
+ end
286
+
287
+ def yield_write(str)
288
+ CoRun.write_to(self, str: str)
289
+ end
290
+ end
215
291
  end
216
292
  end
@@ -0,0 +1,266 @@
1
+ require 'nio'
2
+ require_relative 'actor'
3
+
4
+ module Rbgo
5
+ class IOReceipt
6
+ attr_reader :registered_op, :done_flag
7
+ attr_accessor :res
8
+
9
+ def wait
10
+ mutex.synchronize do
11
+ until done_flag
12
+ cond.wait(mutex)
13
+ end
14
+ end
15
+ nil
16
+ end
17
+
18
+ def notify
19
+ mutex.synchronize do
20
+ self.done_flag = true
21
+ cond.signal
22
+ end
23
+ nil
24
+ end
25
+
26
+ private
27
+
28
+ attr_accessor :mutex, :cond
29
+ attr_writer :registered_op, :done_flag
30
+
31
+ def initialize(op)
32
+ self.done_flag = false
33
+ self.mutex = Mutex.new
34
+ self.cond = ConditionVariable.new
35
+ self.registered_op = op
36
+ end
37
+ end
38
+
39
+
40
+ class IOMachine
41
+ def do_read(io, length: nil)
42
+ op = [:register_read, io, length]
43
+ receipt = IOReceipt.new(op)
44
+ actor.send_msg(receipt)
45
+ receipt
46
+ end
47
+
48
+ def do_write(io, str:)
49
+ op = [:register_write, io, str]
50
+ receipt = IOReceipt.new(op)
51
+ actor.send_msg(receipt)
52
+ receipt
53
+ end
54
+
55
+ def close
56
+ actor.close
57
+ selector.close
58
+ end
59
+
60
+ def closed?
61
+ actor.closed? && selector.closed?
62
+ end
63
+
64
+ private
65
+
66
+ attr_accessor :selector, :actor, :monitors
67
+
68
+ def initialize
69
+ self.selector = NIO::Selector.new
70
+ self.monitors = {}
71
+
72
+ self.actor = Actor.new do |msg, actor|
73
+ if msg == :do_select
74
+ handle_select_msg(actor)
75
+ next
76
+ end
77
+
78
+ receipt = msg
79
+ op = receipt.registered_op
80
+
81
+ case
82
+ when param_pattern_match([:register_read, IO, Integer], op)
83
+ handle_read_msg(receipt, actor)
84
+ when param_pattern_match([:register_read, IO, nil], op)
85
+ handle_read_msg(receipt, actor)
86
+ when param_pattern_match([:register_write, IO, String], op)
87
+ handle_write_msg(receipt, actor)
88
+ end
89
+ end #end of actor
90
+
91
+ end
92
+
93
+ #end of initialize
94
+
95
+
96
+ def handle_select_msg(actor)
97
+ return if selector.empty?
98
+ selector.select(0.1) do |monitor|
99
+ if monitor.readiness == :r
100
+ monitor.value[0].call
101
+ elsif monitor.readiness == :w
102
+ monitor.value[1].call
103
+ else
104
+ monitor.value[0].call
105
+ monitor.value[1].call
106
+ end
107
+ end
108
+ return if selector.empty?
109
+ actor.send_msg :do_select
110
+ nil
111
+ end
112
+
113
+
114
+ def handle_read_msg(receipt, actor)
115
+ op = receipt.registered_op
116
+ io = op[1]
117
+ len = op[2]
118
+ res = StringIO.new
119
+ buf_size = 1024 * 512
120
+ registered_monitor = monitors[io]
121
+ if registered_monitor && (registered_monitor.interests == :r || registered_monitor.interests == :rw)
122
+ actor.send_msg receipt
123
+ return
124
+ end
125
+
126
+ if registered_monitor
127
+ registered_monitor.add_interest(:r)
128
+ monitor = registered_monitor
129
+ else
130
+ monitor = selector.register(io, :r)
131
+ monitors[io] = monitor
132
+ end
133
+
134
+ monitor.value ||= []
135
+ monitor.value[0] = proc do
136
+ if len.nil?
137
+ notify_blk = proc do
138
+ monitors.delete(monitor.io)
139
+ monitor.close
140
+ receipt.res = res.string
141
+ receipt.notify
142
+ end
143
+ loop do
144
+ begin
145
+ buf = io.read_nonblock(buf_size, exception: false)
146
+ rescue Exception => ex
147
+ notify_blk.call
148
+ STDERR.puts ex
149
+ break
150
+ end
151
+ if buf == :wait_readable
152
+ break
153
+ elsif buf.nil?
154
+ notify_blk.call
155
+ break
156
+ end
157
+ res << buf
158
+ end
159
+ elsif len == 0
160
+ monitors.delete(monitor.io)
161
+ monitor.close
162
+ receipt.res = ""
163
+ receipt.notify
164
+ break
165
+ else
166
+ notify_blk = proc do
167
+ monitors.delete(monitor.io)
168
+ monitor.close
169
+ if res.string.length == 0
170
+ receipt.res = nil
171
+ else
172
+ receipt.res = res.string
173
+ end
174
+ receipt.notify
175
+ end
176
+
177
+ bytes_read_n = 0
178
+ loop do
179
+ need_read_bytes_n = len - bytes_read_n
180
+ if need_read_bytes_n <= 0
181
+ notify_blk.call
182
+ break
183
+ end
184
+ begin
185
+ buf = io.read_nonblock(need_read_bytes_n, exception: false)
186
+ rescue Exception => ex
187
+ notify_blk.call
188
+ STDERR.puts ex
189
+ break
190
+ end
191
+ if buf == :wait_readable
192
+ break
193
+ elsif buf.nil?
194
+ notify_blk.call
195
+ break
196
+ end
197
+ res << buf
198
+ bytes_read_n += buf.bytesize
199
+ end
200
+ end
201
+ end
202
+ actor.send_msg :do_select
203
+ end
204
+
205
+
206
+ def handle_write_msg(receipt, actor)
207
+ op = receipt.registered_op
208
+ io = op[1]
209
+ str = op[2].to_s
210
+
211
+ registered_monitor = monitors[io]
212
+ if registered_monitor && (registered_monitor.interests == :w || registered_monitor.interests == :rw)
213
+ actor.send_msg receipt
214
+ return
215
+ end
216
+
217
+ if registered_monitor
218
+ registered_monitor.add_interest(:w)
219
+ monitor = registered_monitor
220
+ else
221
+ monitor = selector.register(io, :w)
222
+ monitors[io] = monitor
223
+ end
224
+
225
+ buf = NIO::ByteBuffer.new(str.bytesize)
226
+ buf << str
227
+ buf.flip
228
+ bytes_written_n = 0
229
+ monitor.value ||= []
230
+ monitor.value[1] = proc do
231
+ begin
232
+ if buf.remaining > 0
233
+ n = buf.write_to(io)
234
+ bytes_written_n += n
235
+ else
236
+ monitors.delete(monitor.io)
237
+ monitor.close
238
+ receipt.res = str.bytesize
239
+ receipt.notify
240
+ end
241
+ rescue Exception => ex
242
+ monitors.delete(monitor.io)
243
+ monitor.close
244
+ receipt.res = bytes_written_n
245
+ receipt.notify
246
+ STDERR.puts ex
247
+ end
248
+ end
249
+ actor.send_msg :do_select
250
+ end
251
+
252
+
253
+ def param_pattern_match(pattern, params)
254
+ return false unless pattern.is_a?(Array) && params.is_a?(Array)
255
+ return false if pattern.size != params.size
256
+ match = true
257
+ pattern.zip(params) do |type, param|
258
+ unless type === param
259
+ match = false
260
+ break
261
+ end
262
+ end
263
+ match
264
+ end
265
+ end
266
+ end
@@ -19,6 +19,7 @@ module Rbgo
19
19
 
20
20
  def register(io)
21
21
  register_mutex.synchronize do
22
+ ios.delete_if { |io| io.closed? }
22
23
  ios << io
23
24
  end
24
25
  end
@@ -53,7 +54,7 @@ module Rbgo
53
54
  self.have_enq_waiting_flag = false
54
55
  self.have_deq_waiting_flag = false
55
56
 
56
- self.ios = []
57
+ self.ios = []
57
58
  self.register_mutex = Mutex.new
58
59
  end
59
60
 
@@ -202,7 +203,7 @@ module Rbgo
202
203
  super(max)
203
204
  @mutex = Mutex.new
204
205
 
205
- self.ios = []
206
+ self.ios = []
206
207
  self.register_mutex = Mutex.new
207
208
  end
208
209
 
data/lib/rbgo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rbgo
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.5"
3
3
  end
data/lib/rbgo.rb CHANGED
@@ -3,5 +3,6 @@ require_relative 'rbgo/select_chan'
3
3
  require_relative 'rbgo/wait_group'
4
4
  require_relative 'rbgo/once'
5
5
  require_relative 'rbgo/actor'
6
+ require_relative 'rbgo/io_machine'
6
7
  require_relative 'rbgo/network_service'
7
8
  require_relative 'rbgo/version'
data/rbgo.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
  In MRI write program to run concurrently even not parallelly is also important.
21
21
  END
22
22
  s.add_dependency "system", "~> 0.1.3"
23
+ s.add_dependency "nio4r", "~> 2.4"
23
24
  s.files = `git ls-files`.split("\n")
24
25
  s.license = 'MIT'
25
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbgo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wang Yin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-07 00:00:00.000000000 Z
11
+ date: 2019-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: system
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.1.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: nio4r
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.4'
27
41
  description: " Write concurrent program with Ruby in Golang style.
28
42
  \ \n\n You can produce a light weight routine easily with a method
29
43
  'go' and communicate with each routine by channel.\n\n In MRI
@@ -44,6 +58,7 @@ files:
44
58
  - lib/rbgo.rb
45
59
  - lib/rbgo/actor.rb
46
60
  - lib/rbgo/corun.rb
61
+ - lib/rbgo/io_machine.rb
47
62
  - lib/rbgo/network_service.rb
48
63
  - lib/rbgo/once.rb
49
64
  - lib/rbgo/select_chan.rb