perform_every 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1e9c1509e5c84a94b1a0029dd2a71e5f62a81deb63f1673ac7532a2a43bfdbe4
4
+ data.tar.gz: 3c7f5acd581c44792a5dcb2577eaf2ec32d42dd36a2abbab333f8f20186ef79c
5
+ SHA512:
6
+ metadata.gz: 98636a9c1083dc8cd8ae302c1cb4c3ebd1f0b1e09cb3bd1cd0d6892d5f721f89d1e4d2ab579c74d39a6e19dc7bba9763fb8c225b2e675e1fcd3300738dd7645c
7
+ data.tar.gz: da526fc6fbb9a6029f317072953fe15df393c8238605ae5cff5b89ef1d393ed4ca8a721abf8ac883e99790c203e552e29fd7f21f6d367c00e3835e40fb681366
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Matthias Kadenbach
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # perform_every
2
+
3
+ Cron jobs for Rails. Just add `perform_every "5 minutes"` to any job.
4
+
5
+ Requires Postgres and a configured Rails' ActiveJob adapter,
6
+ like [delayed_job](https://github.com/collectiveidea/delayed_job) or
7
+ [sidekiq](https://github.com/mperham/sidekiq).
8
+
9
+
10
+ ## Usage
11
+
12
+ Include `gem 'perform_every'` and then run:
13
+
14
+ ```
15
+ bundle install
16
+ rails generate perform_every:active_record
17
+ rails db:migrate
18
+ ```
19
+
20
+ Create a new job `app/jobs/example_job.rb`:
21
+
22
+ ```ruby
23
+ class ExampleJob < ApplicationJob
24
+ queue_as :default
25
+
26
+ # multiple perform_every and perform_at are allowed
27
+ perform_every "10 minutes"
28
+ perform_at "October 1st, 2030"
29
+ perform_at "October 1st, 2050"
30
+
31
+ # This job runs every 10 minutes and on October 1st, 2030 and 2050.
32
+ # No `perform` parameters are allowed, because `perform_every` will
33
+ # just use the configured `Rails.config.active_job.queue_adapter` to
34
+ # queue this job.
35
+ def perform
36
+ User.all.each do |user|
37
+ send_cat_meme(user) #priceless
38
+ end
39
+ end
40
+ end
41
+ ```
42
+
43
+ Finally start the worker which will enqueue jobs:
44
+
45
+ ```
46
+ rails perform_every:run
47
+ ```
48
+
49
+ ---
50
+
51
+ ### `perform_every`
52
+
53
+ ```ruby
54
+ perform_every "interval", {:accuracy => 1.minute}
55
+
56
+ perform_every "day at five"
57
+ perform_every "weekday at five"
58
+ perform_every "day at 5 pm"
59
+ perform_every "tuesday at 5 pm"
60
+ perform_every "wed at 5 pm"
61
+ perform_every "day at 16:30"
62
+ perform_every "day at noon"
63
+ perform_every "day at midnight"
64
+ perform_every "tuesday"
65
+ perform_every "day at 5 pm on America/Los_Angeles"
66
+ perform_every "day at 6 pm in Asia/Tokyo"
67
+ perform_every "3 hours"
68
+ perform_every "4 months"
69
+ perform_every "5 minutes"
70
+ ```
71
+
72
+ * `interval` should be >= 1.minute
73
+ * `interval` default timezone is UTC
74
+ * `accuracy` is set to `1.minute` by default (see notes below)
75
+ * multiple unique `perform_every` can be added
76
+
77
+ ---
78
+
79
+ ### `perform_at`
80
+
81
+ ```ruby
82
+ perform_at "timestamp", {:accuracy => 1.minute}
83
+
84
+ perform_at "2017-12-12"
85
+ perform_at "2017-12-12 12:00:00 America/New_York"
86
+ perform_at "October 1st, 2050"
87
+ ```
88
+
89
+ * `timestamp` default timezone is UTC
90
+ * `accuracy` is set to `1.minute` by default (see notes below)
91
+ * multiple unique `perform_at` an be added
92
+
93
+ ---
94
+
95
+ ## Commands
96
+
97
+ ```
98
+ rails perform_every:run # Run scheduler
99
+ rails perform_every:cleanup # Remove deprecated jobs from database
100
+ rails perform_every:reset # Reset persisted jobs in database
101
+ ```
102
+
103
+
104
+ ## Notes
105
+
106
+ * Several workers (`rails perform_every:run`) can be started. During a leader election phase
107
+ one worker will become master. This is done via
108
+ [Postgres Advisory Locks](https://www.postgresql.org/docs/11/explicit-locking.html#ADVISORY-LOCKS)
109
+ and [with_advisory_lock gem](https://github.com/ClosureTree/with_advisory_lock).
110
+ An `exclusive session level advisory lock` is obtained. If the worker dies, another
111
+ worker will become master and take over.
112
+ * Workers will only enqueue jobs to your backend queue adapter.
113
+ * Workers will gracefully shutdown when SIGINT or SIGTERM is received.
114
+ * Job state is persited in Postgres in table `perform_every`.
115
+ * `perform_at` and `perform_every` statements can be added and removed between deploys,
116
+ the workers support rolling deploys. Obsolete jobs are marked as `deprecated` in table `perform_every`.
117
+ Run `rails perform_every:cleanup` after deploys to delete deprecated tasks.
118
+ * Enable `Rails.config.log_level = :debug` to output verbose logging to understand scheduling logic.
119
+ * Accuracy is set to 1 minute by default. If a job is scheduled to run at 4:00pm, the perform_every
120
+ worker has until 4:01pm to actually schedule the job.
121
+ Accuracy is important in case things go wrong.
122
+ Here is another example: Every day at 8am a job is supposed to send out email newsletters.
123
+ This can only happen between 8am and 9am. `perform_every "day at 8am", {:accuracy => 1.hour}`
124
+ ensures that if no workers are alive between 8am and 9am the newsletter job would not
125
+ be scheduled after 9:01am anymore.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'PerformEvery'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,31 @@
1
+
2
+ module PerformEvery
3
+ class ActiveRecordGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+ source_root File.expand_path("../templates", __FILE__)
6
+
7
+ def copy_migration
8
+ migration_template "migration.rb", "db/migrate/create_perform_every.rb"
9
+ end
10
+
11
+ #def generate_model
12
+ #invoke "active_record:model", ["PerformEvery"], migration: false unless model_exists? && behavior == :invoke
13
+ #end
14
+
15
+ private
16
+
17
+ # see https://stackoverflow.com/questions/11079617/next-migration-number-notimplementederror-notimplementederror-using-wysihat
18
+ def self.next_migration_number(dirname)
19
+ next_migration_number = current_migration_number(dirname) + 1
20
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
21
+ end
22
+
23
+ #def model_exists?
24
+ #File.exist?(File.join(destination_root, model_path))
25
+ #end
26
+
27
+ #def model_path
28
+ #@model_path ||= File.join("app", "models", "perform_every.rb")
29
+ #end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ class CreatePerformEvery < ActiveRecord::Migration[4.2]
2
+ def change
3
+ create_table :perform_every do |t|
4
+ t.string :job_name
5
+ t.string :typ # every|at
6
+ t.string :value
7
+ t.string :history, array: true
8
+ t.datetime :last_performed_at
9
+ t.datetime :perform_at
10
+ t.boolean :deprecated, null: false, default: false
11
+ end
12
+
13
+ add_index :perform_every, [:job_name, :typ, :value], unique: true, name: "perform_every_unique_job"
14
+ add_index :perform_every, :deprecated
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require "perform_every/helper"
2
+ require "perform_every/job"
3
+ require "perform_every/reflection"
4
+ require "perform_every/scheduler"
5
+ require "perform_every/railtie"
6
+
7
+ require 'fugit'
8
+
9
+ ActiveSupport.on_load(:active_job) do
10
+ require "perform_every/activejob"
11
+ ActiveJob::Base.send(:include, ::PerformEvery::ActiveJobExtension)
12
+ end
13
+
14
+ module PerformEvery
15
+ DEFAULT_ACCURACY = 1.minute # must be >= 1 minute
16
+
17
+ ADVISORY_LOCK_NAME = "perform_every_scheduler"
18
+ SLEEP_INTERVAL = 30 # seconds (should be dividable by 2)
19
+ MAX_HISTORY = 10
20
+
21
+ mattr_accessor :dry_run, default: false
22
+ end
@@ -0,0 +1,46 @@
1
+ module PerformEvery
2
+ module ActiveJobExtension
3
+ extend ActiveSupport::Concern
4
+
5
+ # pattern from:
6
+ # https://guides.rubyonrails.org/plugins.html#add-an-acts-as-method-to-active-record
7
+ # and https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations.rb
8
+
9
+ class_methods do
10
+ def perform_every(interval, opts={})
11
+ j = Job.new
12
+ j.job_name = self.name
13
+ j.typ = "interval"
14
+ j.value = interval.strip
15
+ j.accuracy = opts[:accuracy]
16
+
17
+ if j.value.blank?
18
+ raise "#{self.name}#perform_every needs interval"
19
+ end
20
+
21
+ # TODO raise if perform method has parameters
22
+ # Object.const_get(self.name).instance_method(:perform).parameters.flatten.count
23
+
24
+ PerformEvery::Reflection.insert(j)
25
+ end
26
+
27
+ def perform_at(timestamp, opts={})
28
+ j = Job.new
29
+ j.job_name = self.name
30
+ j.typ = "timestamp"
31
+ j.value = timestamp.strip
32
+ j.accuracy = opts[:accuracy]
33
+
34
+ if j.value.blank?
35
+ raise "#{self.name}#perform_at needs timestamp"
36
+ end
37
+
38
+ # TODO raise if perform method has parameters
39
+ # Object.const_get(self.name).instance_method(:perform).parameters.flatten.count
40
+
41
+ PerformEvery::Reflection.insert(j)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,9 @@
1
+ module PerformEvery
2
+ module Helper
3
+ include ActionView::Helpers::DateHelper
4
+
5
+ def distance(a, b)
6
+ distance_of_time_in_words(a, b)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,170 @@
1
+ require 'action_view'
2
+
3
+ module PerformEvery
4
+ class Job < ActiveRecord::Base
5
+ include PerformEvery::Helper
6
+
7
+ self.table_name = "perform_every"
8
+
9
+ attr_accessor :accuracy
10
+
11
+ def title
12
+ return "#{self.job_name} every #{self.value}" if self.typ == "interval"
13
+ return "#{self.job_name} at #{self.value}" if self.typ == "timestamp"
14
+ raise "unknown typ"
15
+ end
16
+
17
+ def from_reflection_store(attr)
18
+ s = PerformEvery::Reflection.find(self)
19
+ return nil if s.nil?
20
+ s.send(attr.to_sym)
21
+ end
22
+
23
+ def perform?(now = Time.now.utc)
24
+ return false if self.deprecated
25
+ return false if self.perform_at.blank?
26
+ return false if self.too_old?(now)
27
+
28
+ now >= self.perform_at && (self.last_performed_at.blank? || self.last_performed_at < self.perform_at)
29
+ end
30
+
31
+ def too_old?(now = Time.now.utc)
32
+ raise "job is not scheduled to run because perform_at is nil" if self.perform_at.blank?
33
+ accuracy = self.from_reflection_store(:accuracy) || PerformEvery::DEFAULT_ACCURACY
34
+ now - self.perform_at >= accuracy
35
+ end
36
+
37
+ def perform!(now = Time.now.utc)
38
+ return :skip_deprecated if self.perform_at.blank? || self.deprecated
39
+
40
+ if self.too_old?(now)
41
+ rescue_perform_at = self.perform_at
42
+ self.perform_at = self.perform_once? ? nil : self.perform_next_at
43
+ self.deprecated = self.perform_once?
44
+ self.save!
45
+
46
+ # prepare debug log
47
+ log = []
48
+ log << "'#{self.title}' was skipped."
49
+ log << "It was scheduled for #{rescue_perform_at} but now it's #{distance(now, rescue_perform_at)} too late to still run the job."
50
+ if self.perform_multi?
51
+ log << "The job is scheduled to perform next in #{distance(now, self.perform_at)} at #{self.perform_at}."
52
+ else
53
+ log << "This one-time job will not be scheduled again."
54
+ end
55
+ Rails.logger.error log.join(" ")
56
+
57
+ return :skip_too_old
58
+ end
59
+
60
+ if !self.perform?(now)
61
+ # prepare debug log
62
+ log = []
63
+ log << "'#{self.title}' was skipped."
64
+ perform_next_str = ""
65
+ unless self.last_performed_at.blank?
66
+ log << "It ran #{distance(now, self.last_performed_at)} ago."
67
+ perform_next_str = "next"
68
+ else
69
+ perform_next_str = self.perform_once? ? "once" : "for the first time"
70
+ end
71
+ unless self.perform_at.blank?
72
+ log << "The job is scheduled to perform #{perform_next_str} in #{distance(now, self.perform_at)} at #{self.perform_at}."
73
+ end
74
+ Rails.logger.debug log.join(" ")
75
+
76
+ return :skip
77
+ end
78
+
79
+ # call the actual job
80
+ schedule_error = nil
81
+ unless PerformEvery.dry_run
82
+ begin
83
+ Object.const_get(self.job_name).send(:perform_now)
84
+ rescue => e
85
+ schedule_error = e
86
+ end
87
+ end
88
+
89
+ # prepare debug log
90
+ log = []
91
+ unless schedule_error
92
+ log << "'#{self.title}' was scheduled."
93
+ else
94
+ log << "'#{self.title}' failed with error: #{schedule_error}."
95
+ end
96
+ if self.perform_multi?
97
+ log << "The job is scheduled to perform next in #{distance(now, self.perform_next_at)} at #{self.perform_next_at}."
98
+ else
99
+ log << "This one-time job will not be scheduled again."
100
+ end
101
+
102
+ unless schedule_error
103
+ Rails.logger.debug log.join(" ")
104
+ else
105
+ Rails.logger.error log.join(" ")
106
+ end
107
+
108
+ # log warning if job is performed with more than 1 minute delay
109
+ if now - self.perform_at > 1.minute
110
+ Rails.logger.warn "'#{self.title}' was run with a delay of #{distance(now, self.perform_at)}."
111
+ end
112
+
113
+ self.last_performed_at = now.utc
114
+ self.add_history(self.last_performed_at)
115
+ self.perform_at = self.perform_once? ? nil : self.perform_next_at
116
+ self.deprecated = self.perform_once?
117
+ self.save!
118
+
119
+ return schedule_error.blank? ? :perform : :error
120
+ end
121
+
122
+ def add_history(t = Time.now.utc)
123
+ self.history ||= []
124
+ self.history << t.to_s
125
+ self.history.shift(self.history.count - MAX_HISTORY) if self.history.count > MAX_HISTORY
126
+ end
127
+
128
+ def perform_next_at
129
+ if self.typ == "interval"
130
+ self.parse_interval_value.next_time.utc
131
+ elsif self.typ == "timestamp"
132
+ self.parse_timestamp_value.utc
133
+ else
134
+ raise "unknown typ"
135
+ end
136
+ end
137
+
138
+ def parse_interval_value
139
+ raise "must be interval" if self.value.blank?
140
+ interval = ::Fugit::Nat.parse("every " + self.value, multi: :fail)
141
+ raise "must be interval" if interval.blank? || !interval.is_a?(::Fugit::Cron)
142
+ return interval
143
+ end
144
+
145
+ def parse_timestamp_value
146
+ raise "must be timestamp" if self.value.blank?
147
+ timestamp = ::Fugit::At.parse(self.value)
148
+ raise "must be timestamp" if timestamp.blank? || !timestamp.is_a?(::EtOrbi::EoTime)
149
+ return timestamp
150
+ end
151
+
152
+ def == j
153
+ self.job_name == j.job_name && self.typ == j.typ && self.value == j.value
154
+ end
155
+
156
+ def perform_once?
157
+ self.typ == "timestamp"
158
+ end
159
+
160
+ def perform_multi?
161
+ self.typ == "interval"
162
+ end
163
+
164
+ def mark_deprecated!
165
+ self.deprecated = true
166
+ self.save!
167
+ end
168
+
169
+ end
170
+ end
@@ -0,0 +1,7 @@
1
+ module PerformEvery
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/perform_every_tasks.rake"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ module PerformEvery
2
+
3
+ module Reflection
4
+ mattr_reader :store, default: []
5
+
6
+ # insert into store, ignoring duplicates
7
+ def self.insert(job)
8
+ return false if @@store.include?(job)
9
+ @@store << job
10
+ true
11
+ end
12
+
13
+ def self.find(job)
14
+ i = @@store.index(job)
15
+ return nil if i.nil?
16
+ @@store[i]
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,101 @@
1
+ require 'with_advisory_lock'
2
+
3
+ module PerformEvery
4
+ class Scheduler
5
+
6
+ def run_forever
7
+ Zeitwerk::Loader.eager_load_all # make sure all jobs are loaded
8
+
9
+ # trap SIGINT and SIGTERM signals for clean shutdown
10
+ kill = false
11
+ Signal.trap("INT") {|s| kill = true }
12
+ Signal.trap("TERM") {|s| kill = true }
13
+
14
+ # try to continuously acquire advisory lock so that only one worker
15
+ # at a time will schedule jobs. wait 5 seconds for lock, then try again after 30 seconds.
16
+ loop do
17
+ Rails.logger.info "Leader election: waiting to become master ..."
18
+ ActiveRecord::Base.with_advisory_lock(ADVISORY_LOCK_NAME, timeout_seconds: 5) do
19
+ Rails.logger.info "Leader election: I'm the master!"
20
+
21
+ # persist new jobs in the database
22
+ local_jobs_count = Scheduler.persist_jobs
23
+ Rails.logger.info "Found #{local_jobs_count} job/s in local files"
24
+
25
+ metrics = {}
26
+
27
+ at_exit do
28
+ Rails.logger.info "#{metrics}" unless metrics.blank?
29
+ Rails.logger.info "Bye"
30
+ end
31
+
32
+ # start endless loop
33
+ loop do
34
+ Rails.logger.info "Running scheduler ..."
35
+
36
+ # handle all jobs and schedule job if it's about time
37
+ jobs = Job.where(:deprecated => false)
38
+ jobs.each do |job|
39
+
40
+
41
+ # check if job is still present in local job files
42
+ if Reflection.store.include?(job)
43
+ op = job.perform!
44
+ metrics[op] ||= 0
45
+ metrics[op] += 1
46
+ else
47
+ job.mark_deprecated!
48
+ end
49
+
50
+ return if kill
51
+ end
52
+
53
+ metrics[:total_jobs] = jobs.count
54
+ Rails.logger.info "#{metrics}"
55
+ metrics = {}
56
+
57
+ if local_jobs_count > jobs.count + Job.where(:deprecated => true).count
58
+ Rails.logger.warn "Unpersisted jobs found. Will retry to persist."
59
+ Scheduler.persist_jobs
60
+ end
61
+
62
+ # go sleeping for SLEEP_INTERVAL and keep watching for kill commands
63
+ (SLEEP_INTERVAL / 2).times do
64
+ return if kill
65
+ sleep 2 # seconds
66
+ end
67
+ end
68
+ end
69
+
70
+ # sleep for 30 seconds, keep watching for kill commands
71
+ 15.times do
72
+ return if kill
73
+ sleep 2
74
+ end
75
+
76
+ end # /loop around with_advisory_lock
77
+ end
78
+
79
+ private
80
+
81
+ # insert new jobs to database
82
+ def self.persist_jobs
83
+ return 0 if Reflection.store.blank?
84
+ Job.insert_all(Reflection.store.map{|j| {
85
+ job_name: j.job_name,
86
+ typ: j.typ,
87
+ value: j.value,
88
+ perform_at: j.perform_next_at} })
89
+ Reflection.store.count
90
+ end
91
+
92
+ def self.cleanup_deprecated_jobs
93
+ Job.where(:deprecated => true).delete_all
94
+ end
95
+
96
+ def self.reset_jobs
97
+ Job.connection.truncate(Job.table_name)
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module PerformEvery
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,19 @@
1
+ namespace :perform_every do
2
+
3
+ desc "Run scheduler"
4
+ task run: :environment do
5
+ s = PerformEvery::Scheduler.new
6
+ s.run_forever
7
+ end
8
+
9
+ desc "Remove deprecated jobs"
10
+ task cleanup: :environment do
11
+ PerformEvery::Scheduler.cleanup_deprecated_jobs
12
+ end
13
+
14
+ desc "Reset jobs"
15
+ task reset: :environment do
16
+ PerformEvery::Scheduler.reset_jobs
17
+ end
18
+
19
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perform_every
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthias Kadenbach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.2
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.0.2.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.2
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.0.2.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: fugit
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: with_advisory_lock
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: pg
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 1.2.2
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 1.2.2
75
+ - !ruby/object:Gem::Dependency
76
+ name: byebug
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '11.1'
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 11.1.1
85
+ type: :development
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '11.1'
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 11.1.1
95
+ description: Runs jobs at specified intervals.
96
+ email:
97
+ - matthias.kadenbach@gmail.com
98
+ executables: []
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - LICENSE
103
+ - README.md
104
+ - Rakefile
105
+ - lib/generators/perform_every/active_record_generator.rb
106
+ - lib/generators/perform_every/templates/migration.rb
107
+ - lib/perform_every.rb
108
+ - lib/perform_every/activejob.rb
109
+ - lib/perform_every/helper.rb
110
+ - lib/perform_every/job.rb
111
+ - lib/perform_every/railtie.rb
112
+ - lib/perform_every/reflection.rb
113
+ - lib/perform_every/scheduler.rb
114
+ - lib/perform_every/version.rb
115
+ - lib/tasks/perform_every_tasks.rake
116
+ homepage: https://github.com/mattes/perform_every
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 3.1.2
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Cron for ActiveJob
139
+ test_files: []