mojombo-god 0.7.7
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/History.txt +255 -0
- data/Manifest.txt +107 -0
- data/README.txt +59 -0
- data/Rakefile +35 -0
- data/bin/god +127 -0
- data/examples/events.god +84 -0
- data/examples/gravatar.god +54 -0
- data/examples/single.god +66 -0
- data/ext/god/extconf.rb +55 -0
- data/ext/god/kqueue_handler.c +123 -0
- data/ext/god/netlink_handler.c +167 -0
- data/init/god +42 -0
- data/lib/god.rb +644 -0
- data/lib/god/behavior.rb +52 -0
- data/lib/god/behaviors/clean_pid_file.rb +21 -0
- data/lib/god/behaviors/clean_unix_socket.rb +21 -0
- data/lib/god/behaviors/notify_when_flapping.rb +51 -0
- data/lib/god/cli/command.rb +206 -0
- data/lib/god/cli/run.rb +177 -0
- data/lib/god/cli/version.rb +23 -0
- data/lib/god/condition.rb +96 -0
- data/lib/god/conditions/always.rb +23 -0
- data/lib/god/conditions/complex.rb +86 -0
- data/lib/god/conditions/cpu_usage.rb +80 -0
- data/lib/god/conditions/degrading_lambda.rb +52 -0
- data/lib/god/conditions/disk_usage.rb +27 -0
- data/lib/god/conditions/flapping.rb +128 -0
- data/lib/god/conditions/http_response_code.rb +168 -0
- data/lib/god/conditions/lambda.rb +25 -0
- data/lib/god/conditions/memory_usage.rb +82 -0
- data/lib/god/conditions/process_exits.rb +72 -0
- data/lib/god/conditions/process_running.rb +74 -0
- data/lib/god/conditions/tries.rb +44 -0
- data/lib/god/configurable.rb +57 -0
- data/lib/god/contact.rb +106 -0
- data/lib/god/contacts/email.rb +95 -0
- data/lib/god/dependency_graph.rb +41 -0
- data/lib/god/diagnostics.rb +37 -0
- data/lib/god/driver.rb +108 -0
- data/lib/god/errors.rb +24 -0
- data/lib/god/event_handler.rb +111 -0
- data/lib/god/event_handlers/dummy_handler.rb +13 -0
- data/lib/god/event_handlers/kqueue_handler.rb +17 -0
- data/lib/god/event_handlers/netlink_handler.rb +13 -0
- data/lib/god/logger.rb +120 -0
- data/lib/god/metric.rb +59 -0
- data/lib/god/process.rb +325 -0
- data/lib/god/registry.rb +32 -0
- data/lib/god/simple_logger.rb +53 -0
- data/lib/god/socket.rb +96 -0
- data/lib/god/sugar.rb +47 -0
- data/lib/god/system/portable_poller.rb +42 -0
- data/lib/god/system/process.rb +42 -0
- data/lib/god/system/slash_proc_poller.rb +82 -0
- data/lib/god/task.rb +487 -0
- data/lib/god/timeline.rb +25 -0
- data/lib/god/trigger.rb +43 -0
- data/lib/god/watch.rb +183 -0
- data/test/configs/child_events/child_events.god +44 -0
- data/test/configs/child_events/simple_server.rb +3 -0
- data/test/configs/child_polls/child_polls.god +37 -0
- data/test/configs/child_polls/simple_server.rb +12 -0
- data/test/configs/complex/complex.god +59 -0
- data/test/configs/complex/simple_server.rb +3 -0
- data/test/configs/contact/contact.god +74 -0
- data/test/configs/contact/simple_server.rb +3 -0
- data/test/configs/daemon_events/daemon_events.god +37 -0
- data/test/configs/daemon_events/simple_server.rb +8 -0
- data/test/configs/daemon_events/simple_server_stop.rb +11 -0
- data/test/configs/daemon_polls/daemon_polls.god +17 -0
- data/test/configs/daemon_polls/simple_server.rb +6 -0
- data/test/configs/degrading_lambda/degrading_lambda.god +31 -0
- data/test/configs/degrading_lambda/tcp_server.rb +15 -0
- data/test/configs/matias/matias.god +50 -0
- data/test/configs/real.rb +59 -0
- data/test/configs/running_load/running_load.god +16 -0
- data/test/configs/stress/simple_server.rb +3 -0
- data/test/configs/stress/stress.god +15 -0
- data/test/configs/task/logs/.placeholder +0 -0
- data/test/configs/task/task.god +26 -0
- data/test/configs/test.rb +61 -0
- data/test/helper.rb +151 -0
- data/test/suite.rb +6 -0
- data/test/test_behavior.rb +21 -0
- data/test/test_condition.rb +50 -0
- data/test/test_conditions_disk_usage.rb +56 -0
- data/test/test_conditions_http_response_code.rb +109 -0
- data/test/test_conditions_process_running.rb +44 -0
- data/test/test_conditions_tries.rb +67 -0
- data/test/test_contact.rb +109 -0
- data/test/test_dependency_graph.rb +62 -0
- data/test/test_driver.rb +11 -0
- data/test/test_event_handler.rb +80 -0
- data/test/test_god.rb +598 -0
- data/test/test_handlers_kqueue_handler.rb +16 -0
- data/test/test_logger.rb +63 -0
- data/test/test_metric.rb +72 -0
- data/test/test_process.rb +246 -0
- data/test/test_registry.rb +15 -0
- data/test/test_socket.rb +42 -0
- data/test/test_sugar.rb +42 -0
- data/test/test_system_portable_poller.rb +17 -0
- data/test/test_system_process.rb +30 -0
- data/test/test_task.rb +262 -0
- data/test/test_timeline.rb +37 -0
- data/test/test_trigger.rb +59 -0
- data/test/test_watch.rb +279 -0
- metadata +186 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module God
|
4
|
+
module Conditions
|
5
|
+
|
6
|
+
# Condition Symbol :http_response_code
|
7
|
+
# Type: Poll
|
8
|
+
#
|
9
|
+
# Trigger based on the response from an HTTP request.
|
10
|
+
#
|
11
|
+
# Paramaters
|
12
|
+
# Required
|
13
|
+
# +host+ is the hostname to connect [required]
|
14
|
+
# --one of code_is or code_is_not--
|
15
|
+
# +code_is+ trigger if the response code IS one of these
|
16
|
+
# e.g. 500 or '500' or [404, 500] or %w{404 500}
|
17
|
+
# +code_is_not+ trigger if the response code IS NOT one of these
|
18
|
+
# e.g. 200 or '200' or [200, 302] or %w{200 302}
|
19
|
+
# Optional
|
20
|
+
# +port+ is the port to connect (default 80)
|
21
|
+
# +path+ is the path to connect (default '/')
|
22
|
+
# +headers+ is the hash of HTTP headers to send (default none)
|
23
|
+
# +times+ is the number of times after which to trigger (default 1)
|
24
|
+
# e.g. 3 (times in a row) or [3, 5] (three out of fives times)
|
25
|
+
# +timeout+ is the time to wait for a connection (default 60.seconds)
|
26
|
+
#
|
27
|
+
# Examples
|
28
|
+
#
|
29
|
+
# Trigger if the response code from www.example.com/foo/bar
|
30
|
+
# is not a 200 (or if the connection is refused or times out:
|
31
|
+
#
|
32
|
+
# on.condition(:http_response_code) do |c|
|
33
|
+
# c.host = 'www.example.com'
|
34
|
+
# c.path = '/foo/bar'
|
35
|
+
# c.code_is_not = 200
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# Trigger if the response code is a 404 or a 500 (will not
|
39
|
+
# be triggered by a connection refusal or timeout):
|
40
|
+
#
|
41
|
+
# on.condition(:http_response_code) do |c|
|
42
|
+
# c.host = 'www.example.com'
|
43
|
+
# c.path = '/foo/bar'
|
44
|
+
# c.code_is = [404, 500]
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# Trigger if the response code is not a 200 five times in a row:
|
48
|
+
#
|
49
|
+
# on.condition(:http_response_code) do |c|
|
50
|
+
# c.host = 'www.example.com'
|
51
|
+
# c.path = '/foo/bar'
|
52
|
+
# c.code_is_not = 200
|
53
|
+
# c.times = 5
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Trigger if the response code is not a 200 or does not respond
|
57
|
+
# within 10 seconds:
|
58
|
+
#
|
59
|
+
# on.condition(:http_response_code) do |c|
|
60
|
+
# c.host = 'www.example.com'
|
61
|
+
# c.path = '/foo/bar'
|
62
|
+
# c.code_is_not = 200
|
63
|
+
# c.timeout = 10
|
64
|
+
# end
|
65
|
+
class HttpResponseCode < PollCondition
|
66
|
+
attr_accessor :code_is, # e.g. 500 or '500' or [404, 500] or %w{404 500}
|
67
|
+
:code_is_not, # e.g. 200 or '200' or [200, 302] or %w{200 302}
|
68
|
+
:times, # e.g. 3 or [3, 5]
|
69
|
+
:host, # e.g. www.example.com
|
70
|
+
:port, # e.g. 8080
|
71
|
+
:timeout, # e.g. 60.seconds
|
72
|
+
:path, # e.g. '/'
|
73
|
+
:headers # e.g. {'Host' => 'myvirtual.mydomain.com'}
|
74
|
+
|
75
|
+
def initialize
|
76
|
+
super
|
77
|
+
self.port = 80
|
78
|
+
self.path = '/'
|
79
|
+
self.headers = {}
|
80
|
+
self.times = [1, 1]
|
81
|
+
self.timeout = 60.seconds
|
82
|
+
end
|
83
|
+
|
84
|
+
def prepare
|
85
|
+
self.code_is = Array(self.code_is).map { |x| x.to_i } if self.code_is
|
86
|
+
self.code_is_not = Array(self.code_is_not).map { |x| x.to_i } if self.code_is_not
|
87
|
+
|
88
|
+
if self.times.kind_of?(Integer)
|
89
|
+
self.times = [self.times, self.times]
|
90
|
+
end
|
91
|
+
|
92
|
+
@timeline = Timeline.new(self.times[1])
|
93
|
+
@history = Timeline.new(self.times[1])
|
94
|
+
end
|
95
|
+
|
96
|
+
def reset
|
97
|
+
@timeline.clear
|
98
|
+
@history.clear
|
99
|
+
end
|
100
|
+
|
101
|
+
def valid?
|
102
|
+
valid = true
|
103
|
+
valid &= complain("Attribute 'host' must be specified", self) if self.host.nil?
|
104
|
+
valid &= complain("One (and only one) of attributes 'code_is' and 'code_is_not' must be specified", self) if
|
105
|
+
(self.code_is.nil? && self.code_is_not.nil?) || (self.code_is && self.code_is_not)
|
106
|
+
valid
|
107
|
+
end
|
108
|
+
|
109
|
+
def test
|
110
|
+
response = nil
|
111
|
+
|
112
|
+
Net::HTTP.start(self.host, self.port) do |http|
|
113
|
+
http.read_timeout = self.timeout
|
114
|
+
response = http.get(self.path, self.headers)
|
115
|
+
end
|
116
|
+
|
117
|
+
actual_response_code = response.code.to_i
|
118
|
+
if self.code_is && self.code_is.include?(actual_response_code)
|
119
|
+
pass(actual_response_code)
|
120
|
+
elsif self.code_is_not && !self.code_is_not.include?(actual_response_code)
|
121
|
+
pass(actual_response_code)
|
122
|
+
else
|
123
|
+
fail(actual_response_code)
|
124
|
+
end
|
125
|
+
rescue Errno::ECONNREFUSED
|
126
|
+
self.code_is ? fail('Refused') : pass('Refused')
|
127
|
+
rescue Errno::ECONNRESET
|
128
|
+
self.code_is ? fail('Reset') : pass('Reset')
|
129
|
+
rescue EOFError
|
130
|
+
self.code_is ? fail('EOF') : pass('EOF')
|
131
|
+
rescue Timeout::Error
|
132
|
+
self.code_is ? fail('Timeout') : pass('Timeout')
|
133
|
+
rescue Errno::ETIMEDOUT
|
134
|
+
self.code_is ? fail('Timedout') : pass('Timedout')
|
135
|
+
rescue Exception => failure
|
136
|
+
self.code_is ? fail(failure.class.name) : pass(failure.class.name)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def pass(code)
|
142
|
+
@timeline << true
|
143
|
+
if @timeline.select { |x| x }.size >= self.times.first
|
144
|
+
self.info = "http response abnormal #{history(code, true)}"
|
145
|
+
true
|
146
|
+
else
|
147
|
+
self.info = "http response nominal #{history(code, true)}"
|
148
|
+
false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def fail(code)
|
153
|
+
@timeline << false
|
154
|
+
self.info = "http response nominal #{history(code, false)}"
|
155
|
+
false
|
156
|
+
end
|
157
|
+
|
158
|
+
def history(code, passed)
|
159
|
+
entry = code.to_s.dup
|
160
|
+
entry = '*' + entry if passed
|
161
|
+
@history << entry
|
162
|
+
'[' + @history.join(", ") + ']'
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
class Lambda < PollCondition
|
5
|
+
attr_accessor :lambda
|
6
|
+
|
7
|
+
def valid?
|
8
|
+
valid = true
|
9
|
+
valid &= complain("Attribute 'lambda' must be specified", self) if self.lambda.nil?
|
10
|
+
valid
|
11
|
+
end
|
12
|
+
|
13
|
+
def test
|
14
|
+
if self.lambda.call()
|
15
|
+
self.info = "lambda condition was satisfied"
|
16
|
+
true
|
17
|
+
else
|
18
|
+
self.info = "lambda condition was not satisfied"
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
# Condition Symbol :memory_usage
|
5
|
+
# Type: Poll
|
6
|
+
#
|
7
|
+
# Trigger when the resident memory of a process is above a specified limit.
|
8
|
+
#
|
9
|
+
# Paramaters
|
10
|
+
# Required
|
11
|
+
# +pid_file+ is the pid file of the process in question. Automatically
|
12
|
+
# populated for Watches.
|
13
|
+
# +above+ is the amount of resident memory (in kilobytes) above which
|
14
|
+
# the condition should trigger. You can also use the sugar
|
15
|
+
# methods #kilobytes, #megabytes, and #gigabytes to clarify
|
16
|
+
# this amount (see examples).
|
17
|
+
#
|
18
|
+
# Examples
|
19
|
+
#
|
20
|
+
# Trigger if the process is using more than 100 megabytes of resident
|
21
|
+
# memory (from a Watch):
|
22
|
+
#
|
23
|
+
# on.condition(:memory_usage) do |c|
|
24
|
+
# c.above = 100.megabytes
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# Non-Watch Tasks must specify a PID file:
|
28
|
+
#
|
29
|
+
# on.condition(:memory_usage) do |c|
|
30
|
+
# c.above = 100.megabytes
|
31
|
+
# c.pid_file = "/var/run/mongrel.3000.pid"
|
32
|
+
# end
|
33
|
+
class MemoryUsage < PollCondition
|
34
|
+
attr_accessor :above, :times, :pid_file
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
super
|
38
|
+
self.above = nil
|
39
|
+
self.times = [1, 1]
|
40
|
+
end
|
41
|
+
|
42
|
+
def prepare
|
43
|
+
if self.times.kind_of?(Integer)
|
44
|
+
self.times = [self.times, self.times]
|
45
|
+
end
|
46
|
+
|
47
|
+
@timeline = Timeline.new(self.times[1])
|
48
|
+
end
|
49
|
+
|
50
|
+
def reset
|
51
|
+
@timeline.clear
|
52
|
+
end
|
53
|
+
|
54
|
+
def pid
|
55
|
+
self.pid_file ? File.read(self.pid_file).strip.to_i : self.watch.pid
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid?
|
59
|
+
valid = true
|
60
|
+
valid &= complain("Attribute 'pid_file' must be specified", self) if self.pid_file.nil? && self.watch.pid_file.nil?
|
61
|
+
valid &= complain("Attribute 'above' must be specified", self) if self.above.nil?
|
62
|
+
valid
|
63
|
+
end
|
64
|
+
|
65
|
+
def test
|
66
|
+
process = System::Process.new(self.pid)
|
67
|
+
@timeline.push(process.memory)
|
68
|
+
|
69
|
+
history = "[" + @timeline.map { |x| "#{x > self.above ? '*' : ''}#{x}kb" }.join(", ") + "]"
|
70
|
+
|
71
|
+
if @timeline.select { |x| x > self.above }.size >= self.times.first
|
72
|
+
self.info = "memory out of bounds #{history}"
|
73
|
+
return true
|
74
|
+
else
|
75
|
+
self.info = "memory within bounds #{history}"
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
# Condition Symbol :process_exits
|
5
|
+
# Type: Event
|
6
|
+
#
|
7
|
+
# Trigger when a process exits.
|
8
|
+
#
|
9
|
+
# Paramaters
|
10
|
+
# Required
|
11
|
+
# +pid_file+ is the pid file of the process in question. Automatically
|
12
|
+
# populated for Watches.
|
13
|
+
#
|
14
|
+
# Examples
|
15
|
+
#
|
16
|
+
# Trigger if process exits (from a Watch):
|
17
|
+
#
|
18
|
+
# on.condition(:process_exits)
|
19
|
+
#
|
20
|
+
# Trigger if process exits:
|
21
|
+
#
|
22
|
+
# on.condition(:process_exits) do |c|
|
23
|
+
# c.pid_file = "/var/run/mongrel.3000.pid"
|
24
|
+
# end
|
25
|
+
class ProcessExits < EventCondition
|
26
|
+
attr_accessor :pid_file
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
self.info = "process exited"
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def pid
|
37
|
+
self.pid_file ? File.read(self.pid_file).strip.to_i : self.watch.pid
|
38
|
+
end
|
39
|
+
|
40
|
+
def register
|
41
|
+
pid = self.pid
|
42
|
+
|
43
|
+
begin
|
44
|
+
EventHandler.register(pid, :proc_exit) do |extra|
|
45
|
+
formatted_extra = extra.size > 0 ? " #{extra.inspect}" : ""
|
46
|
+
self.info = "process #{pid} exited#{formatted_extra}"
|
47
|
+
self.watch.trigger(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
msg = "#{self.watch.name} registered 'proc_exit' event for pid #{pid}"
|
51
|
+
applog(self.watch, :info, msg)
|
52
|
+
rescue StandardError
|
53
|
+
raise EventRegistrationFailedError.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def deregister
|
58
|
+
pid = self.pid
|
59
|
+
if pid
|
60
|
+
EventHandler.deregister(pid, :proc_exit)
|
61
|
+
|
62
|
+
msg = "#{self.watch.name} deregistered 'proc_exit' event for pid #{pid}"
|
63
|
+
applog(self.watch, :info, msg)
|
64
|
+
else
|
65
|
+
pid_file_location = self.pid_file || self.watch.pid_file
|
66
|
+
applog(self.watch, :error, "#{self.watch.name} could not deregister: no cached PID or PID file #{pid_file_location} (#{self.base_name})")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
# Condition Symbol :process_running
|
5
|
+
# Type: Poll
|
6
|
+
#
|
7
|
+
# Trigger when a process is running or not running depending on attributes.
|
8
|
+
#
|
9
|
+
# Paramaters
|
10
|
+
# Required
|
11
|
+
# +pid_file+ is the pid file of the process in question. Automatically
|
12
|
+
# populated for Watches.
|
13
|
+
# +running" specifies whether you want to trigger if the process is
|
14
|
+
# running (true) or whether it is not running (false)
|
15
|
+
#
|
16
|
+
# Examples
|
17
|
+
#
|
18
|
+
# Trigger if process IS NOT running (from a Watch):
|
19
|
+
#
|
20
|
+
# on.condition(:process_running) do |c|
|
21
|
+
# c.running = false
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Trigger if process IS running (from a Watch):
|
25
|
+
#
|
26
|
+
# on.condition(:process_running) do |c|
|
27
|
+
# c.running = true
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Non-Watch Tasks must specify a PID file:
|
31
|
+
#
|
32
|
+
# on.condition(:process_running) do |c|
|
33
|
+
# c.running = false
|
34
|
+
# c.pid_file = "/var/run/mongrel.3000.pid"
|
35
|
+
# end
|
36
|
+
class ProcessRunning < PollCondition
|
37
|
+
attr_accessor :running, :pid_file
|
38
|
+
|
39
|
+
def pid
|
40
|
+
self.pid_file ? File.read(self.pid_file).strip.to_i : self.watch.pid
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid?
|
44
|
+
valid = true
|
45
|
+
valid &= complain("Attribute 'pid_file' must be specified", self) if self.pid_file.nil? && self.watch.pid_file.nil?
|
46
|
+
valid &= complain("Attribute 'running' must be specified", self) if self.running.nil?
|
47
|
+
valid
|
48
|
+
end
|
49
|
+
|
50
|
+
def test
|
51
|
+
self.info = []
|
52
|
+
|
53
|
+
pid = self.pid
|
54
|
+
active = pid && System::Process.new(pid).exists?
|
55
|
+
|
56
|
+
if (self.running && active)
|
57
|
+
self.info.concat(["process is running"])
|
58
|
+
true
|
59
|
+
elsif (!self.running && !active)
|
60
|
+
self.info.concat(["process is not running"])
|
61
|
+
true
|
62
|
+
else
|
63
|
+
if self.running
|
64
|
+
self.info.concat(["process is not running"])
|
65
|
+
else
|
66
|
+
self.info.concat(["process is running"])
|
67
|
+
end
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
class Tries < PollCondition
|
5
|
+
attr_accessor :times, :within
|
6
|
+
|
7
|
+
def prepare
|
8
|
+
@timeline = Timeline.new(self.times)
|
9
|
+
end
|
10
|
+
|
11
|
+
def reset
|
12
|
+
@timeline.clear
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?
|
16
|
+
valid = true
|
17
|
+
valid &= complain("Attribute 'times' must be specified", self) if self.times.nil?
|
18
|
+
valid
|
19
|
+
end
|
20
|
+
|
21
|
+
def test
|
22
|
+
@timeline << Time.now
|
23
|
+
|
24
|
+
concensus = (@timeline.size == self.times)
|
25
|
+
duration = self.within.nil? || (@timeline.last - @timeline.first) < self.within
|
26
|
+
|
27
|
+
if within
|
28
|
+
history = "[#{@timeline.size}/#{self.times} within #{(@timeline.last - @timeline.first).to_i}s]"
|
29
|
+
else
|
30
|
+
history = "[#{@timeline.size}/#{self.times}]"
|
31
|
+
end
|
32
|
+
|
33
|
+
if concensus && duration
|
34
|
+
self.info = "tries exceeded #{history}"
|
35
|
+
return true
|
36
|
+
else
|
37
|
+
self.info = "tries within bounds #{history}"
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|