aeternitas 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ module Aeternitas
2
+ module Metrics
3
+ # Wrapper for {TabsTabs::Metrics::Counter::Stats}.
4
+ # It is for Counter metrics.
5
+ class Counter
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ def_delegators :@tabstabs_stats, :min, :max, :avg, :each, :to_a, :first, :last
10
+
11
+ # Create a new Wrapper
12
+ # @param [TabsTabs::Metrics::Counter::Stats] tabstabs_stats the wrapped stats.
13
+ def initialize(tabstabs_stats)
14
+ @tabstabs_stats = tabstabs_stats
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,67 @@
1
+ module Aeternitas
2
+ module Metrics
3
+ # Stores time series data for ratios
4
+ # @!attribute [r] from
5
+ # @return [DateTime] start of the time series
6
+ # @!attribute [r] to
7
+ # @return [DateTime] end of the time series
8
+ # @!attribute [r] resolution
9
+ # @return [Symbol] resolution of the time series
10
+ # @!attribute [r] values
11
+ # @return [Array] time series data
12
+ # The time series values have the following format:
13
+ # {
14
+ # timestamp: DateTime("2000-01-01 00:00:00 UTC"),
15
+ # ratio: 0.01
16
+ # }
17
+ class Ratio
18
+ include Enumerable
19
+
20
+ attr_reader :from, :to, :resolution, :values
21
+
22
+ # Create a new ratio time series
23
+ # @param [DateTime] from start of the time series
24
+ # @param [DateTime] to end of the time series
25
+ # @param [Symbol] resolution time series resolution
26
+ # @param [Array] values time series data
27
+ def initialize(from, to, resolution, values)
28
+ @from = from
29
+ @to = to
30
+ @resolution = resolution
31
+ @values = values
32
+ end
33
+
34
+ def each(&block)
35
+ @values.each(&block)
36
+ end
37
+
38
+ def to_a
39
+ @values.to_a
40
+ end
41
+
42
+ # Computes the minimum ration within the time series.
43
+ # @return [Float] the minimum ratio
44
+ def min
45
+ @values.min_by { |v| v['ratio'] }['ratio']
46
+ end
47
+
48
+ # Computes the maximum ration within the time series.
49
+ # @return [Float] the maximum ratio
50
+ def max
51
+ @values.max_by { |v| v['ratio'] }['ratio']
52
+ end
53
+
54
+ # Computes the average ration within the time series.
55
+ # @return [Float] the average ratio
56
+ def avg
57
+ return 0 if count.zero?
58
+ p @values
59
+ @values.inject(0) { |sum, v| sum + v[:ratio] } / @values.count
60
+ end
61
+
62
+ def to_s
63
+ values.to_s
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,40 @@
1
+ module Aeternitas
2
+ module Metrics
3
+ # A TabsTabs resolution represeting 10 minute intervals.
4
+ module TenMinutesResolution
5
+ include TabsTabs::Resolutionable
6
+ extend self
7
+
8
+ PATTERN = '%Y-%m-%d-%H-%M'.freeze
9
+
10
+ def name
11
+ :ten_minutes
12
+ end
13
+
14
+ def serialize(ts)
15
+ Time.utc(ts.year, ts.month, ts.day, ts.hour, (ts.min / 10).to_i).strftime(PATTERN)
16
+ end
17
+
18
+ def deserialize(str)
19
+ dt = DateTime.strptime(str, PATTERN)
20
+ normalize(dt)
21
+ end
22
+
23
+ def from_seconds(s)
24
+ s / 10.minutes
25
+ end
26
+
27
+ def to_seconds
28
+ 10.minutes
29
+ end
30
+
31
+ def add(ts, number)
32
+ ts + number * 10.minutes
33
+ end
34
+
35
+ def normalize(ts)
36
+ Time.utc(ts.year, ts.month, ts.day, ts.hour, ts.min)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ module Aeternitas
2
+ module Metrics
3
+ # Wrapper for {TabsTabs::Metrics::Value::Stats}.
4
+ # It is for Value metrics.
5
+ class Values
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ def_delegators :@tabstabs_stats, :sum, :min, :max, :avg, :each, :to_a, :first, :last
10
+
11
+ # Create a new Wrapper
12
+ # @param [TabsTabs::Metrics::Value::Stats] tabstabs_stats the wrapped stats.
13
+ def initialize(tabstabs_stats)
14
+ @tabstabs_stats = tabstabs_stats
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,197 @@
1
+ require 'aeternitas/pollable/configuration'
2
+ require 'aeternitas/pollable/dsl'
3
+
4
+ module Aeternitas
5
+ # Mixin that enables the frequent polling of the receiving class.
6
+ # Classes including this method must implement the .poll method.
7
+ # Polling behaviour can be configured via {.pollable_options}.
8
+ # @note Can only be used by classes inheriting from ActiveRecord::Base
9
+ # @example
10
+ # class MyWebsitePollable < ActiveRecord::Base
11
+ # includes Aeternitas::Pollable
12
+ #
13
+ # polling_options do
14
+ # polling_frequency :daily
15
+ # lock_key ->(obj) {obj.url}
16
+ # end
17
+ #
18
+ # def poll
19
+ # response = HTTParty.get(self.url)
20
+ # raise StandardError, "#{self.url} responded with #{response.status}" unless response.success?
21
+ # HttpSource.create!(content: response.parsed_response)
22
+ # end
23
+ # end
24
+ module Pollable
25
+ extend ActiveSupport::Concern
26
+
27
+ included do
28
+ raise StandardError, 'Aeternitas::Pollable must inherit from ActiveRecord::Base' unless self.ancestors.include?(ActiveRecord::Base)
29
+
30
+ has_one :pollable_meta_data, as: :pollable,
31
+ dependent: :destroy,
32
+ class_name: 'Aeternitas::PollableMetaData'
33
+
34
+ has_many :sources, as: :pollable,
35
+ dependent: :destroy,
36
+ class_name: 'Aeternitas::Source'
37
+
38
+ validates :pollable_meta_data, presence: true
39
+
40
+ before_validation ->(pollable) do
41
+ pollable.pollable_meta_data ||= pollable.build_pollable_meta_data(state: 'waiting')
42
+ pollable.pollable_meta_data.pollable_class = pollable.class.name
43
+ end
44
+
45
+ after_commit ->(pollable) { Aeternitas::Metrics.log(:pollables_created, pollable.class) }, on: :create
46
+
47
+ delegate :next_polling, :last_polling, :disable_polling, to: :pollable_meta_data
48
+ end
49
+
50
+ # This method runs the polling workflow
51
+ def execute_poll
52
+ _before_poll
53
+
54
+ begin
55
+ guard.with_lock { poll }
56
+ rescue StandardError => e
57
+ if pollable_configuration.deactivation_errors.include?(e.class)
58
+ disable_polling(e)
59
+ return false
60
+ elsif pollable_configuration.ignored_errors.include?(e.class)
61
+ pollable_meta_data.has_errored!
62
+ raise Aeternitas::Errors::Ignored, e
63
+ else
64
+ pollable_meta_data.has_errored!
65
+ raise e
66
+ end
67
+ end
68
+
69
+ _after_poll
70
+ rescue StandardError => e
71
+ begin
72
+ log_poll_error(e)
73
+ ensure
74
+ raise e
75
+ end
76
+ end
77
+
78
+
79
+
80
+ # This method implements the class specific polling behaviour.
81
+ # It is only called after the lock was acquired successfully.
82
+ #
83
+ # @abstract This method must be implemented when {Aeternitas::Pollable} is included
84
+ def poll
85
+ raise NotImplementedError, "#{self.class.name} does not implement #poll, required by Aeternitas::Pollable"
86
+ end
87
+
88
+ # Registers the instance as pollable.
89
+ #
90
+ # @note Manual registration is only needed if the object was created before
91
+ # {Aeternitas::Pollable} was included. Otherwise it is done automatically after creation.
92
+ def register_pollable
93
+ self.pollable_meta_data ||= create_pollable_meta_data(
94
+ state: 'waiting',
95
+ pollable_class: self.class.name
96
+ )
97
+ end
98
+
99
+ def guard
100
+ guard_key = pollable_configuration.guard_options[:key].call(self)
101
+ guard_timeout = pollable_configuration.guard_options[:timeout]
102
+ guard_cooldown = pollable_configuration.guard_options[:cooldown]
103
+ Aeternitas::Guard.new(guard_key, guard_cooldown, guard_timeout)
104
+ end
105
+
106
+ # Access the Pollables configuration
107
+ #
108
+ # @return [Aeternitas::Pollable::Configuration] the pollables configuration
109
+ def pollable_configuration
110
+ self.class.pollable_configuration
111
+ end
112
+
113
+ # Creates a new source with the given content if it does not exist
114
+ # @example
115
+ # #...
116
+ # def poll
117
+ # response = HTTParty.get("http://example.com")
118
+ # add_source(response.parsed_response)
119
+ # end
120
+ # #...
121
+ # @param [String] raw_content the sources raw content
122
+ # @return [Aeternitas::Source] the newly created or existing source
123
+ def add_source(raw_content)
124
+ source = self.sources.create(raw_content: raw_content)
125
+ return nil unless source.persisted?
126
+
127
+ Aeternitas::Metrics.log(:sources_created, self.class)
128
+ source
129
+ end
130
+
131
+ private
132
+
133
+ # Run all prepolling methods
134
+ def _before_poll
135
+ @start_time = Time.now
136
+ Aeternitas::Metrics.log(:polls, self.class)
137
+
138
+ pollable_configuration.before_polling.each { |action| action.call(self) }
139
+ pollable_meta_data.poll!
140
+ end
141
+
142
+ # Run all postpolling methods
143
+ def _after_poll
144
+ pollable_meta_data.wait! do
145
+ pollable_meta_data.update_attributes!(
146
+ last_polling: Time.now,
147
+ next_polling: pollable_configuration.polling_frequency.call(self)
148
+ )
149
+ end
150
+
151
+ pollable_configuration.after_polling.each { |action| action.call(self) }
152
+
153
+ if @start_time
154
+ execution_time = Time.now - @start_time
155
+ Aeternitas::Metrics.log_value(:execution_time, self.class, execution_time)
156
+ Aeternitas::Metrics.log(:guard_timeout_exceeded, self.class) if execution_time > pollable_configuration.guard_options[:timeout]
157
+ @start_time = nil
158
+ end
159
+ Aeternitas::Metrics.log(:successful_polls, self.class)
160
+ end
161
+
162
+ def log_poll_error(e)
163
+ if e.is_a? Aeternitas::Guard::GuardIsLocked
164
+ Aeternitas::Metrics.log(:guard_locked, self.class)
165
+ Aeternitas::Metrics.log_value(:guard_timeout, self.class, e.timeout - Time.now)
166
+ elsif e.is_a? Aeternitas::Errors::Ignored
167
+ Aeternitas::Metrics.log(:ignored_error, self.class)
168
+ Aeternitas::Metrics.log(:failed_polls, self.class)
169
+ else
170
+ Aeternitas::Metrics.log(:failed_polls, self.class)
171
+ end
172
+ end
173
+
174
+ class_methods do
175
+ # Access the Pollables configuration
176
+ # @return [Aeternitas::Pollable::Configuration] the pollables configuration
177
+ def pollable_configuration
178
+ @pollable_configuration ||= Aeternitas::Pollable::Configuration.new
179
+ end
180
+
181
+ def pollable_configuration=(config)
182
+ @pollable_configuration = config
183
+ end
184
+
185
+ # Configure the polling process.
186
+ # For available configuration options see {Aeternitas::Pollable::Configuration} and {Aeternitas::Pollable::DSL}
187
+ def polling_options(&block)
188
+ Aeternitas::Pollable::Dsl.new(self.pollable_configuration, &block)
189
+ end
190
+
191
+ def inherited(other)
192
+ super
193
+ other.pollable_configuration = @pollable_configuration.copy
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,64 @@
1
+ module Aeternitas
2
+ module Pollable
3
+ # Holds the configuration of a pollable class
4
+ # @!attribute [rw] polling_frequency
5
+ # Method that calculates the next polling time after a successful poll.
6
+ # (Default: {Aeternitas::PollingFrequency::DAILY})
7
+ # @!attribute [rw] before_polling
8
+ # Methods to be run before each poll
9
+ # @!attribute [rw] after_polling
10
+ # Methods to be run after each successful poll
11
+ # @!attribute [rw] queue
12
+ # Sidekiq queue the poll job will be enqueued in (Default: 'polling')
13
+ # @!attribute [rw] guard_options
14
+ # Configuration of the pollables lock (Default: key => class+id, cooldown => 5.seconds, timeout => 10.minutes)
15
+ # @!attribute [rw] deactivation_errors
16
+ # The pollable instance will be deactivated if any of these errors occur while polling
17
+ # @!attribute [rw] ignored_errors
18
+ # Errors in this list will be wrapped by {Aeternitas::Error::Ignored} if they occur while polling
19
+ # (i.e. ignore in your exception tracker)
20
+ # @!attribute [rw] sleep_on_guard_locked
21
+ # When set to true poll jobs (and effectively the Sidekiq worker thread) will sleep until the
22
+ # lock is released if the lock could not be acquired. (Default: true)
23
+ class Configuration
24
+ attr_accessor :deactivation_errors,
25
+ :before_polling,
26
+ :queue,
27
+ :polling_frequency,
28
+ :after_polling,
29
+ :guard_options,
30
+ :ignored_errors,
31
+ :sleep_on_guard_locked
32
+
33
+ # Creates a new Configuration with default options
34
+ def initialize
35
+ @polling_frequency = Aeternitas::PollingFrequency::DAILY
36
+ @before_polling = []
37
+ @after_polling = []
38
+ @guard_options = {
39
+ key: ->(obj) { return obj.class.name.to_s },
40
+ timeout: 10.minutes,
41
+ cooldown: 5.seconds
42
+ }
43
+ @deactivation_errors = []
44
+ @ignored_errors = []
45
+ @queue = 'polling'
46
+ @sleep_on_guard_locked = true
47
+ end
48
+
49
+ def copy
50
+ config = Configuration.new
51
+ config.polling_frequency = self.polling_frequency
52
+ config.before_polling = self.before_polling.deep_dup
53
+ config.after_polling = self.after_polling.deep_dup
54
+ config.guard_options = self.guard_options.deep_dup
55
+ config.deactivation_errors = self.deactivation_errors.deep_dup
56
+ config.ignored_errors = self.ignored_errors.deep_dup
57
+ config.queue = self.queue
58
+ config.sleep_on_guard_locked = self.sleep_on_guard_locked
59
+ config
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,124 @@
1
+ module Aeternitas
2
+ module Pollable
3
+ # DSL wrapper to conveniently configure pollables
4
+ class Dsl
5
+ # Create a new DSL instance and configure the configuration with the given block
6
+ # @param [Aeternitas::Pollable::Configuration] configuration a pollables configuration
7
+ # @param [Proc] block configuration block
8
+ def initialize(configuration, &block)
9
+ @configuration = configuration
10
+ instance_eval(&block)
11
+ end
12
+
13
+ # Configures the polling frequency. This can be either the name of a {Aeternitas::PollingFrequency}
14
+ # or a lambda that receives a pollable instance and returns a DateTime
15
+ #
16
+ # @param [Symbol, Proc] frequency Sets the polling frequency.
17
+ # representing the next polling time.
18
+ # @example using a preset
19
+ # polling_frequency :weekly
20
+ # @example using a custom block
21
+ # polling_frequency ->(pollable) {Time.now + 1.month + Time.now - pollable.created_at.to_i / 3.month * 1.month}
22
+ # @todo allow custom methods via reference
23
+ def polling_frequency(frequency)
24
+ if frequency.is_a?(Symbol)
25
+ @configuration.polling_frequency = Aeternitas::PollingFrequency.by_name(frequency)
26
+ else
27
+ @configuration.polling_frequency = frequency
28
+ end
29
+ end
30
+
31
+ # Configures a method that will be run before every poll
32
+ #
33
+ # @param [Symbol, Block] method method or method name
34
+ # @example method by reference
35
+ # before_polling :my_method
36
+ # ...
37
+ # def my_method(pollable) do_something end
38
+ # @example method by block
39
+ # before_polling ->(pollable) {do_something}
40
+ def before_polling(method)
41
+ if method.is_a?(Symbol)
42
+ @configuration.before_polling << ->(pollable) { pollable.send(method) }
43
+ else
44
+ @configuration.before_polling << method
45
+ end
46
+ end
47
+
48
+ # Configures a method that will be run after every successful poll
49
+ #
50
+ # @param [Symbol, Block] method method or method name
51
+ # @example method by reference
52
+ # after:polling :my_method
53
+ # ...
54
+ # def my_method(pollable) do_something end
55
+ # @example method by block
56
+ # after_polling ->(pollable) {do_something}
57
+ def after_polling(method)
58
+ if method.is_a?(Symbol)
59
+ @configuration.after_polling << ->(pollable) { pollable.send(method) }
60
+ else
61
+ @configuration.after_polling << method
62
+ end
63
+ end
64
+
65
+ # Configure errors that will cause the pollable instance to be deactivated immediately during poll.
66
+ #
67
+ # @param [Object] error_class error classes
68
+ def deactivate_on(*error_class)
69
+ @configuration.deactivation_errors |= error_class
70
+ end
71
+
72
+ # Configure errors that will be wrapped in {Aeternitas::Error::Ignored}.
73
+ # Use this to group exceptions which should be ignored in your exception tracker.
74
+ #
75
+ # @param [Object] error_class error classes
76
+ def ignore_error(*error_class)
77
+ @configuration.ignored_errors |= error_class
78
+ end
79
+
80
+ # Configure the Sidekiq queue into which the instance's poll jobs will be enqueued.
81
+ # @param [String] queue name of the Sidekiq queue
82
+ def queue(queue)
83
+ @configuration.queue = queue
84
+ end
85
+
86
+ # Configure the guard key. This can be either a fixed String, a method reference or a block
87
+ #
88
+ # @param [String, Symbol, Proc] key lock key
89
+ # @example using a fixed String
90
+ # guard_key "MyLockKey"
91
+ # @example using a method reference
92
+ # guard_key :url
93
+ # @example using a block
94
+ # guard_key ->(pollable) {URI.parse(pollable.url).host}
95
+ def guard_key(key)
96
+ @configuration.guard_options[:key] = case key
97
+ when Symbol
98
+ ->(obj) { return obj.send(key) }
99
+ when Proc
100
+ key
101
+ else
102
+ ->(obj) { return key.to_s }
103
+ end
104
+ end
105
+
106
+ # Configure the guard.
107
+ # @see guard_key
108
+ # @see Aeternitas::Guard
109
+ # @param [Hash] options guard options
110
+ def guard_options(options)
111
+ @configuration.guard_options.merge!(options)
112
+ end
113
+
114
+ # Configure the behaviour of poll jobs if a lock can't be acquired.
115
+ # When set to true poll jobs (and effectively the Sidekiq worker thread) will sleep until the
116
+ # lock is released if the lock could not be acquired.
117
+ # @param [Boolean] switch true|false
118
+ def sleep_on_guard_locked(switch)
119
+ @configuration.sleep_on_guard_locked = switch
120
+ end
121
+ end
122
+ end
123
+ end
124
+