aeternitas 0.1.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.
- 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
|