ace-eye 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +38 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CHANGES.md +77 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +212 -0
- data/Rakefile +35 -0
- data/bin/eye +5 -0
- data/bin/loader_eye +72 -0
- data/bin/runner +16 -0
- data/examples/dependency.eye +17 -0
- data/examples/notify.eye +19 -0
- data/examples/plugin/README.md +15 -0
- data/examples/plugin/main.eye +15 -0
- data/examples/plugin/plugin.rb +63 -0
- data/examples/process_thin.rb +29 -0
- data/examples/processes/em.rb +57 -0
- data/examples/processes/forking.rb +20 -0
- data/examples/processes/sample.rb +144 -0
- data/examples/processes/thin.ru +12 -0
- data/examples/puma.eye +29 -0
- data/examples/rbenv.eye +11 -0
- data/examples/sidekiq.eye +23 -0
- data/examples/test.eye +87 -0
- data/examples/thin-farm.eye +30 -0
- data/examples/unicorn.eye +39 -0
- data/eye.gemspec +40 -0
- data/lib/eye.rb +28 -0
- data/lib/eye/application.rb +73 -0
- data/lib/eye/checker.rb +258 -0
- data/lib/eye/checker/children_count.rb +44 -0
- data/lib/eye/checker/children_memory.rb +12 -0
- data/lib/eye/checker/cpu.rb +17 -0
- data/lib/eye/checker/cputime.rb +13 -0
- data/lib/eye/checker/file_ctime.rb +24 -0
- data/lib/eye/checker/file_size.rb +34 -0
- data/lib/eye/checker/file_touched.rb +15 -0
- data/lib/eye/checker/http.rb +96 -0
- data/lib/eye/checker/memory.rb +17 -0
- data/lib/eye/checker/nop.rb +6 -0
- data/lib/eye/checker/runtime.rb +18 -0
- data/lib/eye/checker/socket.rb +159 -0
- data/lib/eye/child_process.rb +101 -0
- data/lib/eye/cli.rb +185 -0
- data/lib/eye/cli/commands.rb +78 -0
- data/lib/eye/cli/render.rb +130 -0
- data/lib/eye/cli/server.rb +93 -0
- data/lib/eye/client.rb +32 -0
- data/lib/eye/config.rb +91 -0
- data/lib/eye/control.rb +2 -0
- data/lib/eye/controller.rb +54 -0
- data/lib/eye/controller/commands.rb +88 -0
- data/lib/eye/controller/helpers.rb +101 -0
- data/lib/eye/controller/load.rb +224 -0
- data/lib/eye/controller/options.rb +18 -0
- data/lib/eye/controller/send_command.rb +177 -0
- data/lib/eye/controller/status.rb +72 -0
- data/lib/eye/dsl.rb +53 -0
- data/lib/eye/dsl/application_opts.rb +39 -0
- data/lib/eye/dsl/chain.rb +12 -0
- data/lib/eye/dsl/child_process_opts.rb +13 -0
- data/lib/eye/dsl/config_opts.rb +55 -0
- data/lib/eye/dsl/group_opts.rb +32 -0
- data/lib/eye/dsl/helpers.rb +20 -0
- data/lib/eye/dsl/main.rb +51 -0
- data/lib/eye/dsl/opts.rb +151 -0
- data/lib/eye/dsl/process_opts.rb +36 -0
- data/lib/eye/dsl/pure_opts.rb +121 -0
- data/lib/eye/dsl/validation.rb +88 -0
- data/lib/eye/group.rb +140 -0
- data/lib/eye/group/chain.rb +81 -0
- data/lib/eye/loader.rb +10 -0
- data/lib/eye/local.rb +100 -0
- data/lib/eye/logger.rb +104 -0
- data/lib/eye/notify.rb +118 -0
- data/lib/eye/notify/jabber.rb +30 -0
- data/lib/eye/notify/mail.rb +48 -0
- data/lib/eye/process.rb +85 -0
- data/lib/eye/process/children.rb +60 -0
- data/lib/eye/process/commands.rb +280 -0
- data/lib/eye/process/config.rb +81 -0
- data/lib/eye/process/controller.rb +73 -0
- data/lib/eye/process/data.rb +78 -0
- data/lib/eye/process/monitor.rb +108 -0
- data/lib/eye/process/notify.rb +32 -0
- data/lib/eye/process/scheduler.rb +82 -0
- data/lib/eye/process/states.rb +86 -0
- data/lib/eye/process/states_history.rb +66 -0
- data/lib/eye/process/system.rb +97 -0
- data/lib/eye/process/trigger.rb +34 -0
- data/lib/eye/process/validate.rb +33 -0
- data/lib/eye/process/watchers.rb +66 -0
- data/lib/eye/reason.rb +20 -0
- data/lib/eye/server.rb +60 -0
- data/lib/eye/sigar.rb +5 -0
- data/lib/eye/system.rb +139 -0
- data/lib/eye/system_resources.rb +99 -0
- data/lib/eye/trigger.rb +136 -0
- data/lib/eye/trigger/check_dependency.rb +30 -0
- data/lib/eye/trigger/flapping.rb +41 -0
- data/lib/eye/trigger/stop_children.rb +17 -0
- data/lib/eye/trigger/transition.rb +15 -0
- data/lib/eye/trigger/wait_dependency.rb +49 -0
- data/lib/eye/utils.rb +45 -0
- data/lib/eye/utils/alive_array.rb +57 -0
- data/lib/eye/utils/celluloid_chain.rb +71 -0
- data/lib/eye/utils/celluloid_klass.rb +5 -0
- data/lib/eye/utils/leak_19.rb +10 -0
- data/lib/eye/utils/mini_active_support.rb +111 -0
- data/lib/eye/utils/pmap.rb +7 -0
- data/lib/eye/utils/tail.rb +20 -0
- metadata +398 -0
data/lib/eye/trigger.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
class Eye::Trigger
|
2
|
+
include Eye::Dsl::Validation
|
3
|
+
|
4
|
+
autoload :Flapping, 'eye/trigger/flapping'
|
5
|
+
autoload :Transition, 'eye/trigger/transition'
|
6
|
+
autoload :StopChildren, 'eye/trigger/stop_children'
|
7
|
+
autoload :WaitDependency, 'eye/trigger/wait_dependency'
|
8
|
+
autoload :CheckDependency, 'eye/trigger/check_dependency'
|
9
|
+
|
10
|
+
TYPES = {:flapping => 'Flapping', :transition => 'Transition', :stop_children => 'StopChildren',
|
11
|
+
:wait_dependency => 'WaitDependency', :check_dependency => 'CheckDependency'
|
12
|
+
}
|
13
|
+
|
14
|
+
attr_reader :message, :options, :process
|
15
|
+
|
16
|
+
def self.name_and_class(type)
|
17
|
+
type = type.to_sym
|
18
|
+
return {:name => type, :type => type} if TYPES[type]
|
19
|
+
|
20
|
+
if type =~ /\A(.*?)_?[0-9]+\z/
|
21
|
+
ctype = $1.to_sym
|
22
|
+
return {:name => type, :type => ctype} if TYPES[ctype]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.get_class(type)
|
27
|
+
klass = eval("Eye::Trigger::#{TYPES[type]}") rescue nil
|
28
|
+
raise "unknown trigger #{type}" unless klass
|
29
|
+
if deps = klass.requires
|
30
|
+
Array(deps).each { |d| require d }
|
31
|
+
end
|
32
|
+
klass
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.create(process, options = {})
|
36
|
+
get_class(options[:type]).new(process, options)
|
37
|
+
|
38
|
+
rescue Exception, Timeout::Error => ex
|
39
|
+
log_ex(ex)
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.validate!(options = {})
|
44
|
+
get_class(options[:type]).validate(options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(process, options = {})
|
48
|
+
@options = options
|
49
|
+
@process = process
|
50
|
+
@full_name = @process.full_name if @process
|
51
|
+
|
52
|
+
debug "add #{options}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"<#{self.class} @process='#{@full_name}' @options=#{@options}>"
|
57
|
+
end
|
58
|
+
|
59
|
+
def logger_tag
|
60
|
+
@process.logger.prefix
|
61
|
+
end
|
62
|
+
|
63
|
+
def logger_sub_tag
|
64
|
+
"trigger(#{@options[:type]})"
|
65
|
+
end
|
66
|
+
|
67
|
+
def notify(transition, reason)
|
68
|
+
debug "check (:#{transition.event}) :#{transition.from} => :#{transition.to}"
|
69
|
+
@reason = reason
|
70
|
+
@transition = transition
|
71
|
+
|
72
|
+
check(transition) if filter_transition(transition)
|
73
|
+
|
74
|
+
rescue Exception, Timeout::Error => ex
|
75
|
+
if ex.class == Eye::Process::StateError
|
76
|
+
raise ex
|
77
|
+
else
|
78
|
+
log_ex(ex)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
param :to, [Symbol, Array]
|
83
|
+
param :from, [Symbol, Array]
|
84
|
+
param :event, [Symbol, Array]
|
85
|
+
|
86
|
+
def filter_transition(trans)
|
87
|
+
return true unless to || from || event
|
88
|
+
|
89
|
+
compare_state(trans.to_name, to) &&
|
90
|
+
compare_state(trans.from_name, from) &&
|
91
|
+
compare_state(trans.event, event)
|
92
|
+
end
|
93
|
+
|
94
|
+
def check(transition)
|
95
|
+
raise NotImplementedError
|
96
|
+
end
|
97
|
+
|
98
|
+
def run_in_process_context(p)
|
99
|
+
process.instance_exec(&p) if process.alive?
|
100
|
+
end
|
101
|
+
|
102
|
+
def defer(&block)
|
103
|
+
Celluloid::Future.new(&block).value
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.register(base)
|
107
|
+
name = base.to_s.gsub('Eye::Trigger::', '')
|
108
|
+
type = name.underscore.to_sym
|
109
|
+
Eye::Trigger::TYPES[type] = name
|
110
|
+
Eye::Trigger.const_set(name, base)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.requires
|
114
|
+
end
|
115
|
+
|
116
|
+
class Custom < Eye::Trigger
|
117
|
+
def self.inherited(base)
|
118
|
+
super
|
119
|
+
register(base)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def compare_state(state_name, condition)
|
126
|
+
case condition
|
127
|
+
when Symbol
|
128
|
+
state_name == condition
|
129
|
+
when Array
|
130
|
+
condition.include?(state_name)
|
131
|
+
else
|
132
|
+
true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Eye::Trigger::CheckDependency < Eye::Trigger
|
2
|
+
param :names, [Array], true, 5
|
3
|
+
|
4
|
+
def check(transition)
|
5
|
+
check_dependency(transition.to_name) if transition.from_name == :up
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def check_dependency(to)
|
11
|
+
processes = names.map do |name|
|
12
|
+
Eye::Control.find_nearest_process(name, process.group_name_pure, process.app_name)
|
13
|
+
end.compact
|
14
|
+
return if processes.empty?
|
15
|
+
processes = Eye::Utils::AliveArray.new(processes)
|
16
|
+
|
17
|
+
act = case to
|
18
|
+
when :down, :restarting; :restart
|
19
|
+
when :stopping; :stop
|
20
|
+
when :unmonitored; :unmonitor
|
21
|
+
end
|
22
|
+
|
23
|
+
if act
|
24
|
+
processes.each do |p|
|
25
|
+
p.schedule act, Eye::Reason.new(:"#{act} dependecies")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Eye::Trigger::Flapping < Eye::Trigger
|
2
|
+
|
3
|
+
# trigger :flapping, :times => 10, :within => 1.minute,
|
4
|
+
# :retry_in => 10.minutes, :retry_times => 15
|
5
|
+
|
6
|
+
param :times, [Fixnum], true, 5
|
7
|
+
param :within, [Float, Fixnum], true
|
8
|
+
param :retry_in, [Float, Fixnum]
|
9
|
+
param :retry_times, [Fixnum]
|
10
|
+
|
11
|
+
def check(transition)
|
12
|
+
on_flapping if transition.event == :crashed && !good?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def good?
|
18
|
+
states = process.states_history.states_for_period( within, @last_at )
|
19
|
+
down_count = states.count{|st| st == :down }
|
20
|
+
|
21
|
+
if down_count >= times
|
22
|
+
@last_at = process.states_history.last_state_changed_at
|
23
|
+
false
|
24
|
+
else
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_flapping
|
30
|
+
debug 'flapping recognized!!!'
|
31
|
+
|
32
|
+
process.notify :error, 'flapping!'
|
33
|
+
process.schedule :unmonitor, Eye::Reason.new(:flapping)
|
34
|
+
|
35
|
+
return unless retry_in
|
36
|
+
return if retry_times && process.flapping_times >= retry_times
|
37
|
+
|
38
|
+
process.schedule_in(retry_in.to_f, :retry_start_after_flapping)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Eye::Trigger::StopChildren < Eye::Trigger
|
2
|
+
|
3
|
+
# Kill process children when parent process crashed, or stopped:
|
4
|
+
#
|
5
|
+
# trigger :stop_children
|
6
|
+
|
7
|
+
param :timeout, [Fixnum, Float], nil, 60
|
8
|
+
|
9
|
+
# default on stopped, crashed
|
10
|
+
param_default :event, [:stopped, :crashed]
|
11
|
+
|
12
|
+
def check(trans)
|
13
|
+
debug 'stopping children'
|
14
|
+
process.children.pmap { |pid, c| c.stop }
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Eye::Trigger::Transition < Eye::Trigger
|
2
|
+
|
3
|
+
# trigger :transition, :to => :up, :from => :starting, :do => ->{ ... }
|
4
|
+
|
5
|
+
param :do, [Proc, Symbol]
|
6
|
+
|
7
|
+
def check(trans)
|
8
|
+
act = @options[:do]
|
9
|
+
if act
|
10
|
+
instance_exec(&@options[:do]) if act.is_a?(Proc)
|
11
|
+
send(act, process) if act.is_a?(Symbol)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Eye::Trigger::WaitDependency < Eye::Trigger
|
2
|
+
param :names, [Array], true
|
3
|
+
param :wait_timeout, [Numeric], nil, 15.seconds
|
4
|
+
param :retry_after, [Numeric], nil, 1.minute
|
5
|
+
param :should_start, [TrueClass, FalseClass]
|
6
|
+
|
7
|
+
def check(transition)
|
8
|
+
wait_dependency if transition.to_name == :starting
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def wait_dependency
|
14
|
+
processes = names.map do |name|
|
15
|
+
Eye::Control.find_nearest_process(name, process.group_name_pure, process.app_name)
|
16
|
+
end.compact
|
17
|
+
return if processes.empty?
|
18
|
+
processes = Eye::Utils::AliveArray.new(processes)
|
19
|
+
|
20
|
+
processes.each do |p|
|
21
|
+
if p.state_name != :up && (should_start == nil || should_start)
|
22
|
+
p.schedule :start, Eye::Reason.new(:start_dependency)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
res = true
|
27
|
+
|
28
|
+
processes.pmap do |p|
|
29
|
+
name = p.name
|
30
|
+
|
31
|
+
res &= process.wait_for_condition(wait_timeout, 0.5) do
|
32
|
+
info "wait for #{name} until it :up"
|
33
|
+
p.state_name == :up
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
unless res
|
38
|
+
warn "not waited for #{names} to be up"
|
39
|
+
process.switch :unmonitoring
|
40
|
+
|
41
|
+
if retry_after
|
42
|
+
process.schedule_in retry_after, :start, Eye::Reason.new(:wait_dependency)
|
43
|
+
end
|
44
|
+
|
45
|
+
raise Eye::Process::StateError.new('stop transition because dependency is not up')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/lib/eye/utils.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Eye::Utils
|
4
|
+
autoload :Tail, 'eye/utils/tail'
|
5
|
+
autoload :AliveArray, 'eye/utils/alive_array'
|
6
|
+
autoload :CelluloidChain, 'eye/utils/celluloid_chain'
|
7
|
+
|
8
|
+
def self.deep_clone(value)
|
9
|
+
case
|
10
|
+
when value.is_a?(Array) then value.map{|v| deep_clone(v) }
|
11
|
+
when value.is_a?(Hash) then value.inject({}){|r, (k, v)| r[ deep_clone(k) ] = deep_clone(v); r }
|
12
|
+
else value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# deep merging b into a (a deeply changed)
|
17
|
+
def self.deep_merge!(a, b, allowed_keys = nil)
|
18
|
+
b.each do |k, v|
|
19
|
+
next if allowed_keys && !allowed_keys.include?(k)
|
20
|
+
if a[k].is_a?(Hash) && v.is_a?(Hash)
|
21
|
+
deep_merge!(a[k], v)
|
22
|
+
else
|
23
|
+
a[k] = v
|
24
|
+
end
|
25
|
+
end
|
26
|
+
a
|
27
|
+
end
|
28
|
+
|
29
|
+
D1 = '%H:%M'
|
30
|
+
D2 = '%b%d'
|
31
|
+
|
32
|
+
def self.human_time(unix_time)
|
33
|
+
time = Time.at(unix_time.to_i)
|
34
|
+
d1 = time.to_date
|
35
|
+
d2 = Time.now.to_date
|
36
|
+
time.strftime (d1 == d2) ? D1 : D2
|
37
|
+
end
|
38
|
+
|
39
|
+
DF = '%d %b %H:%M'
|
40
|
+
|
41
|
+
def self.human_time2(unix_time)
|
42
|
+
Time.at(unix_time.to_i).strftime(DF)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Eye::Utils::AliveArray
|
2
|
+
extend Forwardable
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def_delegators :@arr, :[], :<<, :clear, :delete, :size, :empty?, :push,
|
6
|
+
:flatten, :present?, :uniq!, :select!
|
7
|
+
|
8
|
+
def initialize(arr = [])
|
9
|
+
@arr = arr
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
@arr.each{|elem| elem && elem.alive? && block[elem] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
map{|x| x }
|
18
|
+
end
|
19
|
+
|
20
|
+
def full_size
|
21
|
+
@arr.size
|
22
|
+
end
|
23
|
+
|
24
|
+
def pure
|
25
|
+
@arr
|
26
|
+
end
|
27
|
+
|
28
|
+
def sort_by(&block)
|
29
|
+
self.class.new super
|
30
|
+
end
|
31
|
+
|
32
|
+
def sort(&block)
|
33
|
+
self.class.new super
|
34
|
+
end
|
35
|
+
|
36
|
+
def +(other)
|
37
|
+
if other.is_a?(Eye::Utils::AliveArray)
|
38
|
+
@arr += other.pure
|
39
|
+
elsif other.is_a?(Array)
|
40
|
+
@arr += other
|
41
|
+
else
|
42
|
+
raise "Unexpected + #{other}"
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
if other.is_a?(Eye::Utils::AliveArray)
|
49
|
+
@arr == other.pure
|
50
|
+
elsif other.is_a?(Array)
|
51
|
+
@arr == other
|
52
|
+
else
|
53
|
+
raise "Unexpected == #{other}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
|
3
|
+
class Eye::Utils::CelluloidChain
|
4
|
+
include Celluloid
|
5
|
+
|
6
|
+
def initialize(target)
|
7
|
+
@target = target
|
8
|
+
@calls = []
|
9
|
+
@running = false
|
10
|
+
@target_class = @target.class
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(method_name, *args, &block)
|
14
|
+
@calls << {:method_name => method_name, :args => args, :block => block}
|
15
|
+
ensure_process
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_wo_dups(method_name, *args, &block)
|
19
|
+
h = {:method_name => method_name, :args => args, :block => block}
|
20
|
+
if @calls[-1] != h
|
21
|
+
@calls << h
|
22
|
+
ensure_process
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_wo_dups_current(method_name, *args, &block)
|
27
|
+
h = {:method_name => method_name, :args => args, :block => block}
|
28
|
+
if !@calls.include?(h) && @call != h
|
29
|
+
@calls << h
|
30
|
+
ensure_process
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def list
|
35
|
+
@calls
|
36
|
+
end
|
37
|
+
|
38
|
+
def names_list
|
39
|
+
list.map{|el| el[:method_name].to_sym }
|
40
|
+
end
|
41
|
+
|
42
|
+
def clear
|
43
|
+
@calls = []
|
44
|
+
end
|
45
|
+
|
46
|
+
alias :clear_pending_list :clear
|
47
|
+
|
48
|
+
# need, because of https://github.com/celluloid/celluloid/issues/22
|
49
|
+
def inspect
|
50
|
+
"Celluloid::Chain(#{@target_class}: #{@calls.size})"
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :running
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def ensure_process
|
58
|
+
unless @running
|
59
|
+
@running = true
|
60
|
+
async.process
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def process
|
65
|
+
while @call = @calls.shift
|
66
|
+
@running = true
|
67
|
+
@target.send(@call[:method_name], *@call[:args], &@call[:block]) if @target.alive?
|
68
|
+
end
|
69
|
+
@running = false
|
70
|
+
end
|
71
|
+
end
|