From 142f0a6f1c8c119948f22d56332b6b0b0e376466 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Tue, 5 Jun 2018 15:24:44 +0200 Subject: [PATCH] Check processor with task to update/sync dates --- app/models/check.rb | 6 ++ app/models/notification.rb | 3 + app/services/check_processor.rb | 70 ++++++++++++++ app/services/notifier/resolver.rb | 5 +- config/chexpire.example.yml | 4 + config/chexpire.test.yml | 4 + lib/tasks/checks.rake | 7 ++ test/factories/checks.rb | 12 ++- test/services/check_processor_test.rb | 133 ++++++++++++++++++++++++++ 9 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 app/services/check_processor.rb create mode 100644 lib/tasks/checks.rake create mode 100644 test/services/check_processor_test.rb diff --git a/app/models/check.rb b/app/models/check.rb index 7a02a34..fb6b0aa 100644 --- a/app/models/check.rb +++ b/app/models/check.rb @@ -52,6 +52,12 @@ class Check < ApplicationRecord after_update :reset_notifications after_save :enqueue_sync + scope :active, -> { where(active: true) } + scope :last_run_failed, -> { + where("(last_success_at IS NULL AND last_run_at IS NOT NULL) + OR (last_success_at <= DATE_SUB(last_run_at, INTERVAL 5 MINUTE))") + } + private def domain_created_at_past diff --git a/app/models/notification.rb b/app/models/notification.rb index f3c3bd1..27f6289 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -31,6 +31,9 @@ class Notification < ApplicationRecord validates :delay, numericality: { only_integer: true, greater_than_or_equal_to: 1 } validates :recipient, presence: true + scope :active_check, -> { Check.active } + scope :check_last_run_failed, -> { Check.last_run_failed } + def pending! self.sent_at = nil super diff --git a/app/services/check_processor.rb b/app/services/check_processor.rb new file mode 100644 index 0000000..31737c3 --- /dev/null +++ b/app/services/check_processor.rb @@ -0,0 +1,70 @@ +class CheckProcessor + attr_reader :configuration + + def initialize(configuration = nil) + @configuration = configuration || default_configuration + end + + def sync_dates # rubocop:disable Metrics/MethodLength + %i[ + resolve_last_run_failed + resolve_expire_short_term + resolve_expire_long_term + resolve_unknown_expiry + ].each do |resolver| + public_send(resolver).find_each(batch_size: 100).each do |check| + process(check) + + sleep configuration.interval + end + end + end + + def resolve_last_run_failed + scope.last_run_failed + end + + def resolve_expire_long_term + scope + .where("DATE(domain_expires_at) >= DATE_ADD(CURDATE(), INTERVAL ? DAY)", + configuration.long_term) + .where("DATEDIFF(domain_expires_at, CURDATE()) MOD ? = 0", + configuration.long_term_frequency) + end + + def resolve_expire_short_term + scope.where("DATE(domain_expires_at) < DATE_ADD(CURDATE(), INTERVAL ? DAY)", + configuration.long_term) + end + + def resolve_unknown_expiry + scope.where("domain_expires_at IS NULL") + end + + private + + def scope + Check + .active + .where("last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL 12 HOUR)") + end + + def process(check) + case check.kind.to_sym + when :domain + WhoisSyncJob.perform_now(check.id) + else + fail ArgumentError, "Unsupported check kind `#{check.kind}`" + end + end + + def default_configuration + config = Rails.configuration.chexpire.fetch("checks", {}) + + OpenStruct.new( + interval: config.fetch("interval") { 0.00 }, + long_term: config.fetch("long_term") { 60 }, + long_term_frequency: config.fetch("long_term_frequency") { 10 }, + ) + end +end diff --git a/app/services/notifier/resolver.rb b/app/services/notifier/resolver.rb index 21e1fa8..a40c959 100644 --- a/app/services/notifier/resolver.rb +++ b/app/services/notifier/resolver.rb @@ -12,7 +12,7 @@ module Notifier # Logical rules are in plain ruby inside processor scope .includes(check: :logs) - .where("checks.last_success_at <= DATE_SUB(checks.last_run_at, INTERVAL 5 MINUTE)") + .merge(Check.last_run_failed) end private @@ -20,7 +20,8 @@ module Notifier def scope Notification .includes(:check) - .where(status: [:pending, :failed], checks: { active: true }) + .where(status: [:pending, :failed]) + .merge(Check.active) .where.not(checks: { user: ignore_users }) end diff --git a/config/chexpire.example.yml b/config/chexpire.example.yml index 3543eaf..2e4ce13 100644 --- a/config/chexpire.example.yml +++ b/config/chexpire.example.yml @@ -3,6 +3,10 @@ default: &default notifier: interval: 0.00 failure_days: 3 + checks: + interval: 0.5 + long_term: 60 + long_term_frequency: 10 development: <<: *default diff --git a/config/chexpire.test.yml b/config/chexpire.test.yml index 371a542..db88e9a 100644 --- a/config/chexpire.test.yml +++ b/config/chexpire.test.yml @@ -4,3 +4,7 @@ test: notifier: interval: 0.00 failure_days: 3 + checks: + interval: 0.00 + long_term: 60 + long_term_frequency: 10 diff --git a/lib/tasks/checks.rake b/lib/tasks/checks.rake new file mode 100644 index 0000000..5d82236 --- /dev/null +++ b/lib/tasks/checks.rake @@ -0,0 +1,7 @@ +namespace :checks do + desc "Refresh expiry dates for checks" + task sync_dates: :environment do + process = CheckProcessor.new + process.sync_dates + end +end diff --git a/test/factories/checks.rb b/test/factories/checks.rb index 7621f96..62d95ec 100644 --- a/test/factories/checks.rb +++ b/test/factories/checks.rb @@ -54,14 +54,18 @@ FactoryBot.define do domain_expires_at 1.week.from_now end + trait :expires_next_year do + domain_expires_at 1.year.from_now + end + trait :last_runs_failed do - last_run_at Time.now - 90.minutes - last_success_at 1.week.ago - 2.hours + last_run_at 3.days.ago - 90.minutes + last_success_at 7.days.ago - 2.hours end trait :last_run_succeed do - last_run_at 1.hour.ago - last_success_at 1.hour.ago + last_run_at 25.hour.ago + last_success_at 25.hour.ago end trait :inactive do diff --git a/test/services/check_processor_test.rb b/test/services/check_processor_test.rb new file mode 100644 index 0000000..66e1e53 --- /dev/null +++ b/test/services/check_processor_test.rb @@ -0,0 +1,133 @@ +require "test_helper" + +class CheckProcessorTest < ActiveSupport::TestCase + setup do + @processor = CheckProcessor.new + end + + test "process WhoisSyncJob for domain checks" do + domain = "domain.fr" + check = create(:check, :domain, :nil_dates, domain: domain) + + mock_system_command("whois", domain, stdout: file_fixture("whois/domain.fr.txt").read) do + @processor.send(:process, check) + end + + check.reload + + assert_equal Time.new(2019, 2, 17, 0, 0, 0, 0), check.domain_expires_at + end + + test "raises an error for an unsupported check kind" do + check = build(:check) + + check.stub :kind, :unknown do + assert_raises ArgumentError do + @processor.send(:process, check) + end + end + end + + test "resolve_last_run_failed includes already and never succeeded" do + c1 = create(:check, :last_runs_failed) + c2 = create(:check, :last_run_succeed) + c3 = create(:check, last_run_at: 4.days.ago, last_success_at: nil) + + checks = @processor.resolve_last_run_failed + + assert_includes checks, c1 + assert_not_includes checks, c2 + assert_includes checks, c3 + end + + test "resolve_unknown_expiry" do + c1 = create(:check, :nil_dates) + c2 = create(:check) + + checks = @processor.resolve_unknown_expiry + + assert_includes checks, c1 + assert_not_includes checks, c2 + end + + test "resolve_expire_short_term" do + c1 = create(:check, :expires_next_week) + c2 = create(:check, :expires_next_year) + + checks = @processor.resolve_expire_short_term + + assert_includes checks, c1 + assert_not_includes checks, c2 + end + + test "resolve_expire_long_term returns checks with respect of frequency" do + c1 = create(:check, domain_expires_at: 380.days.from_now) + c2 = create(:check, domain_expires_at: 390.days.from_now) + c3 = create(:check, domain_expires_at: 391.days.from_now) + c4 = create(:check, domain_expires_at: 20.days.from_now) + + checks = @processor.resolve_expire_long_term + + assert_includes checks, c1 + assert_includes checks, c2 + assert_not_includes checks, c3 + assert_not_includes checks, c4 + end + + test "resolvers does not include checks recently executed" do + c1 = create(:check, :expires_next_week) + c2 = create(:check, :expires_next_week, last_run_at: 4.hours.ago) + + checks = @processor.resolve_expire_short_term + + assert_includes checks, c1 + assert_not_includes checks, c2 + end + + test "resolvers include checks never executed" do + c1 = create(:check, :expires_next_week, last_run_at: 4.days.ago) + + checks = @processor.resolve_expire_short_term + + assert_includes checks, c1 + end + + test "resolvers does not include inactive checks" do + c1 = create(:check, :expires_next_week) + c2 = create(:check, :expires_next_week, :inactive) + + checks = @processor.resolve_expire_short_term + + assert_includes checks, c1 + assert_not_includes checks, c2 + end + + test "#sync_dates respects the interval configuration between sends" do + create_list(:check, 3, :expires_next_week) + + configuration = Minitest::Mock.new + 2.times do configuration.expect(:long_term, 60) end + configuration.expect(:long_term_frequency, 10) + + 3.times do + configuration.expect(:interval, 0.000001) + end + + processor = CheckProcessor.new(configuration) + + mock = Minitest::Mock.new + assert_stub = lambda { |actual_time| + assert_equal 0.000001, actual_time + mock + } + + processor.stub :process, nil do + processor.stub :sleep, assert_stub do + processor.sync_dates + end + end + + configuration.verify + mock.verify + end +end