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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +1 -0
  3. data/.github/workflows/lint.yml +25 -0
  4. data/.github/workflows/tests.yml +28 -0
  5. data/.gitignore +2 -5
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +35 -0
  8. data/Gemfile +1 -1
  9. data/README.md +93 -148
  10. data/Rakefile +1 -1
  11. data/aeternitas.gemspec +23 -34
  12. data/lib/aeternitas/aeternitas_job.rb +7 -0
  13. data/lib/aeternitas/cleanup_old_metrics_job.rb +12 -0
  14. data/lib/aeternitas/cleanup_stale_locks_job.rb +12 -0
  15. data/lib/aeternitas/errors.rb +1 -2
  16. data/lib/aeternitas/guard.rb +72 -106
  17. data/lib/aeternitas/guard_lock.rb +19 -0
  18. data/lib/aeternitas/maintenance.rb +35 -0
  19. data/lib/aeternitas/metric.rb +12 -0
  20. data/lib/aeternitas/metrics.rb +54 -136
  21. data/lib/aeternitas/poll_job.rb +135 -0
  22. data/lib/aeternitas/pollable/configuration.rb +21 -22
  23. data/lib/aeternitas/pollable/dsl.rb +16 -17
  24. data/lib/aeternitas/pollable.rb +19 -18
  25. data/lib/aeternitas/pollable_meta_data.rb +18 -9
  26. data/lib/aeternitas/polling_frequency.rb +4 -4
  27. data/lib/aeternitas/source.rb +5 -5
  28. data/lib/aeternitas/storage_adapter/file.rb +9 -12
  29. data/lib/aeternitas/storage_adapter.rb +1 -3
  30. data/lib/aeternitas/unique_job_lock.rb +15 -0
  31. data/lib/aeternitas/version.rb +1 -1
  32. data/lib/aeternitas.rb +23 -26
  33. data/lib/generators/aeternitas/install_generator.rb +14 -8
  34. data/lib/generators/aeternitas/templates/add_aeternitas.rb.erb +34 -2
  35. data/lib/generators/aeternitas/templates/initializer.rb +10 -7
  36. metadata +35 -123
  37. data/.idea/.rakeTasks +0 -7
  38. data/.idea/misc.xml +0 -4
  39. data/.idea/modules.xml +0 -8
  40. data/.idea/vcs.xml +0 -6
  41. data/.rspec +0 -2
  42. data/.rubocop.yml +0 -2
  43. data/.travis.yml +0 -8
  44. data/lib/aeternitas/metrics/counter.rb +0 -18
  45. data/lib/aeternitas/metrics/ratio.rb +0 -67
  46. data/lib/aeternitas/metrics/ten_minutes_resolution.rb +0 -40
  47. data/lib/aeternitas/metrics/values.rb +0 -18
  48. data/lib/aeternitas/sidekiq/middleware.rb +0 -31
  49. data/lib/aeternitas/sidekiq/poll_job.rb +0 -30
  50. data/lib/aeternitas/sidekiq.rb +0 -5
  51. data/logo.png +0 -0
  52. 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
- # Sidekiq queue the poll job will be enqueued in (Default: 'polling')
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 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)
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
- :before_polling,
26
- :queue,
27
- :polling_frequency,
28
- :after_polling,
29
- :guard_options,
30
- :ignored_errors,
31
- :sleep_on_guard_locked
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) { return obj.class.name.to_s },
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 = 'polling'
46
- @sleep_on_guard_locked = true
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 = 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
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
- @configuration.polling_frequency = Aeternitas::PollingFrequency.by_name(frequency)
24
+ @configuration.polling_frequency = if frequency.is_a?(Symbol)
25
+ Aeternitas::PollingFrequency.by_name(frequency)
26
26
  else
27
- @configuration.polling_frequency = frequency
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
- @configuration.before_polling << ->(pollable) { pollable.send(method) }
41
+ @configuration.before_polling << if method.is_a?(Symbol)
42
+ ->(pollable) { pollable.send(method) }
43
43
  else
44
- @configuration.before_polling << method
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
- @configuration.after_polling << ->(pollable) { pollable.send(method) }
58
+ @configuration.after_polling << if method.is_a?(Symbol)
59
+ ->(pollable) { pollable.send(method) }
60
60
  else
61
- @configuration.after_polling << method
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
- when Symbol
98
- ->(obj) { return obj.send(key) }
99
- when Proc
100
- key
101
- else
102
- ->(obj) { return key.to_s }
103
- end
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
-
@@ -1,5 +1,5 @@
1
- require 'aeternitas/pollable/configuration'
2
- require 'aeternitas/pollable/dsl'
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, 'Aeternitas::Pollable must inherit from ActiveRecord::Base' unless self.ancestors.include?(ActiveRecord::Base)
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
- dependent: :destroy,
32
- class_name: 'Aeternitas::PollableMetaData'
31
+ dependent: :destroy,
32
+ class_name: "Aeternitas::PollableMetaData"
33
33
 
34
34
  has_many :sources, as: :pollable,
35
- dependent: :destroy,
36
- class_name: 'Aeternitas::Source'
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: 'waiting')
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 StandardError => e
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 StandardError => e
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
- state: 'waiting',
95
- pollable_class: self.class.name
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 = self.sources.create(raw_content: raw_content)
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.update_attributes!(
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(self.pollable_configuration, &block)
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 'aasm'
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 = 'aeternitas_pollable_meta_data'
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: { scope: :pollable_id }
29
- validates :pollable_id, presence: true, uniqueness: { scope: :pollable_type }
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, ->() { waiting.where('next_polling < ?', Time.now) })
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
- self.deactivate
76
+ deactivate
68
77
  self.deactivation_reason = reason.to_s
69
78
  self.deactivated_at = Time.now
70
- self.save!
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 = ->(context) { Time.now + 1.hour }
5
- DAILY = ->(context) { Time.now + 1.day }
6
- WEEKLY = ->(context) { Time.now + 1.week }
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
@@ -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 = 'aeternitas_sources'
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(self.fingerprint)
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(self.fingerprint, raw_content)
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(self.fingerprint)
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, 'w+', encoding: 'ascii-8bit') do |f|
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: 'ascii-8bit'))
23
+ Zlib.inflate(::File.read(file_path(id), encoding: "ascii-8bit"))
25
24
  end
26
25
 
27
26
  def delete(id)
28
- begin
29
- !!::File.delete(file_path(id))
30
- rescue Errno::ENOENT => e
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
- @config[:directory],
61
- id[0..1], id[2..3], id[4..5],
62
- id[6..-1]
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
@@ -1,3 +1,3 @@
1
1
  module Aeternitas
2
- VERSION = "0.2.0"
2
+ VERSION = "2.0.0.rc1"
3
3
  end
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/sidekiq"
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(self.config)
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::Sidekiq::PollJob
39
+ Aeternitas::PollJob
43
40
  .set(queue: pollable_meta_data.pollable.pollable_configuration.queue)
44
- .perform_async(pollable_meta_data.id)
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 :redis, :storage_adapter, :storage_adapter_config
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) ? File.join(Rails.root, %w[aeternitas_data]) : File.join(Dir.getwd, 'aeternitas_data')
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 'Generates (but does not run) a migration to add all tables needed by Aeternitas.' \
12
- ' Also generates an initializer file for configuring Aeternitas'
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, 'add_aeternitas')
17
- ::Kernel.warn "Migration already exists: #{template}"
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('add_aeternitas.rb.erb', 'db/migrate/add_aeternitas.rb')
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('initializer.rb', 'config/initializers/aeternitas.rb')
24
+ copy_file("initializer.rb", "config/initializers/aeternitas.rb")
25
25
  end
26
26
 
27
27
  def reminder
28
- say "Don't forget to regularly run 'Aeternitas.enqueue_due_pollables'. E.g using 'whenever'", :red
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+002"
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