aeternitas 0.2.0 → 2.0.0.rc1
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 +4 -4
- data/.gitattributes +1 -0
- data/.github/workflows/lint.yml +25 -0
- data/.github/workflows/tests.yml +28 -0
- data/.gitignore +2 -5
- data/.ruby-version +1 -1
- data/CHANGELOG.md +35 -0
- data/Gemfile +1 -1
- data/README.md +93 -148
- data/Rakefile +1 -1
- data/aeternitas.gemspec +23 -34
- data/lib/aeternitas/aeternitas_job.rb +7 -0
- data/lib/aeternitas/cleanup_old_metrics_job.rb +12 -0
- data/lib/aeternitas/cleanup_stale_locks_job.rb +12 -0
- data/lib/aeternitas/errors.rb +1 -2
- data/lib/aeternitas/guard.rb +72 -106
- data/lib/aeternitas/guard_lock.rb +19 -0
- data/lib/aeternitas/maintenance.rb +35 -0
- data/lib/aeternitas/metric.rb +12 -0
- data/lib/aeternitas/metrics.rb +54 -136
- data/lib/aeternitas/poll_job.rb +135 -0
- data/lib/aeternitas/pollable/configuration.rb +21 -22
- data/lib/aeternitas/pollable/dsl.rb +16 -17
- data/lib/aeternitas/pollable.rb +19 -18
- data/lib/aeternitas/pollable_meta_data.rb +18 -9
- data/lib/aeternitas/polling_frequency.rb +4 -4
- data/lib/aeternitas/source.rb +5 -5
- data/lib/aeternitas/storage_adapter/file.rb +9 -12
- data/lib/aeternitas/storage_adapter.rb +1 -3
- data/lib/aeternitas/unique_job_lock.rb +15 -0
- data/lib/aeternitas/version.rb +1 -1
- data/lib/aeternitas.rb +23 -26
- data/lib/generators/aeternitas/install_generator.rb +14 -8
- data/lib/generators/aeternitas/templates/add_aeternitas.rb.erb +34 -2
- data/lib/generators/aeternitas/templates/initializer.rb +10 -7
- metadata +35 -123
- data/.idea/.rakeTasks +0 -7
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/vcs.xml +0 -6
- data/.rspec +0 -2
- data/.rubocop.yml +0 -2
- data/.travis.yml +0 -8
- data/lib/aeternitas/metrics/counter.rb +0 -18
- data/lib/aeternitas/metrics/ratio.rb +0 -67
- data/lib/aeternitas/metrics/ten_minutes_resolution.rb +0 -40
- data/lib/aeternitas/metrics/values.rb +0 -18
- data/lib/aeternitas/sidekiq/middleware.rb +0 -31
- data/lib/aeternitas/sidekiq/poll_job.rb +0 -30
- data/lib/aeternitas/sidekiq.rb +0 -5
- data/logo.png +0 -0
- data/logo.svg +0 -198
@@ -9,7 +9,7 @@ module Aeternitas
|
|
9
9
|
# @!attribute [rw] after_polling
|
10
10
|
# Methods to be run after each successful poll
|
11
11
|
# @!attribute [rw] queue
|
12
|
-
#
|
12
|
+
# The queue the poll job will be enqueued in (Default: 'polling')
|
13
13
|
# @!attribute [rw] guard_options
|
14
14
|
# Configuration of the pollables lock (Default: key => class+id, cooldown => 5.seconds, timeout => 10.minutes)
|
15
15
|
# @!attribute [rw] deactivation_errors
|
@@ -18,17 +18,17 @@ module Aeternitas
|
|
18
18
|
# Errors in this list will be wrapped by {Aeternitas::Error::Ignored} if they occur while polling
|
19
19
|
# (i.e. ignore in your exception tracker)
|
20
20
|
# @!attribute [rw] sleep_on_guard_locked
|
21
|
-
# When set to true
|
22
|
-
# lock is released if the lock could not be acquired. (Default:
|
21
|
+
# When set to true, the ActiveJob worker thread will sleep until the
|
22
|
+
# lock is released if the lock could not be acquired. (Default: false)
|
23
23
|
class Configuration
|
24
24
|
attr_accessor :deactivation_errors,
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
:before_polling,
|
26
|
+
:queue,
|
27
|
+
:polling_frequency,
|
28
|
+
:after_polling,
|
29
|
+
:guard_options,
|
30
|
+
:ignored_errors,
|
31
|
+
:sleep_on_guard_locked
|
32
32
|
|
33
33
|
# Creates a new Configuration with default options
|
34
34
|
def initialize
|
@@ -36,29 +36,28 @@ module Aeternitas
|
|
36
36
|
@before_polling = []
|
37
37
|
@after_polling = []
|
38
38
|
@guard_options = {
|
39
|
-
key: ->(obj) {
|
39
|
+
key: ->(obj) { obj.class.name.to_s },
|
40
40
|
timeout: 10.minutes,
|
41
41
|
cooldown: 5.seconds
|
42
42
|
}
|
43
43
|
@deactivation_errors = []
|
44
44
|
@ignored_errors = []
|
45
|
-
@queue =
|
46
|
-
@sleep_on_guard_locked =
|
45
|
+
@queue = "polling"
|
46
|
+
@sleep_on_guard_locked = false
|
47
47
|
end
|
48
48
|
|
49
49
|
def copy
|
50
50
|
config = Configuration.new
|
51
|
-
config.polling_frequency =
|
52
|
-
config.before_polling =
|
53
|
-
config.after_polling =
|
54
|
-
config.guard_options =
|
55
|
-
config.deactivation_errors =
|
56
|
-
config.ignored_errors =
|
57
|
-
config.queue =
|
58
|
-
config.sleep_on_guard_locked =
|
51
|
+
config.polling_frequency = polling_frequency
|
52
|
+
config.before_polling = before_polling.deep_dup
|
53
|
+
config.after_polling = after_polling.deep_dup
|
54
|
+
config.guard_options = guard_options.deep_dup
|
55
|
+
config.deactivation_errors = deactivation_errors.deep_dup
|
56
|
+
config.ignored_errors = ignored_errors.deep_dup
|
57
|
+
config.queue = queue
|
58
|
+
config.sleep_on_guard_locked = sleep_on_guard_locked
|
59
59
|
config
|
60
60
|
end
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
64
|
-
|
@@ -21,10 +21,10 @@ module Aeternitas
|
|
21
21
|
# polling_frequency ->(pollable) {Time.now + 1.month + Time.now - pollable.created_at.to_i / 3.month * 1.month}
|
22
22
|
# @todo allow custom methods via reference
|
23
23
|
def polling_frequency(frequency)
|
24
|
-
if frequency.is_a?(Symbol)
|
25
|
-
|
24
|
+
@configuration.polling_frequency = if frequency.is_a?(Symbol)
|
25
|
+
Aeternitas::PollingFrequency.by_name(frequency)
|
26
26
|
else
|
27
|
-
|
27
|
+
frequency
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -38,10 +38,10 @@ module Aeternitas
|
|
38
38
|
# @example method by block
|
39
39
|
# before_polling ->(pollable) {do_something}
|
40
40
|
def before_polling(method)
|
41
|
-
if method.is_a?(Symbol)
|
42
|
-
|
41
|
+
@configuration.before_polling << if method.is_a?(Symbol)
|
42
|
+
->(pollable) { pollable.send(method) }
|
43
43
|
else
|
44
|
-
|
44
|
+
method
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -55,10 +55,10 @@ module Aeternitas
|
|
55
55
|
# @example method by block
|
56
56
|
# after_polling ->(pollable) {do_something}
|
57
57
|
def after_polling(method)
|
58
|
-
if method.is_a?(Symbol)
|
59
|
-
|
58
|
+
@configuration.after_polling << if method.is_a?(Symbol)
|
59
|
+
->(pollable) { pollable.send(method) }
|
60
60
|
else
|
61
|
-
|
61
|
+
method
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
@@ -94,13 +94,13 @@ module Aeternitas
|
|
94
94
|
# guard_key ->(pollable) {URI.parse(pollable.url).host}
|
95
95
|
def guard_key(key)
|
96
96
|
@configuration.guard_options[:key] = case key
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
97
|
+
when Symbol
|
98
|
+
->(obj) { obj.send(key) }
|
99
|
+
when Proc
|
100
|
+
key
|
101
|
+
else
|
102
|
+
->(obj) { key.to_s }
|
103
|
+
end
|
104
104
|
end
|
105
105
|
|
106
106
|
# Configure the guard.
|
@@ -121,4 +121,3 @@ module Aeternitas
|
|
121
121
|
end
|
122
122
|
end
|
123
123
|
end
|
124
|
-
|
data/lib/aeternitas/pollable.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "aeternitas/pollable/configuration"
|
2
|
+
require "aeternitas/pollable/dsl"
|
3
3
|
|
4
4
|
module Aeternitas
|
5
5
|
# Mixin that enables the frequent polling of the receiving class.
|
@@ -25,20 +25,20 @@ module Aeternitas
|
|
25
25
|
extend ActiveSupport::Concern
|
26
26
|
|
27
27
|
included do
|
28
|
-
raise StandardError,
|
28
|
+
raise StandardError, "Aeternitas::Pollable must inherit from ActiveRecord::Base" unless ancestors.include?(ActiveRecord::Base)
|
29
29
|
|
30
30
|
has_one :pollable_meta_data, as: :pollable,
|
31
|
-
|
32
|
-
|
31
|
+
dependent: :destroy,
|
32
|
+
class_name: "Aeternitas::PollableMetaData"
|
33
33
|
|
34
34
|
has_many :sources, as: :pollable,
|
35
|
-
|
36
|
-
|
35
|
+
dependent: :destroy,
|
36
|
+
class_name: "Aeternitas::Source"
|
37
37
|
|
38
38
|
validates :pollable_meta_data, presence: true
|
39
39
|
|
40
40
|
before_validation ->(pollable) do
|
41
|
-
pollable.pollable_meta_data ||= pollable.build_pollable_meta_data(state:
|
41
|
+
pollable.pollable_meta_data ||= pollable.build_pollable_meta_data(state: "waiting")
|
42
42
|
pollable.pollable_meta_data.pollable_class = pollable.class.name
|
43
43
|
end
|
44
44
|
|
@@ -53,7 +53,10 @@ module Aeternitas
|
|
53
53
|
|
54
54
|
begin
|
55
55
|
guard.with_lock { poll }
|
56
|
-
rescue
|
56
|
+
rescue Aeternitas::Guard::GuardIsLocked
|
57
|
+
# Do not transition to the 'errored' state for a guard lock.
|
58
|
+
raise
|
59
|
+
rescue => e
|
57
60
|
if pollable_configuration.deactivation_errors.include?(e.class)
|
58
61
|
disable_polling(e)
|
59
62
|
return false
|
@@ -67,7 +70,7 @@ module Aeternitas
|
|
67
70
|
end
|
68
71
|
|
69
72
|
_after_poll
|
70
|
-
rescue
|
73
|
+
rescue => e
|
71
74
|
begin
|
72
75
|
log_poll_error(e)
|
73
76
|
ensure
|
@@ -75,8 +78,6 @@ module Aeternitas
|
|
75
78
|
end
|
76
79
|
end
|
77
80
|
|
78
|
-
|
79
|
-
|
80
81
|
# This method implements the class specific polling behaviour.
|
81
82
|
# It is only called after the lock was acquired successfully.
|
82
83
|
#
|
@@ -91,8 +92,8 @@ module Aeternitas
|
|
91
92
|
# {Aeternitas::Pollable} was included. Otherwise it is done automatically after creation.
|
92
93
|
def register_pollable
|
93
94
|
self.pollable_meta_data ||= create_pollable_meta_data(
|
94
|
-
|
95
|
-
|
95
|
+
state: "waiting",
|
96
|
+
pollable_class: self.class.name
|
96
97
|
)
|
97
98
|
end
|
98
99
|
|
@@ -121,7 +122,7 @@ module Aeternitas
|
|
121
122
|
# @param [String] raw_content the sources raw content
|
122
123
|
# @return [Aeternitas::Source] the newly created or existing source
|
123
124
|
def add_source(raw_content)
|
124
|
-
source =
|
125
|
+
source = sources.create(raw_content: raw_content)
|
125
126
|
return nil unless source.persisted?
|
126
127
|
|
127
128
|
Aeternitas::Metrics.log(:sources_created, self.class)
|
@@ -142,7 +143,7 @@ module Aeternitas
|
|
142
143
|
# Run all postpolling methods
|
143
144
|
def _after_poll
|
144
145
|
pollable_meta_data.wait! do
|
145
|
-
pollable_meta_data.
|
146
|
+
pollable_meta_data.update!(
|
146
147
|
last_polling: Time.now,
|
147
148
|
next_polling: pollable_configuration.polling_frequency.call(self)
|
148
149
|
)
|
@@ -185,7 +186,7 @@ module Aeternitas
|
|
185
186
|
# Configure the polling process.
|
186
187
|
# For available configuration options see {Aeternitas::Pollable::Configuration} and {Aeternitas::Pollable::DSL}
|
187
188
|
def polling_options(&block)
|
188
|
-
Aeternitas::Pollable::Dsl.new(
|
189
|
+
Aeternitas::Pollable::Dsl.new(pollable_configuration, &block)
|
189
190
|
end
|
190
191
|
|
191
192
|
def inherited(other)
|
@@ -194,4 +195,4 @@ module Aeternitas
|
|
194
195
|
end
|
195
196
|
end
|
196
197
|
end
|
197
|
-
end
|
198
|
+
end
|
@@ -1,10 +1,10 @@
|
|
1
|
-
require
|
1
|
+
require "aasm"
|
2
2
|
|
3
3
|
module Aeternitas
|
4
4
|
# Stores the meta data of all pollables
|
5
5
|
# Every pollable needs to have exactly one meta data object
|
6
6
|
class PollableMetaData < ActiveRecord::Base
|
7
|
-
self.table_name =
|
7
|
+
self.table_name = "aeternitas_pollable_meta_data"
|
8
8
|
|
9
9
|
include AASM
|
10
10
|
######
|
@@ -25,20 +25,29 @@ module Aeternitas
|
|
25
25
|
|
26
26
|
belongs_to :pollable, polymorphic: true
|
27
27
|
|
28
|
-
validates :pollable_type, presence: true, uniqueness: {
|
29
|
-
validates :pollable_id, presence: true, uniqueness: {
|
28
|
+
validates :pollable_type, presence: true, uniqueness: {scope: :pollable_id}
|
29
|
+
validates :pollable_id, presence: true, uniqueness: {scope: :pollable_type}
|
30
30
|
validates :pollable_class, presence: true
|
31
31
|
validates :next_polling, presence: true
|
32
32
|
|
33
33
|
aasm column: :state do
|
34
|
+
# Only pollables in this state are picked up for enqueueing ('next_polling')
|
34
35
|
state :waiting, initial: true
|
36
|
+
|
37
|
+
# A PollJob has been submitted and the pollable is waiting for a worker to start processing.
|
35
38
|
state :enqueued
|
39
|
+
|
40
|
+
# A worker has picked up the job and is currently executing the pollable.
|
36
41
|
state :active
|
42
|
+
|
43
|
+
# Polling has been permanently disabled (e.g., due to retries exhausted).
|
37
44
|
state :deactivated
|
45
|
+
|
46
|
+
# A generic error occurred during polling. The job will be retried.
|
38
47
|
state :errored
|
39
48
|
|
40
49
|
event :enqueue do
|
41
|
-
transitions from: %i[waiting deactivated errored], to: :enqueued
|
50
|
+
transitions from: %i[waiting deactivated errored active], to: :enqueued
|
42
51
|
end
|
43
52
|
|
44
53
|
event :poll do
|
@@ -58,16 +67,16 @@ module Aeternitas
|
|
58
67
|
end
|
59
68
|
end
|
60
69
|
|
61
|
-
scope(:due, ->
|
70
|
+
scope(:due, -> { waiting.where("next_polling < ?", Time.now) })
|
62
71
|
|
63
72
|
# Disables polling of this instance
|
64
73
|
#
|
65
74
|
# @param [String] reason Reason for the deactivation. (E.g. an error message)
|
66
75
|
def disable_polling(reason = nil)
|
67
|
-
|
76
|
+
deactivate
|
68
77
|
self.deactivation_reason = reason.to_s
|
69
78
|
self.deactivated_at = Time.now
|
70
|
-
|
79
|
+
save!
|
71
80
|
end
|
72
81
|
end
|
73
|
-
end
|
82
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Aeternitas
|
2
2
|
# Stores default polling frequency calculation methods.
|
3
3
|
module PollingFrequency
|
4
|
-
HOURLY
|
5
|
-
DAILY
|
6
|
-
WEEKLY
|
4
|
+
HOURLY = ->(context) { Time.now + 1.hour }
|
5
|
+
DAILY = ->(context) { Time.now + 1.day }
|
6
|
+
WEEKLY = ->(context) { Time.now + 1.week }
|
7
7
|
MONTHLY = ->(context) { Time.now + 1.month }
|
8
8
|
|
9
9
|
# Retrieves the build-in polling frequency methods by name.
|
@@ -21,4 +21,4 @@ module Aeternitas
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
24
|
-
end
|
24
|
+
end
|
data/lib/aeternitas/source.rb
CHANGED
@@ -13,7 +13,7 @@ module Aeternitas
|
|
13
13
|
# end
|
14
14
|
# add_index :aeternitas_sources, [:pollable_id, :pollable_type], name: 'aeternitas_pollable_source'
|
15
15
|
######
|
16
|
-
self.table_name =
|
16
|
+
self.table_name = "aeternitas_sources"
|
17
17
|
|
18
18
|
attr_writer :raw_content
|
19
19
|
|
@@ -42,21 +42,21 @@ module Aeternitas
|
|
42
42
|
# Get the sources raw content.
|
43
43
|
# @return [String] the sources raw content
|
44
44
|
def raw_content
|
45
|
-
@raw_content ||= Aeternitas.config.get_storage_adapter.retrieve(
|
45
|
+
@raw_content ||= Aeternitas.config.get_storage_adapter.retrieve(fingerprint)
|
46
46
|
end
|
47
47
|
|
48
48
|
private
|
49
49
|
|
50
50
|
def create_file
|
51
|
-
Aeternitas.config.get_storage_adapter.store(
|
51
|
+
Aeternitas.config.get_storage_adapter.store(fingerprint, raw_content)
|
52
52
|
end
|
53
53
|
|
54
54
|
def delete_file
|
55
|
-
Aeternitas.config.get_storage_adapter.delete(
|
55
|
+
Aeternitas.config.get_storage_adapter.delete(fingerprint)
|
56
56
|
end
|
57
57
|
|
58
58
|
def ensure_fingerprint
|
59
59
|
self.fingerprint ||= generate_fingerprint
|
60
60
|
end
|
61
61
|
end
|
62
|
-
end
|
62
|
+
end
|
@@ -2,7 +2,6 @@ module Aeternitas
|
|
2
2
|
class StorageAdapter
|
3
3
|
# A storage adapter that stores the entries on disk.
|
4
4
|
class File < Aeternitas::StorageAdapter
|
5
|
-
|
6
5
|
# Create a new File storage adapter.
|
7
6
|
# @param [Hash] config the adapters config
|
8
7
|
# @option config [String] :directory specifies where the entries are stored
|
@@ -14,22 +13,20 @@ module Aeternitas
|
|
14
13
|
path = file_path(id)
|
15
14
|
ensure_folders_exist(path)
|
16
15
|
raise(Aeternitas::Errors::SourceDataExists, id) if ::File.exist?(path)
|
17
|
-
::File.open(path,
|
16
|
+
::File.open(path, "w+", encoding: "ascii-8bit") do |f|
|
18
17
|
f.write(Zlib.deflate(raw_content, Zlib::BEST_COMPRESSION))
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
22
21
|
def retrieve(id)
|
23
22
|
raise(Aeternitas::Errors::SourceDataNotFound, id) unless exist?(id)
|
24
|
-
Zlib.inflate(::File.read(file_path(id), encoding:
|
23
|
+
Zlib.inflate(::File.read(file_path(id), encoding: "ascii-8bit"))
|
25
24
|
end
|
26
25
|
|
27
26
|
def delete(id)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
return false
|
32
|
-
end
|
27
|
+
!!::File.delete(file_path(id))
|
28
|
+
rescue Errno::ENOENT
|
29
|
+
false
|
33
30
|
end
|
34
31
|
|
35
32
|
def exist?(id)
|
@@ -57,9 +54,9 @@ module Aeternitas
|
|
57
54
|
# @return [String] the entries location
|
58
55
|
def file_path(id)
|
59
56
|
::File.join(
|
60
|
-
|
61
|
-
|
62
|
-
|
57
|
+
@config[:directory],
|
58
|
+
id[0..1], id[2..3], id[4..5],
|
59
|
+
id[6..]
|
63
60
|
)
|
64
61
|
end
|
65
62
|
|
@@ -70,4 +67,4 @@ module Aeternitas
|
|
70
67
|
end
|
71
68
|
end
|
72
69
|
end
|
73
|
-
end
|
70
|
+
end
|
@@ -3,7 +3,6 @@ module Aeternitas
|
|
3
3
|
# Storage Adapters take care of handling source files.
|
4
4
|
# @abstract Create a subclass and override {#store}, #{retrieve} and #{#delete} to create a new storage adapter
|
5
5
|
class StorageAdapter
|
6
|
-
|
7
6
|
# Create a new storage adapter
|
8
7
|
# @param [Hash] config the adapters configuration
|
9
8
|
def initialize(config)
|
@@ -41,6 +40,5 @@ module Aeternitas
|
|
41
40
|
def exist?(id)
|
42
41
|
raise NotImplementedError, "#{self.class.name} does not implement #exist?, required by Aeternitas::StorageAdapter"
|
43
42
|
end
|
44
|
-
|
45
43
|
end
|
46
|
-
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
3
|
+
module Aeternitas
|
4
|
+
# Since ActiveJob lacks a built-in uniqueness feature, this model prevents duplicate jobs from running simultaneously.
|
5
|
+
# This is achieved using the unique key `lock_digest`, which is calculated for each job instance.
|
6
|
+
# Additionally, this model stores `guard_key_digest` — a hash of the pollable's guard key —
|
7
|
+
# to determine how many jobs are waiting on the same resource and to adjust retries accordingly.
|
8
|
+
# Finally, `expires_at` is used to clean up stale locks left by crashed workers.
|
9
|
+
class UniqueJobLock < ActiveRecord::Base
|
10
|
+
self.table_name = "aeternitas_unique_job_locks"
|
11
|
+
|
12
|
+
validates :lock_digest, presence: true, uniqueness: true
|
13
|
+
validates :expires_at, presence: true
|
14
|
+
end
|
15
|
+
end
|
data/lib/aeternitas/version.rb
CHANGED
data/lib/aeternitas.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
|
+
require "ostruct"
|
1
2
|
require "active_support/all"
|
2
|
-
require "redis"
|
3
|
-
require "connection_pool"
|
4
|
-
require "sidekiq-unique-jobs"
|
5
|
-
require "tabs_tabs"
|
6
3
|
require "aeternitas/version"
|
7
4
|
require "aeternitas/guard"
|
8
5
|
require "aeternitas/pollable"
|
@@ -11,18 +8,18 @@ require "aeternitas/source"
|
|
11
8
|
require "aeternitas/polling_frequency"
|
12
9
|
require "aeternitas/errors"
|
13
10
|
require "aeternitas/storage_adapter"
|
14
|
-
require "aeternitas/
|
11
|
+
require "aeternitas/metric"
|
15
12
|
require "aeternitas/metrics"
|
13
|
+
require "aeternitas/maintenance"
|
14
|
+
require "aeternitas/unique_job_lock"
|
15
|
+
require "aeternitas/guard_lock"
|
16
|
+
require "aeternitas/aeternitas_job"
|
17
|
+
require "aeternitas/poll_job"
|
18
|
+
require "aeternitas/cleanup_stale_locks_job"
|
19
|
+
require "aeternitas/cleanup_old_metrics_job"
|
16
20
|
|
17
21
|
# Aeternitas
|
18
22
|
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
23
|
# Access the configuration
|
27
24
|
# @return [Aeternitas::Configuration] the Aeternitas configuration
|
28
25
|
def self.config
|
@@ -33,36 +30,42 @@ module Aeternitas
|
|
33
30
|
# @see Aeternitas::Configuration
|
34
31
|
# @yieldparam [Aeternitas::Configuration] config the aeternitas configuration
|
35
32
|
def self.configure
|
36
|
-
yield(
|
33
|
+
yield(config)
|
37
34
|
end
|
38
35
|
|
39
36
|
# Enqueues all active pollables for which next polling is lower than the current time
|
40
37
|
def self.enqueue_due_pollables
|
41
38
|
Aeternitas::PollableMetaData.due.find_each do |pollable_meta_data|
|
42
|
-
Aeternitas::
|
39
|
+
Aeternitas::PollJob
|
43
40
|
.set(queue: pollable_meta_data.pollable.pollable_configuration.queue)
|
44
|
-
.
|
41
|
+
.perform_later(pollable_meta_data.id)
|
45
42
|
pollable_meta_data.enqueue
|
46
43
|
pollable_meta_data.save
|
47
44
|
end
|
48
45
|
end
|
49
46
|
|
50
47
|
# Stores the global Aeternitas configuration
|
51
|
-
# @!attribute [rw] redis
|
52
|
-
# Redis configuration hash, Default: nil
|
53
48
|
# @!attribute [rw] storage_adapter_config
|
54
49
|
# Storage adapter configuration, See {Aeternitas::StorageAdapter} for configuration options
|
55
50
|
# @!attribute [rw] storage_adapter
|
56
51
|
# Storage adapter class. Default: {Aeternitas::StorageAdapter::File}
|
52
|
+
# @!attribute [rw] metrics_enabled
|
53
|
+
# Whether to log metrics to the database. Default: false
|
54
|
+
# @!attribute [rw] metric_retention_period
|
55
|
+
# How long to keep metric data before it can be cleaned up. Default: 90.days
|
57
56
|
class Configuration
|
58
|
-
attr_accessor :
|
57
|
+
attr_accessor :storage_adapter,
|
58
|
+
:storage_adapter_config,
|
59
|
+
:metrics_enabled,
|
60
|
+
:metric_retention_period
|
59
61
|
|
60
62
|
def initialize
|
61
|
-
@redis = nil
|
62
63
|
@storage_adapter = Aeternitas::StorageAdapter::File
|
63
64
|
@storage_adapter_config = {
|
64
|
-
directory: defined?(Rails) ?
|
65
|
+
directory: defined?(Rails) ? Rails.root.join("storage", "aeternitas") : File.join(Dir.getwd, "aeternitas_data")
|
65
66
|
}
|
67
|
+
@metrics_enabled = false
|
68
|
+
@metric_retention_period = 90.days
|
66
69
|
end
|
67
70
|
|
68
71
|
# Creates a new StorageAdapter instance with the given options
|
@@ -70,11 +73,5 @@ module Aeternitas
|
|
70
73
|
def get_storage_adapter
|
71
74
|
@storage_adapter.new(storage_adapter_config)
|
72
75
|
end
|
73
|
-
|
74
|
-
def redis=(redis_config)
|
75
|
-
@redis = redis_config
|
76
|
-
TabsTabs.configure { |tabstabs_config| tabstabs_config.redis = redis_config }
|
77
|
-
end
|
78
76
|
end
|
79
77
|
end
|
80
|
-
|
@@ -8,28 +8,34 @@ module Aeternitas
|
|
8
8
|
|
9
9
|
source_root File.expand_path("../templates", __FILE__)
|
10
10
|
|
11
|
-
desc
|
12
|
-
|
11
|
+
desc "Generates (but does not run) a migration to add all tables needed by Aeternitas." \
|
12
|
+
" Also generates an initializer file for configuring Aeternitas"
|
13
13
|
|
14
14
|
def create_migration_file
|
15
15
|
migration_dir = File.expand_path("db/migrate")
|
16
|
-
if self.class.migration_exists?(migration_dir,
|
17
|
-
::Kernel.warn "Migration already exists
|
16
|
+
if self.class.migration_exists?(migration_dir, "add_aeternitas")
|
17
|
+
::Kernel.warn "Migration 'add_aeternitas' already exists. Skipping."
|
18
18
|
else
|
19
|
-
migration_template(
|
19
|
+
migration_template("add_aeternitas.rb.erb", "db/migrate/add_aeternitas.rb")
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
def copy_initializer
|
24
|
-
copy_file(
|
24
|
+
copy_file("initializer.rb", "config/initializers/aeternitas.rb")
|
25
25
|
end
|
26
26
|
|
27
27
|
def reminder
|
28
|
-
say "
|
28
|
+
say "\nDon't forget to regularly run 'Aeternitas.enqueue_due_pollables', e.g., using 'whenever'", :red
|
29
|
+
say "You should also schedule maintenance jobs:\n", :yellow
|
30
|
+
say "To clean up old metrics (if metrics are enabled):\n"
|
31
|
+
say " Aeternitas::CleanupOldMetricsJob.perform_later\n", :white
|
32
|
+
say "To clean up stale locks from crashed workers:\n"
|
33
|
+
say " Aeternitas::CleanupStaleLocksJob.perform_later\n", :white
|
34
|
+
say "Schedule these to run periodically, for example, once a week.\n"
|
29
35
|
end
|
30
36
|
|
31
37
|
def self.next_migration_number(dirname)
|
32
38
|
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
33
39
|
end
|
34
40
|
end
|
35
|
-
end
|
41
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# This migration creates the tables needed by Aeternitas
|
2
|
-
class AddAeternitas < ActiveRecord::Migration
|
2
|
+
class AddAeternitas < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
3
3
|
def change
|
4
4
|
create_table :aeternitas_pollable_meta_data do |t|
|
5
5
|
t.string :pollable_type, null: false
|
6
6
|
t.integer :pollable_id, null: false
|
7
7
|
t.string :pollable_class, null: false
|
8
|
-
t.datetime :next_polling, null: false, default: "1970-01-01 00:00:00
|
8
|
+
t.datetime :next_polling, null: false, default: "1970-01-01 00:00:00"
|
9
9
|
t.datetime :last_polling
|
10
10
|
t.string :state
|
11
11
|
t.text :deactivation_reason
|
@@ -21,5 +21,37 @@ class AddAeternitas < ActiveRecord::Migration
|
|
21
21
|
t.datetime :created_at
|
22
22
|
end
|
23
23
|
add_index :aeternitas_sources, [:pollable_id, :pollable_type], name: 'aeternitas_pollable_source'
|
24
|
+
|
25
|
+
create_table :aeternitas_unique_job_locks do |t|
|
26
|
+
t.string :lock_digest, null: false
|
27
|
+
t.string :guard_key_digest
|
28
|
+
t.string :job_id
|
29
|
+
t.datetime :expires_at, null: false
|
30
|
+
|
31
|
+
t.timestamps
|
32
|
+
end
|
33
|
+
add_index :aeternitas_unique_job_locks, :lock_digest, unique: true
|
34
|
+
add_index :aeternitas_unique_job_locks, :guard_key_digest
|
35
|
+
add_index :aeternitas_unique_job_locks, :expires_at
|
36
|
+
|
37
|
+
create_table :aeternitas_guard_locks do |t|
|
38
|
+
t.string :lock_key, null: false
|
39
|
+
t.string :state, null: false
|
40
|
+
t.string :token, null: false, limit: 20
|
41
|
+
t.datetime :locked_until, null: false
|
42
|
+
t.text :reason
|
43
|
+
|
44
|
+
t.timestamps
|
45
|
+
end
|
46
|
+
add_index :aeternitas_guard_locks, :lock_key, unique: true
|
47
|
+
add_index :aeternitas_guard_locks, :locked_until
|
48
|
+
|
49
|
+
create_table :aeternitas_metrics do |t|
|
50
|
+
t.string :name, null: false
|
51
|
+
t.string :pollable_class, null: false
|
52
|
+
t.float :value, null: false
|
53
|
+
t.datetime :created_at, null: false
|
54
|
+
end
|
55
|
+
add_index :aeternitas_metrics, [:name, :pollable_class, :created_at], name: 'idx_aeternitas_metrics'
|
24
56
|
end
|
25
57
|
end
|