serverengine 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +14 -0
- data/Changelog +5 -0
- data/Gemfile +3 -0
- data/README.md +332 -0
- data/Rakefile +14 -0
- data/lib/serverengine.rb +52 -0
- data/lib/serverengine/blocking_flag.rb +78 -0
- data/lib/serverengine/config_loader.rb +73 -0
- data/lib/serverengine/daemon.rb +165 -0
- data/lib/serverengine/daemon_logger.rb +110 -0
- data/lib/serverengine/embedded_server.rb +47 -0
- data/lib/serverengine/multi_process_server.rb +132 -0
- data/lib/serverengine/multi_thread_server.rb +61 -0
- data/lib/serverengine/multi_worker_server.rb +130 -0
- data/lib/serverengine/process_manager.rb +415 -0
- data/lib/serverengine/server.rb +103 -0
- data/lib/serverengine/signal_thread.rb +143 -0
- data/lib/serverengine/supervisor.rb +230 -0
- data/lib/serverengine/utils.rb +32 -0
- data/lib/serverengine/version.rb +3 -0
- data/lib/serverengine/worker.rb +73 -0
- data/serverengine.gemspec +25 -0
- data/spec/blocking_flag_spec.rb +58 -0
- data/spec/daemon_logger_spec.rb +88 -0
- data/spec/daemon_spec.rb +46 -0
- data/spec/multi_process_server_spec.rb +61 -0
- data/spec/server_worker_context.rb +118 -0
- data/spec/signal_thread_spec.rb +85 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/supervisor_spec.rb +142 -0
- metadata +134 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
#
|
2
|
+
# ServerEngine
|
3
|
+
#
|
4
|
+
# Copyright (C) 2012-2013 FURUHASHI Sadayuki
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
module ServerEngine
|
19
|
+
|
20
|
+
class MultiThreadServer < MultiWorkerServer
|
21
|
+
private
|
22
|
+
|
23
|
+
def start_worker(wid)
|
24
|
+
w = create_worker(wid)
|
25
|
+
|
26
|
+
w.before_fork
|
27
|
+
begin
|
28
|
+
thread = Thread.new(&w.method(:main))
|
29
|
+
ensure
|
30
|
+
w.close
|
31
|
+
end
|
32
|
+
|
33
|
+
return WorkerMonitor.new(w, thread)
|
34
|
+
end
|
35
|
+
|
36
|
+
class WorkerMonitor
|
37
|
+
def initialize(worker, thread)
|
38
|
+
@worker = worker
|
39
|
+
@thread = thread
|
40
|
+
end
|
41
|
+
|
42
|
+
def send_stop(stop_graceful)
|
43
|
+
Thread.new { @worker.stop }
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_reload
|
48
|
+
Thread.new { @worker.reload }
|
49
|
+
end
|
50
|
+
|
51
|
+
#def join
|
52
|
+
# @thread.join
|
53
|
+
#end
|
54
|
+
|
55
|
+
def alive?
|
56
|
+
@thread.alive?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
#
|
2
|
+
# ServerEngine
|
3
|
+
#
|
4
|
+
# Copyright (C) 2012-2013 FURUHASHI Sadayuki
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
module ServerEngine
|
19
|
+
|
20
|
+
class MultiWorkerServer < Server
|
21
|
+
def initialize(worker_module, load_config_proc={}, &block)
|
22
|
+
@monitors = []
|
23
|
+
@last_start_worker_time = 0
|
24
|
+
|
25
|
+
super(worker_module, load_config_proc, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop(stop_graceful)
|
29
|
+
super
|
30
|
+
@monitors.each do |m|
|
31
|
+
m.send_stop(stop_graceful) if m
|
32
|
+
end
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def restart(stop_graceful)
|
37
|
+
super
|
38
|
+
@monitors.each do |m|
|
39
|
+
m.send_stop(stop_graceful) if m
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def reload
|
45
|
+
super
|
46
|
+
@monitors.each_with_index do |m|
|
47
|
+
m.send_reload if m
|
48
|
+
end
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def run
|
53
|
+
while true
|
54
|
+
num_alive = keepalive_workers
|
55
|
+
break if num_alive == 0
|
56
|
+
wait_tick
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def scale_workers(n)
|
61
|
+
@num_workers = n
|
62
|
+
|
63
|
+
plus = n - @monitors.size
|
64
|
+
if plus > 0
|
65
|
+
@monitors.concat Array.new(plus, nil)
|
66
|
+
end
|
67
|
+
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def reload_config
|
74
|
+
super
|
75
|
+
|
76
|
+
@start_worker_delay = @config[:start_worker_delay] || 0
|
77
|
+
@start_worker_delay_rand = @config[:start_worker_delay_rand] || 0.2
|
78
|
+
|
79
|
+
scale_workers(@config[:workers] || 1)
|
80
|
+
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def wait_tick
|
85
|
+
sleep 0.5
|
86
|
+
end
|
87
|
+
|
88
|
+
def keepalive_workers
|
89
|
+
num_alive = 0
|
90
|
+
|
91
|
+
@monitors.each_with_index do |m,wid|
|
92
|
+
if m && m.alive?
|
93
|
+
# alive
|
94
|
+
num_alive += 1
|
95
|
+
|
96
|
+
elsif wid < @num_workers
|
97
|
+
# scale up or reboot
|
98
|
+
unless @stop
|
99
|
+
@monitors[wid] = delayed_start_worker(wid)
|
100
|
+
num_alive += 1
|
101
|
+
end
|
102
|
+
|
103
|
+
elsif m
|
104
|
+
# scale down
|
105
|
+
@monitors[wid] = nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return num_alive
|
110
|
+
end
|
111
|
+
|
112
|
+
def delayed_start_worker(wid)
|
113
|
+
if @start_worker_delay > 0
|
114
|
+
delay = @start_worker_delay +
|
115
|
+
Kernel.rand * @start_worker_delay * @start_worker_delay_rand -
|
116
|
+
@start_worker_delay * @start_worker_delay_rand / 2
|
117
|
+
|
118
|
+
now = Time.now.to_f
|
119
|
+
|
120
|
+
wait = delay - (now - @last_start_worker_time)
|
121
|
+
sleep wait if wait > 0
|
122
|
+
|
123
|
+
@last_start_worker_time = now
|
124
|
+
end
|
125
|
+
|
126
|
+
start_worker(wid)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,415 @@
|
|
1
|
+
#
|
2
|
+
# ServerEngine
|
3
|
+
#
|
4
|
+
# Copyright (C) 2012-2013 FURUHASHI Sadayuki
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
module ServerEngine
|
19
|
+
|
20
|
+
require 'fcntl'
|
21
|
+
|
22
|
+
class ProcessManager
|
23
|
+
def initialize(config={})
|
24
|
+
@monitors = []
|
25
|
+
@rpipes = {}
|
26
|
+
@heartbeat_time = {}
|
27
|
+
|
28
|
+
@cloexec_mode = config[:cloexec_mode]
|
29
|
+
|
30
|
+
@graceful_kill_signal = config[:graceful_kill_signal] || :TERM
|
31
|
+
@immediate_kill_signal = config[:immediate_kill_signal] || :QUIT
|
32
|
+
|
33
|
+
@auto_tick = config.fetch(:auto_tick, true)
|
34
|
+
@tick_interval = config[:tick_interval] || 1
|
35
|
+
|
36
|
+
@auto_heartbeat = config.fetch(:auto_heartbeat, true)
|
37
|
+
|
38
|
+
case op = config.fetch(:abort_on_heartbeat_error, true)
|
39
|
+
when Proc
|
40
|
+
@heartbeat_error_proc = op
|
41
|
+
when true
|
42
|
+
@heartbeat_error_proc = lambda {|t| exit 1 }
|
43
|
+
when false
|
44
|
+
@heartbeat_error_proc = lambda {|t| }
|
45
|
+
else
|
46
|
+
raise ArgumentError, "unexpected :abort_on_heartbeat_error option (expected Proc, true or false but got #{op.class})"
|
47
|
+
end
|
48
|
+
|
49
|
+
configure(config)
|
50
|
+
|
51
|
+
@closed = false
|
52
|
+
@read_buffer = ''
|
53
|
+
|
54
|
+
if @auto_tick
|
55
|
+
TickThread.new(self)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_accessor :logger
|
60
|
+
|
61
|
+
attr_accessor :cloexec_mode
|
62
|
+
|
63
|
+
CONFIG_PARAMS = {
|
64
|
+
heartbeat_interval: 1,
|
65
|
+
heartbeat_timeout: 60,
|
66
|
+
graceful_kill_interval: 2,
|
67
|
+
graceful_kill_interval_increment: 2,
|
68
|
+
graceful_kill_timeout: -1,
|
69
|
+
immediate_kill_interval: 2,
|
70
|
+
immediate_kill_interval_increment: 2,
|
71
|
+
immediate_kill_timeout: 60,
|
72
|
+
}
|
73
|
+
|
74
|
+
attr_reader :graceful_kill_signal, :immediate_kill_signal
|
75
|
+
|
76
|
+
CONFIG_PARAMS.each_pair do |key,default_value|
|
77
|
+
attr_reader key
|
78
|
+
|
79
|
+
define_method("#{key}=") do |v|
|
80
|
+
v = default_value if v == nil
|
81
|
+
instance_variable_set("@#{key}", v)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def configure(config, opts={})
|
86
|
+
prefix = opts[:prefix] || ""
|
87
|
+
CONFIG_PARAMS.keys.each {|key|
|
88
|
+
send("#{key}=", config[:"#{prefix}#{key}"])
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def fork(&block)
|
93
|
+
rpipe, wpipe = new_pair
|
94
|
+
|
95
|
+
begin
|
96
|
+
pid = Process.fork do
|
97
|
+
self.close
|
98
|
+
begin
|
99
|
+
t = Target.new(wpipe)
|
100
|
+
if @auto_heartbeat
|
101
|
+
HeartbeatThread.new(self, t, @heartbeat_error_proc)
|
102
|
+
end
|
103
|
+
|
104
|
+
block.call(t)
|
105
|
+
exit! 0
|
106
|
+
|
107
|
+
rescue
|
108
|
+
ServerEngine.dump_uncaught_error($!)
|
109
|
+
ensure
|
110
|
+
exit! 1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
m = Monitor.new(self, pid)
|
115
|
+
|
116
|
+
@monitors << m
|
117
|
+
@rpipes[rpipe] = m
|
118
|
+
|
119
|
+
return m
|
120
|
+
|
121
|
+
ensure
|
122
|
+
wpipe.close
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def new_pair
|
127
|
+
rpipe, wpipe = IO.pipe
|
128
|
+
|
129
|
+
case @cloexec_mode
|
130
|
+
when :target_only
|
131
|
+
wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
132
|
+
when :monitor_only
|
133
|
+
rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
134
|
+
else
|
135
|
+
rpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
136
|
+
wpipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
137
|
+
end
|
138
|
+
|
139
|
+
rpipe.sync = true
|
140
|
+
wpipe.sync = true
|
141
|
+
|
142
|
+
return rpipe, wpipe
|
143
|
+
end
|
144
|
+
|
145
|
+
def close
|
146
|
+
@closed = true
|
147
|
+
@rpipes.keys.each {|m| m.close }
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
|
151
|
+
def tick(blocking_timeout=0)
|
152
|
+
if @closed
|
153
|
+
raise AlreadyClosedError.new
|
154
|
+
end
|
155
|
+
|
156
|
+
if @rpipes.empty?
|
157
|
+
sleep blocking_timeout if blocking_timeout > 0
|
158
|
+
return nil
|
159
|
+
end
|
160
|
+
|
161
|
+
ready_pipes, _, _ = IO.select(@rpipes.keys, nil, nil, blocking_timeout)
|
162
|
+
unless ready_pipes
|
163
|
+
return nil
|
164
|
+
end
|
165
|
+
|
166
|
+
time ||= Time.now
|
167
|
+
|
168
|
+
ready_pipes.each do |r|
|
169
|
+
begin
|
170
|
+
r.read_nonblock(1024, @read_buffer)
|
171
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
172
|
+
next
|
173
|
+
rescue #EOFError
|
174
|
+
m = @rpipes.delete(r)
|
175
|
+
m.start_immediate_stop!
|
176
|
+
r.close rescue nil
|
177
|
+
next
|
178
|
+
end
|
179
|
+
|
180
|
+
if m = @rpipes[r]
|
181
|
+
m.last_heartbeat_time = time
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
@monitors.delete_if {|m|
|
186
|
+
!m.tick(time)
|
187
|
+
}
|
188
|
+
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.signal_name(n)
|
193
|
+
Signal.list.each_pair {|k,v|
|
194
|
+
return "SIG#{k}" if n == v
|
195
|
+
}
|
196
|
+
return n
|
197
|
+
end
|
198
|
+
|
199
|
+
def self.format_join_status(code)
|
200
|
+
case code
|
201
|
+
when Process::Status
|
202
|
+
if code.signaled?
|
203
|
+
"signal #{signal_name(code.termsig)}"
|
204
|
+
else
|
205
|
+
"status #{code.exitstatus}"
|
206
|
+
end
|
207
|
+
when Exception
|
208
|
+
"exception #{code}"
|
209
|
+
when nil
|
210
|
+
"unknown reason"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class AlreadyClosedError < EOFError
|
215
|
+
end
|
216
|
+
|
217
|
+
HEARTBEAT_MESSAGE = [0].pack('C')
|
218
|
+
|
219
|
+
class Monitor
|
220
|
+
def initialize(pm, pid)
|
221
|
+
@pm = pm
|
222
|
+
@pid = pid
|
223
|
+
|
224
|
+
@error = false
|
225
|
+
@last_heartbeat_time = Time.now
|
226
|
+
@next_kill_time = nil
|
227
|
+
@graceful_kill_start_time = nil
|
228
|
+
@immediate_kill_start_time = nil
|
229
|
+
@kill_count = 0
|
230
|
+
end
|
231
|
+
|
232
|
+
attr_accessor :last_heartbeat_time
|
233
|
+
|
234
|
+
def heartbeat_delay
|
235
|
+
now = Time.now
|
236
|
+
now - @last_heartbeat_time
|
237
|
+
end
|
238
|
+
|
239
|
+
def send_signal(sig)
|
240
|
+
pid = @pid
|
241
|
+
return nil unless pid
|
242
|
+
|
243
|
+
begin
|
244
|
+
Process.kill(sig, pid)
|
245
|
+
return true
|
246
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
247
|
+
return false
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def try_join
|
252
|
+
pid = @pid
|
253
|
+
return true unless pid
|
254
|
+
|
255
|
+
begin
|
256
|
+
pid, status = Process.waitpid2(pid, Process::WNOHANG)
|
257
|
+
code = status
|
258
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
259
|
+
# assume that any errors mean the child process is dead
|
260
|
+
code = $!
|
261
|
+
end
|
262
|
+
|
263
|
+
if code
|
264
|
+
@pid = nil
|
265
|
+
return code
|
266
|
+
end
|
267
|
+
|
268
|
+
return false
|
269
|
+
end
|
270
|
+
|
271
|
+
def join
|
272
|
+
pid = @pid
|
273
|
+
return nil unless pid
|
274
|
+
|
275
|
+
begin
|
276
|
+
pid, status = Process.waitpid2(pid)
|
277
|
+
code = status
|
278
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
279
|
+
# assume that any errors mean the child process is dead
|
280
|
+
code = $!
|
281
|
+
end
|
282
|
+
@pid = nil
|
283
|
+
|
284
|
+
return code
|
285
|
+
end
|
286
|
+
|
287
|
+
def start_graceful_stop!
|
288
|
+
now = Time.now
|
289
|
+
@next_kill_time ||= now
|
290
|
+
@graceful_kill_start_time ||= now
|
291
|
+
end
|
292
|
+
|
293
|
+
def start_immediate_stop!
|
294
|
+
now = Time.now
|
295
|
+
@next_kill_time ||= now
|
296
|
+
@immediate_kill_start_time ||= now
|
297
|
+
end
|
298
|
+
|
299
|
+
def tick(now=Time.now)
|
300
|
+
pid = @pid
|
301
|
+
return false unless pid
|
302
|
+
|
303
|
+
if !@immediate_kill_start_time
|
304
|
+
# check escalation
|
305
|
+
if heartbeat_delay >= @pm.heartbeat_timeout ||
|
306
|
+
(@graceful_kill_start_time && @pm.graceful_kill_timeout > 0 &&
|
307
|
+
@graceful_kill_start_time < now - @pm.graceful_kill_timeout)
|
308
|
+
# escalate to immediate kill
|
309
|
+
@kill_count = 0
|
310
|
+
@immediate_kill_start_time = now
|
311
|
+
@next_kill_time = now
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
if !@next_kill_time || @next_kill_time > now
|
316
|
+
# expect next tick
|
317
|
+
return true
|
318
|
+
end
|
319
|
+
|
320
|
+
# send signal now
|
321
|
+
|
322
|
+
if @immediate_kill_start_time
|
323
|
+
interval = @pm.immediate_kill_interval
|
324
|
+
interval_incr = @pm.immediate_kill_interval_increment
|
325
|
+
if @immediate_kill_start_time <= now - @pm.immediate_kill_timeout
|
326
|
+
# escalate to SIGKILL
|
327
|
+
signal = :KILL
|
328
|
+
else
|
329
|
+
signal = @pm.immediate_kill_signal
|
330
|
+
end
|
331
|
+
|
332
|
+
else
|
333
|
+
signal = @pm.graceful_kill_signal
|
334
|
+
interval = @pm.graceful_kill_interval
|
335
|
+
interval_incr = @pm.graceful_kill_interval_increment
|
336
|
+
end
|
337
|
+
|
338
|
+
begin
|
339
|
+
Process.kill(signal, pid)
|
340
|
+
rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
|
341
|
+
# assume that any errors mean the child process is dead
|
342
|
+
@pid = nil
|
343
|
+
return false
|
344
|
+
end
|
345
|
+
|
346
|
+
@next_kill_time = now + interval + interval_incr * @kill_count
|
347
|
+
@kill_count += 1
|
348
|
+
|
349
|
+
# expect next tick
|
350
|
+
return true
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
class TickThread < Thread
|
355
|
+
def initialize(pm)
|
356
|
+
@pm = pm
|
357
|
+
super(&method(:main))
|
358
|
+
end
|
359
|
+
|
360
|
+
private
|
361
|
+
|
362
|
+
def main
|
363
|
+
while true
|
364
|
+
@pm.tick(@pm.tick_interval)
|
365
|
+
end
|
366
|
+
nil
|
367
|
+
rescue AlreadyClosedError
|
368
|
+
nil
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
class Target
|
373
|
+
def initialize(pipe)
|
374
|
+
@pipe = pipe
|
375
|
+
end
|
376
|
+
|
377
|
+
attr_reader :pipe
|
378
|
+
|
379
|
+
def heartbeat!
|
380
|
+
@pipe.write HEARTBEAT_MESSAGE
|
381
|
+
end
|
382
|
+
|
383
|
+
def close
|
384
|
+
if @pipe
|
385
|
+
@pipe.close rescue nil
|
386
|
+
@pipe = nil
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
class HeartbeatThread < Thread
|
392
|
+
def initialize(pm, target, error_proc)
|
393
|
+
@pm = pm
|
394
|
+
@target = target
|
395
|
+
@error_proc = error_proc
|
396
|
+
super(&method(:main))
|
397
|
+
end
|
398
|
+
|
399
|
+
private
|
400
|
+
|
401
|
+
def main
|
402
|
+
while true
|
403
|
+
sleep @pm.heartbeat_interval
|
404
|
+
@target.heartbeat!
|
405
|
+
end
|
406
|
+
nil
|
407
|
+
rescue
|
408
|
+
@error_proc.call(self)
|
409
|
+
nil
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
end
|
414
|
+
|
415
|
+
end
|