drbqs 0.0.17 → 0.0.18

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 (50) hide show
  1. data/docs/FormatExecute.md +44 -2
  2. data/example/group/execute.rb +19 -0
  3. data/example/group/server.rb +27 -0
  4. data/example/group/sum.rb +9 -0
  5. data/example/mandelbrot/README.md +8 -0
  6. data/example/mandelbrot/execute.rb +4 -0
  7. data/lib/drbqs/command_line/command_node.rb +1 -0
  8. data/lib/drbqs/execute/execute_node.rb +4 -21
  9. data/lib/drbqs/node/connection.rb +1 -2
  10. data/lib/drbqs/node/node.rb +163 -102
  11. data/lib/drbqs/node/state.rb +100 -35
  12. data/lib/drbqs/node/task_client.rb +46 -33
  13. data/lib/drbqs/server/message.rb +13 -7
  14. data/lib/drbqs/server/server.rb +57 -29
  15. data/lib/drbqs/server/server_hook.rb +19 -5
  16. data/lib/drbqs/server/test/node.rb +31 -6
  17. data/lib/drbqs/setting/node.rb +11 -2
  18. data/lib/drbqs/setting/server.rb +1 -1
  19. data/lib/drbqs/task/task.rb +26 -6
  20. data/lib/drbqs/task/task_generator.rb +2 -1
  21. data/lib/drbqs/utility/temporary.rb +27 -6
  22. data/lib/drbqs/utility/transfer/transfer_client.rb +10 -12
  23. data/lib/drbqs/version.rb +1 -1
  24. data/lib/drbqs/worker.rb +2 -0
  25. data/lib/drbqs/worker/forked_process.rb +100 -0
  26. data/lib/drbqs/worker/serialize.rb +66 -0
  27. data/lib/drbqs/worker/worker.rb +133 -0
  28. data/lib/drbqs/worker/worker_process_set.rb +219 -0
  29. data/spec/integration_test/01_basic_usage_spec.rb +3 -2
  30. data/spec/integration_test/06_node_exit_after_task_spec.rb +3 -2
  31. data/spec/integration_test/07_command_server_with_node_spec.rb +1 -0
  32. data/spec/integration_test/08_shutdown_unused_nodes_spec.rb +3 -2
  33. data/spec/integration_test/10_test_server_spec.rb +2 -2
  34. data/spec/integration_test/11_special_tasks_spec.rb +61 -0
  35. data/spec/integration_test/12_multiple_workers_spec.rb +43 -0
  36. data/spec/integration_test/definition/task_obj_definition.rb +33 -6
  37. data/spec/node/connection_spec.rb +6 -6
  38. data/spec/node/node_spec.rb +10 -2
  39. data/spec/node/state_spec.rb +146 -62
  40. data/spec/node/task_client_spec.rb +58 -53
  41. data/spec/server/message_spec.rb +10 -6
  42. data/spec/server/queue_spec.rb +7 -4
  43. data/spec/server/server_hook_spec.rb +28 -1
  44. data/spec/task/task_spec.rb +43 -6
  45. data/spec/utility/temporary_spec.rb +32 -9
  46. data/spec/worker/forked_process_spec.rb +66 -0
  47. data/spec/worker/serialize_spec.rb +73 -0
  48. data/spec/worker/worker_process_set_spec.rb +104 -0
  49. data/spec/worker/worker_spec.rb +127 -0
  50. metadata +34 -19
@@ -0,0 +1,66 @@
1
+ module DRbQS
2
+ class Worker
3
+ class Serialize
4
+ HEADER_BYTE_SIZE = 4
5
+
6
+ class Unpacker
7
+ def initialize
8
+ @chunk = ''
9
+ @next_size = nil
10
+ end
11
+
12
+ def feed(data)
13
+ @chunk << data
14
+ end
15
+
16
+ def next_object
17
+ unless @next_size
18
+ if @chunk.bytesize >= HEADER_BYTE_SIZE
19
+ @chunk.force_encoding('BINARY')
20
+ @next_size = @chunk[0, HEADER_BYTE_SIZE].unpack('N')[0]
21
+ @chunk = @chunk[HEADER_BYTE_SIZE..-1]
22
+ else
23
+ return nil
24
+ end
25
+ end
26
+ if @next_size && @chunk.bytesize >= @next_size
27
+ data = @chunk[0, @next_size]
28
+ @chunk = @chunk[@next_size..-1]
29
+ @next_size = nil
30
+ [:loaded, Marshal.load(data)]
31
+ else
32
+ nil
33
+ end
34
+ end
35
+ private :next_object
36
+
37
+ def each(&block)
38
+ if block_given?
39
+ while ary = next_object
40
+ sym, obj = ary
41
+ yield(obj)
42
+ end
43
+ else
44
+ to_enum(:each)
45
+ end
46
+ end
47
+
48
+ def feed_each(data, &block)
49
+ feed(data)
50
+ each(&block)
51
+ end
52
+ end
53
+
54
+ def self.dump(obj)
55
+ str = Marshal.dump(obj)
56
+ [str.size].pack('N') << str
57
+ end
58
+
59
+ def self.load(s)
60
+ size = s[0, HEADER_BYTE_SIZE].unpack('N')[0]
61
+ data = s[HEADER_BYTE_SIZE, size]
62
+ Marshal.load(data)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,133 @@
1
+ require 'drbqs/worker/worker_process_set'
2
+
3
+ module DRbQS
4
+ # We can use DRbQS::Worker to send some child processes.
5
+ # Note that DRbQS::Worker is not used in DRbQS::Node class and then
6
+ # is not included in main part of DRbQS.
7
+ class Worker
8
+ attr_reader :process
9
+
10
+ def initialize(opts = {})
11
+ @process = DRbQS::Worker::ProcessSet.new(opts[:class])
12
+ if opts[:key]
13
+ opts[:key].each do |key|
14
+ @process.create_process(key)
15
+ end
16
+ end
17
+ @state = Hash.new { |h, k| h[k] = Hash.new }
18
+ @task_pool = {}
19
+ @task_group = Hash.new { |h, k| h[k] = Array.new }
20
+ @task_num = 0
21
+ end
22
+
23
+ def calculating?
24
+ !@task_pool.empty?
25
+ end
26
+
27
+ def sleep(*keys)
28
+ keys.each do |key|
29
+ @state[key][:sleep] = true
30
+ end
31
+ end
32
+
33
+ def wakeup(*keys)
34
+ keys.each do |key|
35
+ @state[key][:sleep] = false
36
+ end
37
+ end
38
+
39
+ def group(grp, *keys)
40
+ keys.each do |key|
41
+ (@state[key][:group] ||= []) << grp
42
+ end
43
+ end
44
+
45
+ def on_result(&block)
46
+ @process.on_result do |proc_key, ary|
47
+ task_id, result = ary
48
+ if task_data = @task_pool.delete(task_id)
49
+ task = task_data[:task]
50
+ @task_group[task.group].delete(task_id)
51
+ task.exec_hook(self, result)
52
+ end
53
+ block.call(proc_key, ary)
54
+ end
55
+ end
56
+
57
+ def on_error(&block)
58
+ @process.on_error(&block)
59
+ end
60
+
61
+ def send_task(proc_key, task_id, task)
62
+ dumped = [task_id] + task.simple_drb_args
63
+ @process.send_task(proc_key, dumped)
64
+ end
65
+ private :send_task
66
+
67
+ def add_task(task, broadcast = nil)
68
+ if broadcast
69
+ @process.all_processes.each do |proc_key|
70
+ send_task(proc_key, nil, task)
71
+ end
72
+ else
73
+ task_id = (@task_num += 1)
74
+ @task_pool[task_id] = { :task => task }
75
+ @task_group[task.group] << task_id
76
+ task_id
77
+ end
78
+ end
79
+
80
+ # This method sends a stored task for each process that is not calculating a task
81
+ # and responds signals from child processes.
82
+ def step
83
+ @process.waiting_processes.each do |proc_key|
84
+ if @state[proc_key][:sleep]
85
+ next
86
+ end
87
+ catch(:add) do
88
+ grps = (@state[proc_key][:group] || []) + [DRbQS::Task::DEFAULT_GROUP]
89
+ grps.each do |gr|
90
+ @task_group[gr].each do |task_id|
91
+ task_data = @task_pool[task_id]
92
+ if !task_data[:calculate]
93
+ send_task(proc_key, task_id, task_data[:task])
94
+ @task_pool[task_id][:calculate] = true
95
+ throw :add
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ @process.respond_signal
102
+ end
103
+
104
+ # Wait finish of task +task_id+ with sleep +interval_time+.
105
+ # @param [Fixnum] task_id
106
+ # @param [Numeric] interval_time An argument of Kernel#sleep.
107
+ def wait(task_id, interval_time)
108
+ while @task_pool[task_id]
109
+ unless step
110
+ Kernel.sleep(interval_time)
111
+ end
112
+ end
113
+ end
114
+
115
+ # Wait finishes of all tasks with sleep +interval_time+.
116
+ # @param [Numeric] interval_time An argument of Kernel#sleep.
117
+ def waitall(interval_time)
118
+ while calculating?
119
+ unless step
120
+ Kernel.sleep(interval_time)
121
+ end
122
+ end
123
+ end
124
+
125
+ # Send signal to exit to all child processes and wait the completion
126
+ # with sleep +interval_time+.
127
+ # @param [Numeric] interval_time An argument of Kernel#sleep.
128
+ def finish(interval_time = 1)
129
+ @process.prepare_to_exit
130
+ @process.waitall(interval_time)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,219 @@
1
+ require 'drbqs/worker/serialize'
2
+ require 'drbqs/worker/forked_process'
3
+ require 'drbqs/utility/temporary.rb'
4
+
5
+ module DRbQS
6
+ class Worker
7
+ READ_BYTE_SIZE = 1024
8
+
9
+ class ProcessSet
10
+ attr_reader :process
11
+
12
+ def initialize(process_class = nil)
13
+ @process_class = process_class || DRbQS::Worker::SimpleForkedProcess
14
+ @process = {}
15
+ @result = Queue.new
16
+ @on_error = nil
17
+ @on_result = nil
18
+ end
19
+
20
+ def on_error(&block)
21
+ @on_error = block
22
+ end
23
+
24
+ def on_result(&block)
25
+ @on_result = block
26
+ end
27
+
28
+ def process_create(key)
29
+ io_r, io_w = IO.pipe('BINARY')
30
+ io_r2, io_w2 = IO.pipe('BINARY')
31
+ parent_pid = Process.pid
32
+ pid = fork do
33
+ $PROGRAM_NAME = "[worker:#{key.to_s}] for drbqs-node (PID #{parent_pid})"
34
+ io_w.close
35
+ io_r2.close
36
+ worker = @process_class.new(io_r, io_w2)
37
+ worker.start
38
+ end
39
+ @process[key] = {
40
+ :pid => pid, :out => io_w, :in => io_r2,
41
+ :unpacker => DRbQS::Worker::Serialize::Unpacker.new,
42
+ :task => []
43
+ }
44
+ end
45
+ private :process_create
46
+
47
+ def get_process(key)
48
+ if @process[key]
49
+ @process[key]
50
+ else
51
+ process_create(key)
52
+ end
53
+ end
54
+ private :get_process
55
+
56
+ def create_process(*keys)
57
+ keys.each do |key|
58
+ get_process(key)
59
+ end
60
+ end
61
+
62
+ # Return true if the process of +key+ exists.
63
+ # @param [Symbol,nil] key Key of child process or nil.
64
+ def exist?(key)
65
+ @process[key]
66
+ end
67
+
68
+ def has_process?
69
+ !@process.empty?
70
+ end
71
+
72
+ # Return true if the process of +key+ is calculating.
73
+ def calculating?(key)
74
+ @process[key] && !@process[key][:task].empty?
75
+ end
76
+
77
+ # Return true if the process +key+ does not calculate any tasks.
78
+ def waiting?(key)
79
+ !calculating?(key)
80
+ end
81
+
82
+ # Return keys of processes not calculating a task.
83
+ def waiting_processes
84
+ @process.keys.select do |key|
85
+ @process[key][:task].empty?
86
+ end
87
+ end
88
+
89
+ def all_processes
90
+ @process.keys
91
+ end
92
+
93
+ def output_to_io(io, obj)
94
+ io.print Serialize.dump(obj)
95
+ io.flush
96
+ end
97
+ private :output_to_io
98
+
99
+ def send_object(key, obj)
100
+ if h = get_process(key)
101
+ output_to_io(h[:out], obj)
102
+ h
103
+ else
104
+ nil
105
+ end
106
+ end
107
+ private :send_object
108
+
109
+ # @param [Array] dumped_task_ary is [task_id, obj, method_name, args].
110
+ def send_task(key, dumped_task_ary)
111
+ if h = send_object(key, dumped_task_ary)
112
+ if dumped_task_ary[0]
113
+ h[:task] << dumped_task_ary[0]
114
+ end
115
+ else
116
+ raise "Process #{key.inspect} does not exist."
117
+ end
118
+ end
119
+
120
+ def prepare_to_exit(key = nil)
121
+ if key
122
+ if h = send_object(key, :prepare_to_exit)
123
+ h[:exit] = true
124
+ end
125
+ else
126
+ @process.each do |key, h|
127
+ prepare_to_exit(key)
128
+ end
129
+ end
130
+ end
131
+
132
+ def delete_process(key)
133
+ if h = get_process(key)
134
+ Process.detach(h[:pid])
135
+ output_to_io(h[:out], :exit)
136
+ else
137
+ nil
138
+ end
139
+ end
140
+ private :delete_process
141
+
142
+ # Read IOs and respond signals from chiled processes.
143
+ # If there no signal from childe processes then the method returns false.
144
+ # Otherwise, true.
145
+ # Types of signals are :result, :node_error, :finish_preparing_to_exit.
146
+ # - :result
147
+ # Execute callback set by DRbQS::Worker::ProcessSet#on_result.
148
+ # - :node_error
149
+ # Execute callback set by DRbQS::Worker::ProcessSet#on_error.
150
+ # - :finish_preparing_to_exit
151
+ # Send :exit signale to the process and delete from list of child processes.
152
+ def respond_signal
153
+ num = 0
154
+ to_be_deleted = []
155
+ @process.each do |key, h|
156
+ if !h[:task].empty? || h[:exit]
157
+ begin
158
+ data = h[:in].read_nonblock(READ_BYTE_SIZE)
159
+ h[:unpacker].feed_each(data) do |ary|
160
+ num += 1
161
+ response_type, response = ary
162
+ case response_type
163
+ when :result
164
+ task_id, result = response
165
+ h[:task].delete(task_id)
166
+ if @on_result
167
+ @on_result.call(key, [task_id, result])
168
+ else
169
+ $stderr.puts "The instance of DRbQS::Worker::ProcessSet can not deal with results from child processes."
170
+ end
171
+ when :node_error
172
+ if @on_error
173
+ @on_error.call(key, response)
174
+ else
175
+ $stderr.puts "The instance of DRbQS::Worker::ProcessSet can not deal with error from child processes."
176
+ end
177
+ when :finish_preparing_to_exit
178
+ delete_process(key)
179
+ to_be_deleted << key
180
+ end
181
+ end
182
+ rescue IO::WaitReadable
183
+ end
184
+ end
185
+ end
186
+ to_be_deleted.each do |key|
187
+ @process.delete(key)
188
+ end
189
+ to_be_deleted.clear
190
+ num > 0
191
+ end
192
+
193
+ def kill_all_processes
194
+ @process.each do |key, h|
195
+ Process.detach(h[:pid])
196
+ Process.kill("KILL", h[:pid])
197
+ end
198
+ @process.clear
199
+ end
200
+
201
+ WAITALL_INTERVAL_TIME = 0.1
202
+
203
+ def waitall(interval_time = nil)
204
+ unless @process.all? { |key, h| h[:exit] }
205
+ return nil
206
+ end
207
+ t = interval_time || WAITALL_INTERVAL_TIME
208
+ until @process.empty?
209
+ respond_signal
210
+ Kernel.sleep(t)
211
+ end
212
+ until Process.waitall == []
213
+ Kernel.sleep(t)
214
+ end
215
+ true
216
+ end
217
+ end
218
+ end
219
+ end
@@ -6,7 +6,7 @@ require_relative 'definition/task_obj_definition.rb'
6
6
  describe DRbQS do
7
7
  before(:all) do
8
8
  @tasks = 5.times.map do |i|
9
- DRbQS::Task.new(Test1.new, :echo, args: [i])
9
+ DRbQS::Task.new(TestCount.new, :echo, args: [i])
10
10
  end
11
11
  @process_id, @uri = drbqs_fork_server(14010, :task => @tasks)
12
12
  @node = DRbQS::Node.new(@uri, :log_file => $stdout, :continue => true)
@@ -42,10 +42,11 @@ describe DRbQS do
42
42
  lambda do
43
43
  @node.calculate
44
44
  end.should_not raise_error
45
- Test1.get_execute_echo_number.should == @tasks.size
45
+ TestCount.get_execute_echo_number.should == @tasks.size
46
46
  end
47
47
 
48
48
  after(:all) do
49
+ TestCount.clear
49
50
  lambda do
50
51
  drbqs_wait_kill_server(@process_id)
51
52
  end.should_not raise_error
@@ -15,8 +15,9 @@ describe DRbQS::Server do
15
15
 
16
16
  it "should send node exit" do
17
17
  execute_node = DRbQS::Execution::ExecuteNode.new(@uri, nil, nil)
18
- execute_node.execute(1)
19
- client_process_id = execute_node.pid[0]
18
+ client_process_id = fork do
19
+ execute_node.execute(1)
20
+ end
20
21
  sleep(@wait)
21
22
  @manage.send_node_exit_after_task(1)
22
23
  th = Process.detach(client_process_id)