aeternitas 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +289 -0
- data/Rakefile +6 -0
- data/aeternitas.gemspec +44 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/aeternitas.rb +80 -0
- data/lib/aeternitas/errors.rb +48 -0
- data/lib/aeternitas/guard.rb +199 -0
- data/lib/aeternitas/metrics.rb +162 -0
- data/lib/aeternitas/metrics/counter.rb +18 -0
- data/lib/aeternitas/metrics/ratio.rb +67 -0
- data/lib/aeternitas/metrics/ten_minutes_resolution.rb +40 -0
- data/lib/aeternitas/metrics/values.rb +18 -0
- data/lib/aeternitas/pollable.rb +197 -0
- data/lib/aeternitas/pollable/configuration.rb +64 -0
- data/lib/aeternitas/pollable/dsl.rb +124 -0
- data/lib/aeternitas/pollable_meta_data.rb +73 -0
- data/lib/aeternitas/polling_frequency.rb +24 -0
- data/lib/aeternitas/sidekiq.rb +5 -0
- data/lib/aeternitas/sidekiq/middleware.rb +31 -0
- data/lib/aeternitas/sidekiq/poll_job.rb +30 -0
- data/lib/aeternitas/source.rb +62 -0
- data/lib/aeternitas/storage_adapter.rb +46 -0
- data/lib/aeternitas/storage_adapter/file.rb +73 -0
- data/lib/aeternitas/version.rb +3 -0
- data/lib/generators/aeternitas/install_generator.rb +35 -0
- data/lib/generators/aeternitas/templates/add_aeternitas.rb.erb +25 -0
- data/lib/generators/aeternitas/templates/initializer.rb +10 -0
- data/logo.png +0 -0
- data/logo.svg +198 -0
- metadata +289 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "aeternitas"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/aeternitas.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
require "redis"
|
3
|
+
require "connection_pool"
|
4
|
+
require "sidekiq-unique-jobs"
|
5
|
+
require "tabs_tabs"
|
6
|
+
require "aeternitas/version"
|
7
|
+
require "aeternitas/guard"
|
8
|
+
require "aeternitas/pollable"
|
9
|
+
require "aeternitas/pollable_meta_data"
|
10
|
+
require "aeternitas/source"
|
11
|
+
require "aeternitas/polling_frequency"
|
12
|
+
require "aeternitas/errors"
|
13
|
+
require "aeternitas/storage_adapter"
|
14
|
+
require "aeternitas/sidekiq"
|
15
|
+
require "aeternitas/metrics"
|
16
|
+
|
17
|
+
# Aeternitas
|
18
|
+
module Aeternitas
|
19
|
+
|
20
|
+
# Get the configured redis connection
|
21
|
+
# @return [ConnectionPool::Wrapper] returns a redis connection from the pool
|
22
|
+
def self.redis
|
23
|
+
@redis ||= ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.new(self.config.redis) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Access the configuration
|
27
|
+
# @return [Aeternitas::Configuration] the Aeternitas configuration
|
28
|
+
def self.config
|
29
|
+
@config ||= Configuration.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Configure Aeternitas
|
33
|
+
# @see Aeternitas::Configuration
|
34
|
+
# @yieldparam [Aeternitas::Configuration] config the aeternitas configuration
|
35
|
+
def self.configure
|
36
|
+
yield(self.config)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Enqueues all active pollables for which next polling is lower than the current time
|
40
|
+
def self.enqueue_due_pollables
|
41
|
+
Aeternitas::PollableMetaData.due.find_each do |pollable_meta_data|
|
42
|
+
Aeternitas::Sidekiq::PollJob
|
43
|
+
.set(queue: pollable_meta_data.pollable.pollable_configuration.queue)
|
44
|
+
.perform_async(pollable_meta_data.id)
|
45
|
+
pollable_meta_data.enqueue
|
46
|
+
pollable_meta_data.save
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Stores the global Aeternitas configuration
|
51
|
+
# @!attribute [rw] redis
|
52
|
+
# Redis configuration hash, Default: nil
|
53
|
+
# @!attribute [rw] storage_adapter_config
|
54
|
+
# Storage adapter configuration, See {Aeternitas::StorageAdapter} for configuration options
|
55
|
+
# @!attribute [rw] storage_adapter
|
56
|
+
# Storage adapter class. Default: {Aeternitas::StorageAdapter::File}
|
57
|
+
class Configuration
|
58
|
+
attr_accessor :redis, :storage_adapter, :storage_adapter_config
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
@redis = nil
|
62
|
+
@storage_adapter = Aeternitas::StorageAdapter::File
|
63
|
+
@storage_adapter_config = {
|
64
|
+
directory: defined?(Rails) ? File.join(Rails.root, %w[aeternitas_data]) : File.join(Dir.getwd, 'aeternitas_data')
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates a new StorageAdapter instance with the given options
|
69
|
+
# @return [Aeternitas::StoragesAdapter] new storage adapter instance
|
70
|
+
def get_storage_adapter
|
71
|
+
@storage_adapter.new(storage_adapter_config)
|
72
|
+
end
|
73
|
+
|
74
|
+
def redis=(redis_config)
|
75
|
+
@redis = redis_config
|
76
|
+
TabsTabs.configure { |tabstabs_config| tabstabs_config.redis = redis_config }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Aeternitas
|
2
|
+
module Errors
|
3
|
+
# Wrapper for ignored errors.
|
4
|
+
# This can be used to conveniently exclude certain errors from e.g. error trackers
|
5
|
+
# @!attribute [r] original error
|
6
|
+
# the wrapped error
|
7
|
+
class Ignored < StandardError
|
8
|
+
attr_reader :original_error
|
9
|
+
|
10
|
+
# Create a new Instance.
|
11
|
+
#
|
12
|
+
# @param [StandardError] original_error the wrapped error
|
13
|
+
def initialize(original_error)
|
14
|
+
@original_error = original_error
|
15
|
+
super("#{original_error.class} - #{original_error.message}")
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
# Raised when a source data already exists.
|
21
|
+
# @!attribute [r] fingerprint
|
22
|
+
# the sources fingerprint
|
23
|
+
class SourceDataExists < StandardError
|
24
|
+
attr_reader :fingerprint
|
25
|
+
|
26
|
+
# Create a new Exception
|
27
|
+
# @param [String] fingerprint the sources fingerprint
|
28
|
+
def initialize(fingerprint)
|
29
|
+
@fingerprint = fingerprint
|
30
|
+
super("The source entry with fingerprint '#{fingerprint}' already exists!")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Raised when a source entry does not exist.
|
35
|
+
# @!attribute [r] fingerprint
|
36
|
+
# the sources fingerprint
|
37
|
+
class SourceDataNotFound < StandardError
|
38
|
+
attr_reader :fingerprint
|
39
|
+
|
40
|
+
# Create a new Exception
|
41
|
+
# @param [String] fingerprint the sources fingerprint
|
42
|
+
def initialize(fingerprint)
|
43
|
+
@fingerprint = fingerprint
|
44
|
+
super("The source entry with fingerprint '#{fingerprint}' does not exist!")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'active_support/duration'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Aeternitas
|
5
|
+
# A distributed lock that can not be acquired after being unlocked for a certain time (cooldown period).
|
6
|
+
# Using Redis key expiration we ensure locks are released even after workers crash after a configurable timout period.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# guard = Aeternitas::Guard.new("Twitter-MY_API_KEY", 5.seconds)
|
10
|
+
# begin
|
11
|
+
# guard.with_lock do
|
12
|
+
# twitter_client.user_timeline('Darth_Max')
|
13
|
+
# end
|
14
|
+
# rescue Twitter::TooManyRequests => e
|
15
|
+
# guard.sleep_until(e.rate_limit.reset_at)
|
16
|
+
# raise Aeternitas::Guard::GuardIsLocked(e.rate_limit.reset_at)
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @!attribute [r] id
|
20
|
+
# @return [String] the guards id
|
21
|
+
# @!attribute [r] timeout
|
22
|
+
# @return [ActiveSupport::Duration] the locks timeout duration
|
23
|
+
# @!attribute [r] cooldown
|
24
|
+
# @return [ActiveSupport::Duration] cooldown time, in which the lock can't be acquired after being released
|
25
|
+
# @!attribute [r] token
|
26
|
+
# @return [String] cryptographic token which ensures we do not lock/unlock a guard held by another process
|
27
|
+
class Guard
|
28
|
+
|
29
|
+
attr_reader :id, :timeout, :cooldown, :token
|
30
|
+
|
31
|
+
# Create a new Guard
|
32
|
+
#
|
33
|
+
# @param [String] id Lock id
|
34
|
+
# @param [ActiveRecord::Duration] cooldown Cooldown time
|
35
|
+
# @param [ActiveRecord::Duration] timeout Lock timeout
|
36
|
+
# @return [Aeternitas::Guard] Creates a new Instance
|
37
|
+
def initialize(id, cooldown, timeout = 10.minutes)
|
38
|
+
@id = id
|
39
|
+
@cooldown = cooldown
|
40
|
+
@timeout = timeout
|
41
|
+
@token = SecureRandom.hex(10)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Runs a given block if the lock can be acquired and releases the lock afterwards.
|
45
|
+
#
|
46
|
+
# @raise [Aeternitas::LockWithCooldown::GuardIsLocked] if the lock can not be acquired
|
47
|
+
# @example
|
48
|
+
# Guard.new("MyId", 5.seconds, 10.minutes).with_lock { do_request() }
|
49
|
+
def with_lock
|
50
|
+
acquire_lock!
|
51
|
+
begin
|
52
|
+
yield
|
53
|
+
ensure
|
54
|
+
unlock
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Locks the guard until the given time.
|
59
|
+
#
|
60
|
+
# @param [Time] until_time sleep time
|
61
|
+
# @param [String] msg hint why the guard sleeps
|
62
|
+
def sleep_until(until_time, msg = nil)
|
63
|
+
sleep(until_time, msg)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Locks the guard for the given duration.
|
67
|
+
#
|
68
|
+
# @param [ActiveSupport::Duration] duration sleeping duration
|
69
|
+
# @param [String] msg hint why the guard sleeps
|
70
|
+
def sleep_for(duration, msg = nil)
|
71
|
+
raise ArgumentError, 'duration must be an ActiveRecord::Duration' unless duration.is_a?(ActiveSupport::Duration)
|
72
|
+
sleep_until(duration.from_now, msg)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Tries to acquire the lock.
|
78
|
+
#
|
79
|
+
# @example The Redis value looks like this
|
80
|
+
# {
|
81
|
+
# id: 'MyId'
|
82
|
+
# state: 'processing'
|
83
|
+
# timeout: '3600'
|
84
|
+
# cooldown: '5'
|
85
|
+
# locked_until: '2017-01-01 10:10:00'
|
86
|
+
# token: '1234567890'
|
87
|
+
# }
|
88
|
+
# @raise [Aeternitas::Guard::GuardIsLocked] if the lock can not be acquired
|
89
|
+
def acquire_lock!
|
90
|
+
payload = {
|
91
|
+
'id' => @id,
|
92
|
+
'state' => 'processing',
|
93
|
+
'timeout' => @timeout,
|
94
|
+
'cooldown' => @cooldown,
|
95
|
+
'locked_until' => @timeout.from_now,
|
96
|
+
'token' => @token
|
97
|
+
}
|
98
|
+
|
99
|
+
has_lock = Aeternitas.redis.set(@id, JSON.unparse(payload), ex: @timeout.to_i, nx: true)
|
100
|
+
|
101
|
+
raise(GuardIsLocked.new(@id, get_timeout)) unless has_lock
|
102
|
+
end
|
103
|
+
|
104
|
+
# Tries to unlock the guard. This starts the cooldown phase.
|
105
|
+
#
|
106
|
+
# @example The Redis value looks like this
|
107
|
+
# {
|
108
|
+
# id: 'MyId'
|
109
|
+
# state: 'cooldown'
|
110
|
+
# timeout: '3600'
|
111
|
+
# cooldown: '5'
|
112
|
+
# locked_until: '2017-01-01 10:00:05'
|
113
|
+
# token: '1234567890'
|
114
|
+
# }
|
115
|
+
def unlock
|
116
|
+
return false unless holds_lock?
|
117
|
+
|
118
|
+
payload = {
|
119
|
+
'id' => @id,
|
120
|
+
'state' => 'cooldown',
|
121
|
+
'timeout' => @timeout,
|
122
|
+
'cooldown' => @cooldown,
|
123
|
+
'locked_until' => @cooldown.from_now,
|
124
|
+
'token' => @token
|
125
|
+
}
|
126
|
+
|
127
|
+
Aeternitas.redis.set(@id, JSON.unparse(payload), ex: @cooldown.to_i)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Lets the guard sleep until the given date.
|
131
|
+
# This means that non can acquire the guards lock
|
132
|
+
#
|
133
|
+
# @example The Redis value looks like this
|
134
|
+
# {
|
135
|
+
# id: 'MyId'
|
136
|
+
# state: 'sleeping'
|
137
|
+
# timeout: '3600'
|
138
|
+
# cooldown: '5'
|
139
|
+
# locked_until: '2017-01-01 13:00'
|
140
|
+
# message: "API Quota Reached"
|
141
|
+
# }
|
142
|
+
# @todo Should this raise an error if the lock is not owned by this instance?
|
143
|
+
# @param [Time] sleep_timeout for how long will the guard sleep
|
144
|
+
# @param [String] msg hint why the guard sleeps
|
145
|
+
def sleep(sleep_timeout, msg = nil)
|
146
|
+
payload = {
|
147
|
+
'id' => @id,
|
148
|
+
'state' => 'sleeping',
|
149
|
+
'timeout' => @timeout,
|
150
|
+
'cooldown' => @cooldown,
|
151
|
+
'locked_until' => sleep_timeout
|
152
|
+
}
|
153
|
+
payload.merge(message: msg) if msg
|
154
|
+
|
155
|
+
Aeternitas.redis.set(@id, JSON.unparse(payload), ex: (sleep_timeout - Time.now).seconds.to_i)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Checks if this instance holds the lock. This is done by retrieving the value from redis and
|
159
|
+
# comparing the token value. If they match, than the lock is held by this instance.
|
160
|
+
#
|
161
|
+
# @todo Make the check atomic
|
162
|
+
# @return [Boolean] if the lock is held by this instance
|
163
|
+
def holds_lock?
|
164
|
+
payload = get_payload
|
165
|
+
payload['token'] == @token && payload['state'] == 'processing'
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the guards current timeout.
|
169
|
+
#
|
170
|
+
# @return [Time] the guards current timeout
|
171
|
+
def get_timeout
|
172
|
+
payload = get_payload
|
173
|
+
payload['state'] == 'processing' ? payload['cooldown'].to_i.seconds.from_now : Time.parse(payload['locked_until'])
|
174
|
+
end
|
175
|
+
|
176
|
+
# Retrieves the locks payload from redis.
|
177
|
+
#
|
178
|
+
# @return [Hash] the locks payload
|
179
|
+
def get_payload
|
180
|
+
value = Aeternitas.redis.get(@id)
|
181
|
+
return {} unless value
|
182
|
+
JSON.parse(value)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Custom error class thrown when the lock can not be acquired
|
186
|
+
# @!attribute [r] timeout
|
187
|
+
# @return [DateTime] the locks current timeout
|
188
|
+
class GuardIsLocked < StandardError
|
189
|
+
attr_reader :timeout
|
190
|
+
|
191
|
+
def initialize(resource_id, timeout, reason = nil)
|
192
|
+
msg = "Resource '#{resource_id}' is locked until #{timeout}."
|
193
|
+
msg += " Reason: #{reason}" if reason
|
194
|
+
super(msg)
|
195
|
+
@timeout = timeout
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'aeternitas/metrics/ten_minutes_resolution'
|
2
|
+
require 'aeternitas/metrics/counter'
|
3
|
+
require 'aeternitas/metrics/values'
|
4
|
+
require 'aeternitas/metrics/ratio'
|
5
|
+
|
6
|
+
module Aeternitas
|
7
|
+
# Provides extensive metrics for Aeternitas.
|
8
|
+
# Every metric is scoped by pollable class.
|
9
|
+
# Available metrics are:
|
10
|
+
# - polls => Number of polling runs
|
11
|
+
# - successful_polls => Number of successful polling runs
|
12
|
+
# - failed_polls => Number of failed polling runs (includes IgnoredErrors,
|
13
|
+
# excludes deactivation errors and Lock errors)
|
14
|
+
# - ignored_errors => Number of raised {Aeternitas::Errors::Ignored}
|
15
|
+
# - deactivation_errors => Number of errors raised which are declared as deactivation_errors
|
16
|
+
# - execution_time => Job execution time in seconds
|
17
|
+
# - guard_locked => Number of encountered locked guards
|
18
|
+
# - guard_timeout => Time until the guard is unlocked in seconds
|
19
|
+
# - guard_timeout_exceeded => Number of jobs that ran longer than the guards timeout
|
20
|
+
# - pollables_created => Number of created pollables
|
21
|
+
# - sources_created => Number of created sources
|
22
|
+
#
|
23
|
+
# Available Resolutions are:
|
24
|
+
# - :minute (stored for 3 days)
|
25
|
+
# - :ten_minutes (stored for 14 days)
|
26
|
+
# - :hour (stored for 2 months)
|
27
|
+
# - :day (stored indefinitely)
|
28
|
+
#
|
29
|
+
# Every metric can be accessed via a getter method:
|
30
|
+
# @example
|
31
|
+
# Aeternitas::Metrics.polls MyPollable, from: 3.days.ago, to: Time.now, resolution: :hour
|
32
|
+
# Aeternitas::Metrics.execution_times MyPollable
|
33
|
+
# @see #get
|
34
|
+
module Metrics
|
35
|
+
AVAILABLE_METRICS = {
|
36
|
+
polls: :counter,
|
37
|
+
successful_polls: :counter,
|
38
|
+
failed_polls: :counter,
|
39
|
+
ignored_errors: :counter,
|
40
|
+
deactivations: :counter,
|
41
|
+
execution_time: :value,
|
42
|
+
guard_locked: :counter,
|
43
|
+
guard_timeout: :value,
|
44
|
+
guard_timeout_exceeded: :counter,
|
45
|
+
sources_created: :counter,
|
46
|
+
pollables_created: :counter
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
TabsTabs.configure do |tabstabs_config|
|
50
|
+
tabstabs_config.unregister_resolutions(:week, :month, :year)
|
51
|
+
|
52
|
+
tabstabs_config.register_resolution Aeternitas::Metrics::TenMinutesResolution
|
53
|
+
|
54
|
+
tabstabs_config.set_expirations(
|
55
|
+
minute: 3.days,
|
56
|
+
ten_minutes: 14.days,
|
57
|
+
hour: 2.months
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
AVAILABLE_METRICS.each_pair do |metric, _|
|
62
|
+
module_eval <<-eoruby, __FILE__, __LINE__ + 1
|
63
|
+
def self.#{metric}(pollable, from: 1.hour.ago, to: Time.now, resolution: :minute)
|
64
|
+
self.get(:#{metric}, pollable, from: from, to: to, resolution: resolution )
|
65
|
+
end
|
66
|
+
eoruby
|
67
|
+
end
|
68
|
+
|
69
|
+
# Increses the specified counter metric for the given pollable.
|
70
|
+
# @param [Symbol, String] name the metric
|
71
|
+
# @param [Pollable] pollable_class pollable instance
|
72
|
+
def self.log(name, pollable_class)
|
73
|
+
raise('Metric not found') unless AVAILABLE_METRICS.key? name
|
74
|
+
raise ArgumentError, "#{name} isn't a Counter" unless AVAILABLE_METRICS[name] == :counter
|
75
|
+
begin
|
76
|
+
TabsTabs.increment_counter(get_key(name, pollable_class))
|
77
|
+
TabsTabs.increment_counter(get_key(name, Aeternitas::Pollable))
|
78
|
+
rescue StandardError ; end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Logs a value in a value metric for the given pollable.
|
82
|
+
# @param [Symbol String] name the metric
|
83
|
+
# @param [Pollable] pollable_class pollable instance
|
84
|
+
# @param [Object] value the value
|
85
|
+
def self.log_value(name, pollable_class, value)
|
86
|
+
raise('Metric not found') unless AVAILABLE_METRICS.key? name
|
87
|
+
raise(ArgumentError, "#{name} isn't a Value") unless AVAILABLE_METRICS[name] == :value
|
88
|
+
begin
|
89
|
+
TabsTabs.record_value(get_key(name, pollable_class), value)
|
90
|
+
TabsTabs.record_value(get_key(name, Aeternitas::Pollable), value)
|
91
|
+
rescue StandardError ; end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Retrieves the stats of the given metric in the given time frame and resolution.
|
95
|
+
# @param [Symbol String] name the metric
|
96
|
+
# @param [Object] pollable_class the pollable class
|
97
|
+
# @param [DateTime] from begin of the time frame
|
98
|
+
# @param [DateTime] to end of the timeframe
|
99
|
+
# @param [Symbol] resolution resolution
|
100
|
+
# @return [Aeternitas::Metrics::Counter, Aeternitas::Metrics::Value] stats
|
101
|
+
def self.get(name, pollable_class, from: 1.hour.ago, to: Time.now, resolution: :minute)
|
102
|
+
raise('Metric not found') unless AVAILABLE_METRICS.key? name
|
103
|
+
raise('Invalid interval') if from > to
|
104
|
+
result = TabsTabs.get_stats(get_key(name, pollable_class), from..to, resolution)
|
105
|
+
if AVAILABLE_METRICS[name] == :counter
|
106
|
+
Counter.new(result)
|
107
|
+
else
|
108
|
+
Values.new(result)
|
109
|
+
end
|
110
|
+
rescue TabsTabs::UnknownMetricError => _
|
111
|
+
TabsTabs.create_metric(get_key(name, pollable_class), AVAILABLE_METRICS[name].to_s)
|
112
|
+
get(name, pollable_class, from: from, to: to, resolution: resolution)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the failure ratio of the given job for given time frame and resolution
|
116
|
+
# @param [Symbol String] name the metric
|
117
|
+
# @param [Object] pollable_class the pollable class
|
118
|
+
# @param [DateTime] from begin of the time frame
|
119
|
+
# @param [DateTime] to end of the timeframe
|
120
|
+
# @param [Symbol] resolution resolution
|
121
|
+
# @return [Aeternitas::Metrics::Ratio] ratio time series
|
122
|
+
def self.failure_ratio(pollable_class, from: 1.hour.ago, to: Time.now, resolution: :minute)
|
123
|
+
polls = polls(pollable_class, from: from, to: to, resolution: resolution)
|
124
|
+
failed_polls = failed_polls(pollable_class, from: from, to: to, resolution: resolution)
|
125
|
+
Ratio.new(from, to, resolution, calculate_ratio(polls, failed_polls))
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns the lock ratio of the given job for given time frame and resolution
|
129
|
+
# @param [Symbol String] name the metric
|
130
|
+
# @param [Object] pollable_class the pollable class
|
131
|
+
# @param [DateTime] from begin of the time frame
|
132
|
+
# @param [DateTime] to end of the timeframe
|
133
|
+
# @param [Symbol] resolution resolution
|
134
|
+
# @return [Aeternitas::Metrics::Ratio] ratio time series
|
135
|
+
def self.guard_locked_ratio(pollable_class, from: 1.hour.ago, to: Time.now, resolution: :minute)
|
136
|
+
polls = polls(pollable_class, from: from, to: to, resolution: resolution)
|
137
|
+
guard_locked = guard_locked(pollable_class, from: from, to: to, resolution: resolution)
|
138
|
+
Ratio.new(from, to, resolution, calculate_ratio(polls, guard_locked))
|
139
|
+
end
|
140
|
+
|
141
|
+
# Computes the metric key of a given metric-pollable pair
|
142
|
+
# @param [Symbol, String] name the metric
|
143
|
+
# @param [Object] pollable_class pollable class
|
144
|
+
# @return [String] the metric key
|
145
|
+
def self.get_key(name, pollable_class)
|
146
|
+
"#{name}:#{pollable_class.name}"
|
147
|
+
end
|
148
|
+
|
149
|
+
# Computes the ratio of a base counter time series and a target counter time series
|
150
|
+
# @param [Array] base base time series data
|
151
|
+
# @param [Array] target target time series data
|
152
|
+
# @return [Array] ratio time series data
|
153
|
+
def self.calculate_ratio(base, target)
|
154
|
+
base.zip(target).map do |b, t|
|
155
|
+
{
|
156
|
+
timestamp: b['timestamp'],
|
157
|
+
ratio: b['count'].to_i.zero? ? 0 : t['count'].to_i / b['count'].to_f
|
158
|
+
}.with_indifferent_access
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|