diff --git a/.github/workflows/tests-schedule.yml b/.github/workflows/tests-schedule.yml new file mode 100644 index 0000000..42ae378 --- /dev/null +++ b/.github/workflows/tests-schedule.yml @@ -0,0 +1,12 @@ +name: tests schedule +on: + schedule: + # Run at 06:20 UTC every Monday (weekly) + - cron: "20 6 * * 1" + workflow_dispatch: + +jobs: + run-tests: + if: ${{ github.repository == 'simplymadeapps/simple-scheduler' }} + uses: ./.github/workflows/tests.yml + secrets: inherit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 268ce44..47aba51 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,28 +4,26 @@ on: push: branches: - master + workflow_call: + jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - ruby: [2.7.6, 3.0.4, 3.1.2] + ruby: [3.2.9, 3.3.10, 3.4.7] env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} RAILS_ENV: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 + - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - uses: supercharge/redis-github-action@1.4.0 - - name: Install Code Climate - run: | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build + + - uses: supercharge/redis-github-action@1.8.0 - name: Appraisal Install run: bundle exec appraisal install @@ -35,6 +33,3 @@ jobs: - name: Rspec run: bundle exec appraisal rspec - - - name: Code Climate - run: ./cc-test-reporter after-build --exit-code $? diff --git a/.rubocop.yml b/.rubocop.yml index 3de21fa..f728835 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,7 +4,10 @@ AllCops: - "vendor/bundle/**/*" - "gemfiles/**/*" NewCops: enable - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.2 + +Gemspec/DevelopmentDependencies: + EnforcedStyle: gemspec Layout/FirstArrayElementIndentation: Enabled: false diff --git a/Appraisals b/Appraisals index 6638dc7..1d718ec 100644 --- a/Appraisals +++ b/Appraisals @@ -1,13 +1,18 @@ # frozen_string_literal: true -appraise "rails-6" do - gem "activejob", "~> 6.0" - gem "sidekiq", "~> 6.0" +appraise "rails-7" do + gem "activejob", "~> 7.2" + gem "sidekiq", "~> 7.0" end -appraise "rails-7" do - gem "activejob", "~> 7.0" - gem "sidekiq", "~> 6.0" +appraise "rails-8" do + gem "activejob", "~> 8.0.0" + gem "sidekiq", "~> 8.0" +end + +appraise "rails-8-1" do + gem "activejob", "~> 8.1.0" + gem "sidekiq", "~> 8.0" end appraise "latest" do diff --git a/README.md b/README.md index 7b10665..c416c3f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Simple Scheduler [![tests](https://github.com/simplymadeapps/simple_scheduler/actions/workflows/tests.yml/badge.svg)](https://github.com/simplymadeapps/simple_scheduler/actions/workflows/tests.yml) -[![Code Climate](https://codeclimate.com/github/simplymadeapps/simple_scheduler/badges/gpa.svg)](https://codeclimate.com/github/simplymadeapps/simple_scheduler) -[![Test Coverage](https://codeclimate.com/github/simplymadeapps/simple_scheduler/badges/coverage.svg)](https://codeclimate.com/github/simplymadeapps/simple_scheduler/coverage) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/simplymadeapps/simple_scheduler/) Simple Scheduler is a scheduling add-on that is designed to be used with @@ -27,7 +25,7 @@ Every option we evaluated seems to have the same flaw: **If your server is down, You must be using: -- Rails 5.0+ +- Rails 7.2+ - ActiveJob - [Sidekiq](http://sidekiq.org) - [Heroku Scheduler](https://elements.heroku.com/addons/scheduler)* diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile index 0a2938f..d3fa671 100644 --- a/gemfiles/rails_7.gemfile +++ b/gemfiles/rails_7.gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" -gem "activejob", "~> 7.0" -gem "sidekiq", "~> 6.0" +gem "activejob", "~> 7.2" +gem "sidekiq", "~> 7.0" gemspec path: "../" diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_8.gemfile similarity index 63% rename from gemfiles/rails_6.gemfile rename to gemfiles/rails_8.gemfile index 67865c0..3867701 100644 --- a/gemfiles/rails_6.gemfile +++ b/gemfiles/rails_8.gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" -gem "activejob", "~> 6.0" -gem "sidekiq", "~> 6.0" +gem "activejob", "~> 8.0.0" +gem "sidekiq", "~> 8.0" gemspec path: "../" diff --git a/gemfiles/rails_8_1.gemfile b/gemfiles/rails_8_1.gemfile new file mode 100644 index 0000000..6f863c5 --- /dev/null +++ b/gemfiles/rails_8_1.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activejob", "~> 8.1.0" +gem "sidekiq", "~> 8.0" + +gemspec path: "../" diff --git a/lib/simple_scheduler.rb b/lib/simple_scheduler.rb index 5a18226..5291ce9 100644 --- a/lib/simple_scheduler.rb +++ b/lib/simple_scheduler.rb @@ -2,12 +2,12 @@ require "active_job" require "sidekiq/api" -require_relative "./simple_scheduler/at" -require_relative "./simple_scheduler/future_job" -require_relative "./simple_scheduler/railtie" -require_relative "./simple_scheduler/scheduler_job" -require_relative "./simple_scheduler/task" -require_relative "./simple_scheduler/version" +require_relative "simple_scheduler/at" +require_relative "simple_scheduler/future_job" +require_relative "simple_scheduler/railtie" +require_relative "simple_scheduler/scheduler_job" +require_relative "simple_scheduler/task" +require_relative "simple_scheduler/version" # Module for scheduling jobs at specific times using Sidekiq. module SimpleScheduler diff --git a/lib/simple_scheduler/at.rb b/lib/simple_scheduler/at.rb index 6380537..c23b245 100644 --- a/lib/simple_scheduler/at.rb +++ b/lib/simple_scheduler/at.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true +require "delegate" + module SimpleScheduler - # A Time class for parsing the :at option on a task into the first time it should run. + # A delegator that parses the :at option on a task into the first time it should run. + # Instead of inheriting from Time (which conflicts with ActiveSupport time helpers), + # this wraps a Time instance and selectively overrides hour/hour? semantics. # Time.now # # => 2016-12-09 08:24:11 -0600 # SimpleScheduler::At.new("*:30") @@ -10,8 +14,8 @@ module SimpleScheduler # # => 2016-12-10 01:00:00 -0600 # SimpleScheduler::At.new("Sun 0:00") # # => 2016-12-11 00:00:00 -0600 - class At < Time - 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/.freeze + class At < SimpleDelegator + 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/ DAYS = %w[Sun Mon Tue Wed Thu Fri Sat].freeze # Error class raised when an invalid string is given for the time. @@ -30,15 +34,13 @@ class InvalidTime < StandardError; end def initialize(at, time_zone = nil) @at = at @time_zone = time_zone || Time.zone - super(parsed_time.year, parsed_time.month, parsed_time.day, - parsed_time.hour, parsed_time.min, parsed_time.sec, parsed_time.utc_offset) + super(parsed_time) # Delegate all Time methods to parsed Time instance end - # Always returns the specified hour if the hour was given, otherwise - # it returns the hour calculated based on other specified options. + # Returns the specified hour if present in the at string, else the delegated Time's hour. # @return [Integer] def hour - hour? ? at_hour : super + hour? ? at_hour : __getobj__.hour end # Returns whether or not the hour was specified in the :at string. @@ -51,8 +53,9 @@ def hour? def at_match @at_match ||= begin - match = AT_PATTERN.match(@at) raise InvalidTime, "The `at` option is required." if @at.nil? + + match = AT_PATTERN.match(@at) raise InvalidTime, "The `at` option '#{@at}' is invalid." if match.nil? match @@ -77,11 +80,11 @@ def at_wday? def next_hour @next_hour ||= begin - next_hour = at_hour + h = at_hour # Add an additional hour if a specific hour wasn't given, if the minutes # given are less than the current time's minutes. - next_hour += 1 if next_hour? - next_hour + h += 1 if next_hour? + h end end @@ -94,37 +97,36 @@ def now end def parsed_day - parsed_day = now.beginning_of_day + day = now.beginning_of_day # If no day of the week is given, return today - return parsed_day unless at_wday? + return day unless at_wday? # Shift to the correct day of the week if given - add_days = at_wday - parsed_day.wday - add_days += 7 if parsed_day.wday > at_wday - parsed_day + add_days.days + add_days = at_wday - day.wday + add_days += 7 if day.wday > at_wday + day + add_days.days end # Returns the very first time a job should be run for the scheduled task. # @return [Time] def parsed_time - return @parsed_time if @parsed_time + return @parsed_time if defined?(@parsed_time) && @parsed_time - @parsed_time = parsed_day + time_object = parsed_day change_hour = next_hour - # There is no hour 24, so we need to move to the next day if change_hour == 24 - @parsed_time = 1.day.from_now(@parsed_time) + time_object = 1.day.from_now(time_object) change_hour = 0 end - - @parsed_time = @parsed_time.change(hour: change_hour, min: at_min) + time_object = time_object.change(hour: change_hour, min: at_min) # If the parsed time is still before the current time, add an additional day if # the week day wasn't specified or add an additional week to get the correct time. - @parsed_time += at_wday? ? 1.week : 1.day if now > @parsed_time - @parsed_time + time_object += at_wday? ? 1.week : 1.day if now > time_object + + @parsed_time = time_object end end end diff --git a/lib/simple_scheduler/version.rb b/lib/simple_scheduler/version.rb index 9d359cd..dfc2f53 100644 --- a/lib/simple_scheduler/version.rb +++ b/lib/simple_scheduler/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SimpleScheduler - VERSION = "1.2.0" + VERSION = "2.0.0" end diff --git a/simple_scheduler.gemspec b/simple_scheduler.gemspec index e23bb73..83d2f19 100644 --- a/simple_scheduler.gemspec +++ b/simple_scheduler.gemspec @@ -21,16 +21,17 @@ Gem::Specification.new do |s| s.license = "MIT" s.metadata["rubygems_mfa_required"] = "true" - s.required_ruby_version = ">= 2.7.0" + s.required_ruby_version = ">= 3.2.0" s.files = Dir["{lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] - s.add_dependency "activejob", ">= 6.0" - s.add_dependency "sidekiq", ">= 6.0" + s.add_dependency "activejob", ">= 7.2" + s.add_dependency "sidekiq", ">= 7.0" + s.add_development_dependency "appraisal" s.add_development_dependency "rainbow" - s.add_development_dependency "rspec-rails" + s.add_development_dependency "rspec-rails", "~> 8" s.add_development_dependency "rubocop", "~> 1.28" - s.add_development_dependency "simplecov", "< 0.18" # https://github.com/codeclimate/test-reporter/issues/413 + s.add_development_dependency "simplecov" s.add_development_dependency "simplecov-rcov" end diff --git a/spec/simple_scheduler/future_job_spec.rb b/spec/simple_scheduler/future_job_spec.rb index 2d856d0..4449d22 100644 --- a/spec/simple_scheduler/future_job_spec.rb +++ b/spec/simple_scheduler/future_job_spec.rb @@ -170,7 +170,7 @@ let(:application_job) do Sidekiq::SortedEntry.new( nil, - nil, + 1, "wrapped" => "SomeApplicationJob", "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" ) @@ -179,7 +179,7 @@ let(:simple_scheduler_job1) do Sidekiq::SortedEntry.new( nil, - nil, + 1, "wrapped" => "SimpleScheduler::FutureJob", "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", "args" => [{ "arguments" => [{ "class" => "TestJob", "name" => "test_task" }] }] @@ -189,7 +189,7 @@ let(:simple_scheduler_job2) do Sidekiq::SortedEntry.new( nil, - nil, + 1, "wrapped" => "SimpleScheduler::FutureJob", "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", "args" => [{ "arguments" => [{ "class" => "AnotherJob", "name" => "another_task" }] }] diff --git a/spec/simple_scheduler/task_spec.rb b/spec/simple_scheduler/task_spec.rb index 5bc9790..2321ca1 100644 --- a/spec/simple_scheduler/task_spec.rb +++ b/spec/simple_scheduler/task_spec.rb @@ -29,7 +29,7 @@ let(:sidekiq_entry_matching_class_and_name) do Sidekiq::SortedEntry.new( nil, - nil, + 1, "wrapped" => "SimpleScheduler::FutureJob", "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", "args" => [{ "arguments" => [{ "class" => "TestJob", "name" => "test_task" }] }] @@ -39,7 +39,7 @@ let(:sidekiq_entry_matching_class_wrong_task_name) do Sidekiq::SortedEntry.new( nil, - nil, + 1, "wrapped" => "SimpleScheduler::FutureJob", "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", "args" => [{ "arguments" => [{ "class" => "TestJob", "name" => "wrong_task" }] }] @@ -49,7 +49,7 @@ let(:sidekiq_entry_wrong_class) do Sidekiq::SortedEntry.new( nil, - nil, + 1, "wrapped" => "SimpleScheduler::FutureJob", "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", "args" => [{ "arguments" => [{ "class" => "SomeOtherJob", "name" => "test_task" }] }]