4 | Dummy
5 | <%= csrf_meta_tags %>
6 |
7 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
8 | <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | appraise "rails-7" do
4 | gem "activejob", "~> 7.2"
5 | gem "sidekiq", "~> 7.0"
6 | end
7 |
8 | appraise "rails-8" do
9 | gem "activejob", "~> 8.0.0"
10 | gem "sidekiq", "~> 8.0"
11 | end
12 |
13 | appraise "rails-8-1" do
14 | gem "activejob", "~> 8.1.0"
15 | gem "sidekiq", "~> 8.0"
16 | end
17 |
18 | appraise "latest" do
19 | gem "activejob"
20 | gem "sidekiq"
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/cable.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the rails generate channel command.
3 | //
4 | //= require action_cable
5 | //= require_self
6 | //= require_tree ./channels
7 |
8 | (function() {
9 | this.App || (this.App = {});
10 |
11 | App.cable = ActionCable.createConsumer();
12 |
13 | }).call(this);
14 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/spec/coverage_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "simplecov"
4 | require "simplecov-rcov"
5 |
6 | # return non-zero if not met
7 | SimpleCov.at_exit do
8 | SimpleCov.minimum_coverage 100
9 | SimpleCov.result.format!
10 | end
11 |
12 | SimpleCov.start do
13 | add_filter "lib/simple_scheduler/railtie"
14 | add_filter "/spec/"
15 | end
16 |
17 | # Format the reports in a way I like
18 | SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
19 |
--------------------------------------------------------------------------------
/lib/tasks/simple_scheduler_tasks.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | desc "Queue future jobs defined using Simple Scheduler"
4 | task simple_scheduler: :environment do
5 | SimpleScheduler::SchedulerJob.perform_now
6 | end
7 |
8 | namespace :simple_scheduler do
9 | desc "Delete existing scheduled jobs and queue them from scratch"
10 | task reset: :environment do
11 | SimpleScheduler::FutureJob.delete_all
12 | SimpleScheduler::SchedulerJob.perform_now
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "action_controller/railtie"
4 | require "rails/test_unit/railtie"
5 |
6 | Bundler.require(*Rails.groups)
7 | require "simple_scheduler"
8 |
9 | module Dummy
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | Exclude:
3 | - "spec/dummy/**/*"
4 | - "vendor/bundle/**/*"
5 | - "gemfiles/**/*"
6 | NewCops: enable
7 | TargetRubyVersion: 3.2
8 |
9 | Gemspec/DevelopmentDependencies:
10 | EnforcedStyle: gemspec
11 |
12 | Layout/FirstArrayElementIndentation:
13 | Enabled: false
14 |
15 | Layout/LineLength:
16 | Max: 120
17 | Exclude:
18 | - "spec/simple_scheduler/task_spec.rb"
19 |
20 | Metrics/BlockLength:
21 | Exclude:
22 | - "spec/**/*"
23 |
24 | Style/StringLiterals:
25 | EnforcedStyle: double_quotes
26 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Declare your gem's dependencies in simple_scheduler.gemspec.
6 | # Bundler will treat runtime dependencies like base dependencies, and
7 | # development dependencies will be added by default to the :development group.
8 | gemspec
9 |
10 | # Declare any dependencies that are still in development here instead of in
11 | # your gemspec. These might include edge Rails or gems from your path or
12 | # Git. Remember to move these dependencies to your gemspec before releasing
13 | # your gem to rubygems.org.
14 |
15 | # To use a debugger
16 | # gem 'byebug', group: [:development, :test]
17 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/spec/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file. JavaScript code in this file should be added after the last require_* statement.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is copied to spec/ when you run 'rails generate rspec:install'
4 | ENV["RAILS_ENV"] ||= "test"
5 | require "coverage_helper"
6 | require File.expand_path("../spec/dummy/config/environment.rb", __dir__)
7 | # Prevent database truncation if the environment is production
8 | abort("The Rails environment is running in production mode!") if Rails.env.production?
9 | require "spec_helper"
10 | require "rspec/rails"
11 | require "sidekiq/testing"
12 |
13 | RSpec.configure do |config|
14 | config.include ActiveJob::TestHelper
15 | config.include ActiveSupport::Testing::Assertions
16 | config.include ActiveSupport::Testing::TimeHelpers
17 | config.filter_rails_from_backtrace!
18 |
19 | ActiveJob::Base.logger = Rails.logger
20 | end
21 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - master
7 | workflow_call:
8 |
9 | jobs:
10 | tests:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | ruby: [3.2.9, 3.3.10, 3.4.7]
16 | env:
17 | RAILS_ENV: test
18 | steps:
19 | - uses: actions/checkout@v5
20 |
21 | - uses: ruby/setup-ruby@v1
22 | with:
23 | ruby-version: ${{ matrix.ruby }}
24 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
25 |
26 | - uses: supercharge/redis-github-action@1.8.0
27 |
28 | - name: Appraisal Install
29 | run: bundle exec appraisal install
30 |
31 | - name: Rubocop
32 | run: bundle exec rubocop
33 |
34 | - name: Rspec
35 | run: bundle exec appraisal rspec
36 |
--------------------------------------------------------------------------------
/spec/dummy/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a way to update your development environment automatically.
15 | # Add necessary update steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | puts "\n== Updating database =="
22 | system! 'bin/rails db:migrate'
23 |
24 | puts "\n== Removing old logs and tempfiles =="
25 | system! 'bin/rails log:clear tmp:clear'
26 |
27 | puts "\n== Restarting application server =="
28 | system! 'bin/rails restart'
29 | end
30 |
--------------------------------------------------------------------------------
/spec/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 9a9bad6c0223f7c2ceea86dc88f89fb937dddcaf96a7cad962531c5dc2ed4205292d2dfce3dd50d89da1a88f134d4611bf53640c0552c7f99754a8582b9248ae
15 |
16 | test:
17 | secret_key_base: d1df70a21d97a010cf817b37f628c14449420253de7063b37fee5990ddfdc8fd5a2d39dba3b73ad8a7704c3ac4686d6d5f4c1b6575e73e62cdf807265d6d79f6
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/spec/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a starting point to setup your application.
15 | # Add necessary setup steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | # puts "\n== Copying sample files =="
22 | # unless File.exist?('config/database.yml')
23 | # cp 'config/database.yml.sample', 'config/database.yml'
24 | # end
25 |
26 | puts "\n== Preparing database =="
27 | system! 'bin/rails db:setup'
28 |
29 | puts "\n== Removing old logs and tempfiles =="
30 | system! 'bin/rails log:clear tmp:clear'
31 |
32 | puts "\n== Restarting application server =="
33 | system! 'bin/rails restart'
34 | end
35 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016 Brian Pattison
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/lib/simple_scheduler.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_job"
4 | require "sidekiq/api"
5 | require_relative "simple_scheduler/at"
6 | require_relative "simple_scheduler/future_job"
7 | require_relative "simple_scheduler/railtie"
8 | require_relative "simple_scheduler/scheduler_job"
9 | require_relative "simple_scheduler/task"
10 | require_relative "simple_scheduler/version"
11 |
12 | # Module for scheduling jobs at specific times using Sidekiq.
13 | module SimpleScheduler
14 | # Used by a Rails initializer to handle expired tasks.
15 | # SimpleScheduler.expired_task do |exception|
16 | # ExceptionNotifier.notify_exception(
17 | # exception,
18 | # data: {
19 | # task: exception.task.name,
20 | # scheduled: exception.scheduled_time,
21 | # actual: exception.run_time
22 | # }
23 | # )
24 | # end
25 | def self.expired_task(&block)
26 | expired_task_blocks << block
27 | end
28 |
29 | # Blocks that should be called when a task doesn't run because it has expired.
30 | # @return [Array]
31 | def self.expired_task_blocks
32 | @expired_task_blocks ||= []
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/SECURITY.MD:
--------------------------------------------------------------------------------
1 | # Reporting Security Issues
2 |
3 | We value the security community and believe that responsible disclosure of security vulnerabilities in open source packages helps us ensure the security and privacy of the users.
4 |
5 | If you believe you have found a security vulnerability, we encourage you to let us know right away. We will investigate all legitimate reports and do our best to quickly fix the problem. Before reporting though, please review our responsible disclosure policy, and those things that should not be reported.
6 |
7 | Submit your report to help@simplymadeapps.com (one issue per report) and respond to the report with any updates. Please do not contact employees directly or through other channels about a report.
8 |
9 | Report security bugs in third-party modules to the person or team maintaining the module.
10 |
11 |
12 | ### Responsible Disclosure Policy
13 |
14 | We ask that:
15 |
16 | * You give us reasonable time to investigate and mitigate an issue you report before making public any information about the report or sharing such information with others.
17 | * You do not exploit a security issue you discover for any reason. (This includes demonstrating additional risk, such as attempted compromise of sensitive company data or probing for additional issues).
18 | * You do not violate any other applicable laws or regulations.
19 |
--------------------------------------------------------------------------------
/simple_scheduler.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | $LOAD_PATH.push File.expand_path("lib", __dir__)
4 |
5 | # Maintain your gem's version:
6 | require "simple_scheduler/version"
7 |
8 | # Describe your gem and declare its dependencies:
9 | Gem::Specification.new do |s|
10 | s.name = "simple_scheduler"
11 | s.version = SimpleScheduler::VERSION
12 | s.authors = ["Brian Pattison"]
13 | s.email = ["brian@brianpattison.com"]
14 | s.homepage = "https://github.com/simplymadeapps/simple_scheduler"
15 | s.summary = "An enhancement for Heroku Scheduler + Sidekiq for scheduling jobs at specific times."
16 | s.description = <<-DESCRIPTION
17 | Simple Scheduler adds the ability to enhance Heroku Scheduler by using Sidekiq to queue
18 | jobs in the future. This allows for defining specific run times (Ex: Every Sunday at 4 AM)
19 | and running tasks more often than Heroku Scheduler's 10 minute limit.
20 | DESCRIPTION
21 | s.license = "MIT"
22 |
23 | s.metadata["rubygems_mfa_required"] = "true"
24 | s.required_ruby_version = ">= 3.2.0"
25 |
26 | s.files = Dir["{lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
27 |
28 | s.add_dependency "activejob", ">= 7.2"
29 | s.add_dependency "sidekiq", ">= 7.0"
30 |
31 | s.add_development_dependency "appraisal"
32 | s.add_development_dependency "rainbow"
33 | s.add_development_dependency "rspec-rails", "~> 8"
34 | s.add_development_dependency "rubocop", "~> 1.28"
35 | s.add_development_dependency "simplecov"
36 | s.add_development_dependency "simplecov-rcov"
37 | end
38 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | # config.public_file_server.enabled = true
17 | # config.public_file_server.headers = {
18 | # 'Cache-Control' => 'public, max-age=3600'
19 | # }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 |
31 | # Print deprecation notices to the stderr.
32 | config.active_support.deprecation = :stderr
33 |
34 | # Raises error for missing translations
35 | # config.action_view.raise_on_missing_translations = true
36 | end
37 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
4 | RSpec.configure do |config|
5 | # rspec-expectations config goes here. You can use an alternate
6 | # assertion/expectation library such as wrong or the stdlib/minitest
7 | # assertions if you prefer.
8 | config.expect_with :rspec do |expectations|
9 | # This option will default to `true` in RSpec 4. It makes the `description`
10 | # and `failure_message` of custom matchers include text for helper methods
11 | # defined using `chain`, e.g.:
12 | # be_bigger_than(2).and_smaller_than(4).description
13 | # # => "be bigger than 2 and smaller than 4"
14 | # ...rather than:
15 | # # => "be bigger than 2"
16 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
17 | end
18 |
19 | # rspec-mocks config goes here. You can use an alternate test double
20 | # library (such as bogus or mocha) by changing the `mock_with` option here.
21 | config.mock_with :rspec do |mocks|
22 | # Prevents you from mocking or stubbing a method that does not exist on
23 | # a real object. This is generally recommended, and will default to
24 | # `true` in RSpec 4.
25 | mocks.verify_partial_doubles = true
26 | end
27 |
28 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
29 | # have no way to turn it off -- the option exists only for backwards
30 | # compatibility in RSpec 3). It causes shared context metadata to be
31 | # inherited by the metadata hash of host groups and examples, rather than
32 | # triggering implicit auto-inclusion in groups with matching metadata.
33 | config.shared_context_metadata_behavior = :apply_to_host_groups
34 | end
35 |
--------------------------------------------------------------------------------
/spec/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/spec/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join('tmp/caching-dev.txt').exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.cache_store = :memory_store
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => 'public, max-age=172800'
22 | }
23 | else
24 | config.action_controller.perform_caching = false
25 |
26 | config.cache_store = :null_store
27 | end
28 |
29 | # Print deprecation notices to the Rails logger.
30 | config.active_support.deprecation = :log
31 |
32 | # Raise an error on page load if there are pending migrations.
33 | config.active_record.migration_error = :page_load
34 |
35 | # Debug mode disables concatenation and preprocessing of assets.
36 | # This option may cause significant delays in view rendering with a large
37 | # number of complex assets.
38 | config.assets.debug = true
39 |
40 | # Suppress logger output for asset requests.
41 | config.assets.quiet = true
42 |
43 | # Raises error for missing translations
44 | # config.action_view.raise_on_missing_translations = true
45 |
46 | # Use an evented file watcher to asynchronously detect changes in source code,
47 | # routes, locales, etc. This feature depends on the listen gem.
48 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
49 | end
50 |
--------------------------------------------------------------------------------
/lib/simple_scheduler/scheduler_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SimpleScheduler
4 | # Active Job class that queues jobs defined in the config file.
5 | class SchedulerJob < ActiveJob::Base
6 | def perform
7 | load_config
8 | queue_future_jobs
9 | end
10 |
11 | private
12 |
13 | # Returns the path of the Simple Scheduler configuration file.
14 | # @return [String]
15 | def config_path
16 | ENV.fetch("SIMPLE_SCHEDULER_CONFIG", "config/simple_scheduler.yml")
17 | end
18 |
19 | # Load the global scheduler config from the YAML file.
20 | def load_config
21 | @config = YAML.safe_load(ERB.new(File.read(config_path)).result)
22 | @queue_ahead = @config["queue_ahead"] || Task::DEFAULT_QUEUE_AHEAD_MINUTES
23 | @queue_name = @config["queue_name"] || "default"
24 | @time_zone = @config["tz"] || Time.zone.tzinfo.name
25 | @config.delete("queue_ahead")
26 | @config.delete("queue_name")
27 | @config.delete("tz")
28 | end
29 |
30 | # Queue each of the future jobs into Sidekiq from the defined tasks.
31 | def queue_future_jobs
32 | tasks.each do |task|
33 | # Schedule the new run times using the future job wrapper.
34 | new_run_times = task.future_run_times - task.existing_run_times
35 | new_run_times.each do |time|
36 | SimpleScheduler::FutureJob.set(queue: @queue_name, wait_until: time)
37 | .perform_later(task.params, time.to_i)
38 | end
39 | end
40 | end
41 |
42 | # The array of tasks loaded from the config YAML.
43 | # @return [Array path))
12 | end
13 |
14 | describe "successfully queues" do
15 | subject(:job) { described_class.perform_later }
16 |
17 | it "queues the job" do
18 | expect { job }.to change(enqueued_jobs, :size).by(1)
19 | end
20 |
21 | it "is in default queue" do
22 | expect(described_class.new.queue_name).to eq("default")
23 | end
24 | end
25 |
26 | describe "scheduling tasks without specifying a config path" do
27 | it "queues the jobs loaded from config/simple_scheduler.yml" do
28 | travel_to(now) do
29 | expect do
30 | described_class.perform_now
31 | end.to change(enqueued_jobs, :size).by(4)
32 | end
33 | end
34 | end
35 |
36 | describe "Sidekiq queue name" do
37 | it "uses 'default' as the queue name if `queue_name` isn't set in the config" do
38 | travel_to(now) do
39 | described_class.perform_now
40 | expect(enqueued_jobs.first[:queue]).to eq("default")
41 | end
42 | end
43 |
44 | it "uses the custom queue name from the config file when adding FutureJob" do
45 | config_path("spec/simple_scheduler/config/custom_queue_name.yml")
46 | travel_to(now) do
47 | described_class.perform_now
48 | expect(enqueued_jobs.first[:queue]).to eq("custom")
49 | end
50 | end
51 | end
52 |
53 | describe "loading a YML file with ERB tags" do
54 | it "parses the file and queues the jobs" do
55 | config_path("spec/simple_scheduler/config/erb_test.yml")
56 | travel_to(now) do
57 | expect do
58 | described_class.perform_now
59 | end.to change(enqueued_jobs, :size).by(2)
60 | end
61 | end
62 | end
63 |
64 | describe "scheduling an hourly task" do
65 | it "queues jobs for at least six hours into the future by default" do
66 | config_path("spec/simple_scheduler/config/hourly_task.yml")
67 | travel_to(now) do
68 | expect do
69 | described_class.perform_now
70 | end.to change(enqueued_jobs, :size).by(7)
71 | end
72 | end
73 |
74 | it "respects the queue_ahead global option" do
75 | config_path("spec/simple_scheduler/config/queue_ahead_global.yml")
76 | travel_to(now) do
77 | expect do
78 | described_class.perform_now
79 | end.to change(enqueued_jobs, :size).by(3)
80 | end
81 | end
82 |
83 | it "respects the queue_ahead option per task" do
84 | config_path("spec/simple_scheduler/config/queue_ahead_per_task.yml")
85 | travel_to(now) do
86 | expect do
87 | described_class.perform_now
88 | end.to change(enqueued_jobs, :size).by(4)
89 | end
90 | end
91 | end
92 |
93 | describe "scheduling a weekly task" do
94 | it "always queues two future jobs" do
95 | config_path("spec/simple_scheduler/config/active_job.yml")
96 | travel_to(now) do
97 | expect do
98 | described_class.perform_now
99 | end.to change(enqueued_jobs, :size).by(2)
100 | end
101 | end
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/lib/simple_scheduler/at.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "delegate"
4 |
5 | module SimpleScheduler
6 | # A delegator that parses the :at option on a task into the first time it should run.
7 | # Instead of inheriting from Time (which conflicts with ActiveSupport time helpers),
8 | # this wraps a Time instance and selectively overrides hour/hour? semantics.
9 | # Time.now
10 | # # => 2016-12-09 08:24:11 -0600
11 | # SimpleScheduler::At.new("*:30")
12 | # # => 2016-12-09 08:30:00 -0600
13 | # SimpleScheduler::At.new("1:00")
14 | # # => 2016-12-10 01:00:00 -0600
15 | # SimpleScheduler::At.new("Sun 0:00")
16 | # # => 2016-12-11 00:00:00 -0600
17 | class At < SimpleDelegator
18 | AT_PATTERN = /\A(Sun|Mon|Tue|Wed|Thu|Fri|Sat)?\s?(?:\*{1,2}|((?:\b[0-1]?[0-9]|2[0-3]))):([0-5]\d)\z/
19 | DAYS = %w[Sun Mon Tue Wed Thu Fri Sat].freeze
20 |
21 | # Error class raised when an invalid string is given for the time.
22 | class InvalidTime < StandardError; end
23 |
24 | # Accepts a time string to determine when a task should be run for the first time.
25 | # Valid formats:
26 | # "18:00"
27 | # "3:30"
28 | # "**:00"
29 | # "*:30"
30 | # "Sun 2:00"
31 | # "[Sun|Mon|Tue|Wed|Thu|Fri|Sat] 00:00"
32 | # @param at [String] The formatted string for a task's run time
33 | # @param time_zone [ActiveSupport::TimeZone] The time zone to parse the at time in
34 | def initialize(at, time_zone = nil)
35 | @at = at
36 | @time_zone = time_zone || Time.zone
37 | super(parsed_time) # Delegate all Time methods to parsed Time instance
38 | end
39 |
40 | # Returns the specified hour if present in the at string, else the delegated Time's hour.
41 | # @return [Integer]
42 | def hour
43 | hour? ? at_hour : __getobj__.hour
44 | end
45 |
46 | # Returns whether or not the hour was specified in the :at string.
47 | # @return [Boolean]
48 | def hour?
49 | at_match[2].present?
50 | end
51 |
52 | private
53 |
54 | def at_match
55 | @at_match ||= begin
56 | raise InvalidTime, "The `at` option is required." if @at.nil?
57 |
58 | match = AT_PATTERN.match(@at)
59 | raise InvalidTime, "The `at` option '#{@at}' is invalid." if match.nil?
60 |
61 | match
62 | end
63 | end
64 |
65 | def at_hour
66 | @at_hour ||= (at_match[2] || now.hour).to_i
67 | end
68 |
69 | def at_min
70 | @at_min ||= (at_match[3] || now.min).to_i
71 | end
72 |
73 | def at_wday
74 | @at_wday ||= DAYS.index(at_match[1])
75 | end
76 |
77 | def at_wday?
78 | at_match[1].present?
79 | end
80 |
81 | def next_hour
82 | @next_hour ||= begin
83 | h = at_hour
84 | # Add an additional hour if a specific hour wasn't given, if the minutes
85 | # given are less than the current time's minutes.
86 | h += 1 if next_hour?
87 | h
88 | end
89 | end
90 |
91 | def next_hour?
92 | !hour? && at_min < now.min
93 | end
94 |
95 | def now
96 | @now ||= @time_zone.now.beginning_of_minute
97 | end
98 |
99 | def parsed_day
100 | day = now.beginning_of_day
101 |
102 | # If no day of the week is given, return today
103 | return day unless at_wday?
104 |
105 | # Shift to the correct day of the week if given
106 | add_days = at_wday - day.wday
107 | add_days += 7 if day.wday > at_wday
108 | day + add_days.days
109 | end
110 |
111 | # Returns the very first time a job should be run for the scheduled task.
112 | # @return [Time]
113 | def parsed_time
114 | return @parsed_time if defined?(@parsed_time) && @parsed_time
115 |
116 | time_object = parsed_day
117 | change_hour = next_hour
118 | # There is no hour 24, so we need to move to the next day
119 | if change_hour == 24
120 | time_object = 1.day.from_now(time_object)
121 | change_hour = 0
122 | end
123 | time_object = time_object.change(hour: change_hour, min: at_min)
124 |
125 | # If the parsed time is still before the current time, add an additional day if
126 | # the week day wasn't specified or add an additional week to get the correct time.
127 | time_object += at_wday? ? 1.week : 1.day if now > time_object
128 |
129 | @parsed_time = time_object
130 | end
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/lib/simple_scheduler/task.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SimpleScheduler
4 | # Class for parsing each task in the scheduler config YAML file and returning
5 | # the values needed to schedule the task in the future.
6 | #
7 | # @!attribute [r] job_class
8 | # @return [Class] The class of the job or worker.
9 | # @!attribute [r] params
10 | # @return [Hash] The params used to create the task
11 | class Task
12 | attr_reader :job_class, :params
13 |
14 | DEFAULT_QUEUE_AHEAD_MINUTES = 360
15 |
16 | # Initializes a task by parsing the params so the task can be queued in the future.
17 | # @param params [Hash]
18 | # @option params [String] :class The class of the Active Job or Sidekiq Worker
19 | # @option params [String] :every How frequently the job will be performed
20 | # @option params [String] :at The starting time for the interval
21 | # @option params [String] :expires_after The interval used to determine how late the job is allowed to run
22 | # @option params [Integer] :queue_ahead The number of minutes that jobs should be queued in the future
23 | # @option params [String] :task_name The name of the task as defined in the YAML config
24 | # @option params [String] :tz The time zone to use when parsing the `at` option
25 | def initialize(params)
26 | validate_params!(params)
27 | @params = params
28 | end
29 |
30 | # The task's first run time as a Time-like object.
31 | # @return [SimpleScheduler::At]
32 | def at
33 | @at ||= At.new(@params[:at], time_zone)
34 | end
35 |
36 | # The time between the scheduled and actual run time that should cause the job not to run.
37 | # @return [String]
38 | def expires_after
39 | @params[:expires_after]
40 | end
41 |
42 | # Returns an array of existing jobs matching the job class of the task.
43 | # @return [Array]
44 | def existing_jobs
45 | @existing_jobs ||= SimpleScheduler::Task.scheduled_set.select do |job|
46 | next unless job.display_class == "SimpleScheduler::FutureJob"
47 |
48 | task_params = job.display_args[0].symbolize_keys
49 | task_params[:class] == job_class_name && task_params[:name] == name
50 | end.to_a
51 | end
52 |
53 | # Returns an array of existing future run times that have already been scheduled.
54 | # @return [Array