cron_swanson 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f5b92fde06c654c2a37b0162a87e0d1701db2d24f28ae707bbfa174d168d73aa
4
+ data.tar.gz: 33d169cfefdb9e1ab75aca3b93f2aedfc2f6c0d6634df1f9a536115b5a964bc9
5
+ SHA512:
6
+ metadata.gz: abd309004421dc9cf12886a4e9df6a58717ab51dc74c2cee0ab4fbdbbb4cb4248b462d4d684f6113a0103a8999559eac2cb51580467c6c5ed18b9ac14d73a1b6
7
+ data.tar.gz: af381bd1b0054755f8a08248962f546db9ce82ea0706657ace6f47fc5c9aa0425266de8295ee39bfe523bfc2b78e3d3243aa94c1eb8a49edd40ac355f7296557
@@ -0,0 +1,12 @@
1
+ Gemfile.lock
2
+ /.bundle/
3
+ /.byebug_history
4
+ /.rubocop-http*
5
+ /.yardoc
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/examples.txt
11
+ /spec/reports/
12
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,3 @@
1
+ inherit_from:
2
+ - https://raw.githubusercontent.com/tedconf/code-style-guides/master/linters/rubocop/rubocop.yml
3
+
@@ -0,0 +1 @@
1
+ 2.5.5
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'byebug'
6
+ gem 'guard', '~> 2.13'
7
+ gem 'guard-rspec', '~> 4.6'
8
+ gem 'rspec', '~> 3.9'
9
+ gem 'rubocop', '~> 0.67.0'
10
+ gem 'whenever'
@@ -0,0 +1,16 @@
1
+ guard :rspec, cmd: "bundle exec rspec" do
2
+ require "guard/rspec/dsl"
3
+ dsl = Guard::RSpec::Dsl.new(self)
4
+
5
+ # Feel free to open issues for suggestions and improvements
6
+
7
+ # RSpec files
8
+ rspec = dsl.rspec
9
+ watch(rspec.spec_helper) { rspec.spec_dir }
10
+ watch(rspec.spec_support) { rspec.spec_dir }
11
+ watch(rspec.spec_files)
12
+
13
+ # Ruby files
14
+ ruby = dsl.ruby
15
+ dsl.watch_spec_files_for(ruby.lib_files)
16
+ end
@@ -0,0 +1,106 @@
1
+ # CronSwanson
2
+
3
+ CronSwanson can help if you have many systems running the same cron job, but you
4
+ don't want them all to start at exactly the same time. (To prevent daily load
5
+ spikes at midnight, as every single app starts its maintenance cron jobs.)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'cron_swanson'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install cron_swanson
22
+
23
+ ## Usage
24
+
25
+ ### schedule
26
+
27
+ Use `schedule` to build a cron schedule. The supplied string is hashed to determine
28
+ the run time of the job.
29
+
30
+ ```ruby
31
+ CronSwanson.schedule 'whiskey'
32
+ #=> "33 18 * * *"
33
+ ```
34
+
35
+ An `interval` (in seconds) can be supplied if you want a job to be run more than
36
+ once/day. This `interval` must be a factor of 24 hours.
37
+
38
+ ```ruby
39
+ CronSwanson.schedule 'bacon', interval: 60 * 60 * 4
40
+ #=> "26 2,6,10,14,18,22 * * *"
41
+ ```
42
+
43
+ ### Whenever Integration
44
+
45
+ `CronSwanson` is built to integrate with the fantastic [whenever](https://github.com/javan/whenever) gem.
46
+
47
+ #### Daily
48
+
49
+ ```ruby
50
+ # in the config/schedule.rb file
51
+ CronSwanson::Whenever.add(self) do
52
+ rake 'sample:job'
53
+ end
54
+ ```
55
+
56
+ This will result in `rake sample:job` being scheduled once per day, at a time
57
+ determined by `CronSwanson`.
58
+
59
+ #### Multiple times/day
60
+
61
+ ```ruby
62
+ # in the config/schedule.rb file
63
+
64
+ # with ActiveSupport
65
+ CronSwanson::Whenever.add(self, interval: 4.hours) do
66
+ rake 'sample:job'
67
+ end
68
+
69
+ # without ActiveSupport
70
+ CronSwanson::Whenever.add(self, interval: 60 * 60 * 4) do
71
+ rake 'sample:job'
72
+ end
73
+ ```
74
+
75
+ #### job types
76
+
77
+ The block is evaluated by `whenever`, so any custom job types will work.
78
+
79
+ ```ruby
80
+ # in config/schedule.rb
81
+ job_type :ron, '/usr/bin/ron :task'
82
+
83
+ CronSwanson::Whenever.add(self) do
84
+ ron 'bacon whiskey'
85
+ end
86
+ ```
87
+
88
+ #### Limitation
89
+
90
+ The whenever integration code currently derives a scheduled time from the source
91
+ location of the `add` call. This means that moving the `.add` invocation to
92
+ a different line in schedule.rb will cause it to be run at a different time.
93
+
94
+ This limitation exists because I (currently) don't know of a way to inspect
95
+ the contents of a block at runtime. If a way to do this can be found, I
96
+ would prefer to calculate the time based on the block's contents.
97
+
98
+ ## Development
99
+
100
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/alexdean/cron_swanson.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task default: :spec
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application '_guard-core' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("guard", "_guard-core")
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cron_swanson"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'guard' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("guard", "guard")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,27 @@
1
+ # lib = File.expand_path("../lib", __FILE__)
2
+ # $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ $:.push File.expand_path('../lib', __FILE__)
4
+ require "cron_swanson/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cron_swanson"
8
+ spec.version = CronSwanson::VERSION
9
+ spec.authors = ["Alex Dean"]
10
+ spec.email = ["github@mostlyalex.com"]
11
+
12
+ spec.summary = "distribute common cron jobs so they don't all start at the same moment"
13
+ spec.description = "easily schedule multiple apps to run the same job at different times"
14
+ spec.homepage = "https://github.com/alexdean/cron_swanson"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.17"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
@@ -0,0 +1,63 @@
1
+ require "cron_swanson/version"
2
+ require "cron_swanson/whenever"
3
+ require 'digest/sha2'
4
+
5
+ # CronSwanson is a utility to generate run times for cron jobs which are
6
+ # regular, but which vary per job.
7
+ module CronSwanson
8
+ SECONDS_PER_HOUR = 60 * 60
9
+ SECONDS_PER_DAY = SECONDS_PER_HOUR * 24
10
+
11
+ def self.default_interval
12
+ SECONDS_PER_DAY
13
+ end
14
+
15
+ # offset within a time period
16
+ #
17
+ # if the interval is 6 hours, the returned offset will be some number of seconds
18
+ # from 0 to 6 hours.
19
+ #
20
+ # @param [String] job_identifier
21
+ # if nil, method will determine this on its own
22
+ # @param [Integer] interval how often will the job be run?
23
+ # @return [Integer] number of seconds to offset this job
24
+ def self.offset(job_identifier, interval: default_interval)
25
+ sha = Digest::SHA256.hexdigest(job_identifier.to_s)
26
+
27
+ # largest possible hex sha256 value
28
+ max_sha256_value = (16**64).to_f
29
+
30
+ # what % of the max sha256 is the current app?
31
+ sha_pct_of_max_sha256 = sha.to_i(16) / max_sha256_value
32
+
33
+ # apply that same % to the desired interval to get an offset
34
+ offset_seconds = (sha_pct_of_max_sha256 * interval).round
35
+
36
+ offset_seconds
37
+ end
38
+
39
+ def self.schedule(job_identifier, interval: default_interval)
40
+ if interval > SECONDS_PER_DAY
41
+ raise ArgumentError, "interval must be less than 1 day (#{SECONDS_PER_DAY} seconds)."
42
+ end
43
+
44
+ if SECONDS_PER_DAY % interval != 0
45
+ raise ArgumentError, "A day (#{SECONDS_PER_DAY} seconds) must be evenly " \
46
+ "divisible by the given interval."
47
+ end
48
+
49
+ # figure out how many times job will happen in a day
50
+ runs_per_day = SECONDS_PER_DAY / interval
51
+
52
+ # raise if runs_per_day has a decimal component.
53
+
54
+ run_at = Time.at(offset(job_identifier, interval: interval)).utc
55
+
56
+ hours = []
57
+ runs_per_day.times do |i|
58
+ hours << run_at.hour + (i * interval / SECONDS_PER_HOUR)
59
+ end
60
+
61
+ "#{run_at.min} #{hours.join(',')} * * *"
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module CronSwanson
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,51 @@
1
+ module CronSwanson
2
+ # integration for the whenever gem: https://github.com/javan/whenever
3
+ module Whenever
4
+ # CronSwanson integration for whenever
5
+ #
6
+ # The given block can use any job types understood by your whenever configuration.
7
+ # See https://github.com/javan/whenever#define-your-own-job-types.
8
+ #
9
+ # CronSwanson currently uses the location it is invoked from in schedule.rb
10
+ # to calculate a job time. This means that moving the `.add` invocation to
11
+ # a different line in schedule.rb will cause it to be run at a different time.
12
+ #
13
+ # This limitation exists because I (currently) don't know of a way to inspect
14
+ # the contents of a block at runtime. If a way to do this can be found, I
15
+ # would prefer to calculate the time based on the block's contents.
16
+ #
17
+ # @example run a job once/day
18
+ # # in the config/schedule.rb file
19
+ # CronSwanson::Whenever.add(self) do
20
+ # rake 'job'
21
+ # end
22
+ #
23
+ # @example run a job four times daily
24
+ # # in the config/schedule.rb file
25
+ #
26
+ # # with ActiveSupport
27
+ # CronSwanson::Whenever.add(self, interval: 4.hours) do
28
+ # rake 'job'
29
+ # end
30
+ #
31
+ # # without ActiveSupport
32
+ # CronSwanson::Whenever.add(self, interval: 60 * 60 * 4) do
33
+ # rake 'job'
34
+ # end
35
+ #
36
+ # @param [Whenever::JobList] whenever_job_list For code in `config/schedule.rb`
37
+ # this can be referred to as `self`.
38
+ # @param [Integer] interval how many seconds do you want between runs of this job
39
+ def self.add(whenever_job_list, interval: CronSwanson.default_interval, &block)
40
+ if !whenever_job_list.is_a?(::Whenever::JobList)
41
+ raise ArgumentError, "supply a Whenever::JobList. (In schedule.rb code, use `self`.)"
42
+ end
43
+
44
+ raise ArgumentError, "provide a block containing jobs to schedule." if !block_given?
45
+
46
+ # TODO: ideally we'd hash the contents of the block, not the location it was defined at
47
+ schedule = CronSwanson.schedule(block.source_location, interval: interval)
48
+ whenever_job_list.every(schedule, &Proc.new)
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cron_swanson
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Dean
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-11-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.17'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.17'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: easily schedule multiple apps to run the same job at different times
42
+ email:
43
+ - github@mostlyalex.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".rspec"
50
+ - ".rubocop.yml"
51
+ - ".ruby-version"
52
+ - Gemfile
53
+ - Guardfile
54
+ - README.md
55
+ - Rakefile
56
+ - bin/_guard-core
57
+ - bin/console
58
+ - bin/guard
59
+ - bin/rspec
60
+ - bin/setup
61
+ - cron_swanson.gemspec
62
+ - lib/cron_swanson.rb
63
+ - lib/cron_swanson/version.rb
64
+ - lib/cron_swanson/whenever.rb
65
+ homepage: https://github.com/alexdean/cron_swanson
66
+ licenses: []
67
+ metadata: {}
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 2.7.6.2
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: distribute common cron jobs so they don't all start at the same moment
88
+ test_files: []