diff --git a/Gemfile b/Gemfile index 9b7f3611ff72..25f60a8d7046 100644 --- a/Gemfile +++ b/Gemfile @@ -124,8 +124,7 @@ gem 'multi_json', '~> 1.15.0' gem 'oj', '~> 3.16.0' gem 'daemons' -gem 'delayed_cron_job', '~> 0.9.0' -gem 'delayed_job_active_record', '~> 4.1.5' +gem 'good_job' gem 'rack-protection', '~> 3.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index b36db8d878c6..7748cc0d5cf3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -456,13 +456,6 @@ GEM deckar01-task_list (2.3.3) html-pipeline declarative (0.0.20) - delayed_cron_job (0.9.0) - fugit (>= 1.5) - delayed_job (4.1.11) - activesupport (>= 3.0, < 8.0) - delayed_job_active_record (4.1.8) - activerecord (>= 3.0, < 8.0) - delayed_job (>= 3.0, < 5) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) diff-lcs (1.5.0) @@ -576,6 +569,13 @@ GEM i18n (>= 0.7) multi_json request_store (>= 1.0) + good_job (3.21.5) + activejob (>= 6.0.0) + activerecord (>= 6.0.0) + concurrent-ruby (>= 1.0.2) + fugit (>= 1.1) + railties (>= 6.0.0) + thor (>= 0.14.1) google-apis-core (0.13.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 1.9) @@ -1158,8 +1158,6 @@ DEPENDENCIES date_validator (~> 0.12.0) debug deckar01-task_list (~> 2.3.1) - delayed_cron_job (~> 0.9.0) - delayed_job_active_record (~> 4.1.5) disposable (~> 0.6.2) doorkeeper (~> 5.6.6) dotenv-rails @@ -1177,6 +1175,7 @@ DEPENDENCIES friendly_id (~> 5.5.0) fuubar (~> 2.5.0) gon (~> 6.4.0) + good_job google-apis-gmail_v1 googleauth grape (~> 2.0.0) diff --git a/app/contracts/settings/working_days_params_contract.rb b/app/contracts/settings/working_days_params_contract.rb index c19f63062f85..1c8f16c28c5f 100644 --- a/app/contracts/settings/working_days_params_contract.rb +++ b/app/contracts/settings/working_days_params_contract.rb @@ -41,8 +41,11 @@ def working_days_are_present end end + # TODO: consider implementing using GoodJob concurrency control mechanisms def unique_job - if WorkPackages::ApplyWorkingDaysChangeJob.scheduled? + if GoodJob::Job + .where(finished_at: nil) + .exists?(job_class: 'WorkPackages::ApplyWorkingDaysChangeJob') errors.add :base, :previous_working_day_changes_unprocessed end end diff --git a/app/seeders/root_seeder.rb b/app/seeders/root_seeder.rb index 029818e43d98..316f102af4ae 100644 --- a/app/seeders/root_seeder.rb +++ b/app/seeders/root_seeder.rb @@ -117,13 +117,13 @@ def prepare_seed! ActionMailer::Base.perform_deliveries = false # Avoid asynchronous DeliverWorkPackageCreatedJob - previous_delay_jobs = Delayed::Worker.delay_jobs - Delayed::Worker.delay_jobs = false + previous_execution_mode = Rails.configuration.good_job.execution_mode + Rails.configuration.good_job.execution_mode = :inline yield ensure ActionMailer::Base.perform_deliveries = previous_perform_deliveries - Delayed::Worker.delay_jobs = previous_delay_jobs + Rails.configuration.good_job.execution_mode = previous_execution_mode end def seed_basic_data diff --git a/app/workers/application_job.rb b/app/workers/application_job.rb index de6bccc0067e..703cedbfe11f 100644 --- a/app/workers/application_job.rb +++ b/app/workers/application_job.rb @@ -92,6 +92,11 @@ def reload_mailer_settings! Setting.reload_mailer_settings! end + def good_job_scheduled_at + GoodJob::Job.where(id: job_id) + .pick(:scheduled_at) + end + private def prepare_job_context diff --git a/app/workers/attachments/cleanup_uncontainered_job.rb b/app/workers/attachments/cleanup_uncontainered_job.rb index 729fd37231be..5b0dc599ef53 100644 --- a/app/workers/attachments/cleanup_uncontainered_job.rb +++ b/app/workers/attachments/cleanup_uncontainered_job.rb @@ -26,12 +26,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Attachments::CleanupUncontaineredJob < Cron::CronJob +class Attachments::CleanupUncontaineredJob < ApplicationJob queue_with_priority :low - # runs at 10:03 pm - self.cron_expression = '03 22 * * *' - def perform Attachment .where(container: nil) @@ -50,7 +47,7 @@ def too_old attachment_table = Attachment.arel_table attachment_table[:created_at] - .lteq(Time.now - OpenProject::Configuration.attachments_grace_period.minutes) + .lteq(Time.zone.now - OpenProject::Configuration.attachments_grace_period.minutes) .to_sql end end diff --git a/app/workers/concerns/scheduled_job.rb b/app/workers/concerns/scheduled_job.rb deleted file mode 100644 index 8c7516a31ec5..000000000000 --- a/app/workers/concerns/scheduled_job.rb +++ /dev/null @@ -1,41 +0,0 @@ -# OpenProject is an open source project management software. -# Copyright (C) 2010-2022 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. - -module ScheduledJob - extend ActiveSupport::Concern - - class_methods do - ## - # Is there a job scheduled? - def scheduled? - delayed_job_query.exists? - end - - def delayed_job_query - Delayed::Job.where('handler LIKE ?', "%job_class: #{name}%") - end - end -end diff --git a/app/workers/cron/clear_old_sessions_job.rb b/app/workers/cron/clear_old_sessions_job.rb index e53494d626d3..252b506d2bb0 100644 --- a/app/workers/cron/clear_old_sessions_job.rb +++ b/app/workers/cron/clear_old_sessions_job.rb @@ -27,12 +27,9 @@ #++ module Cron - class ClearOldSessionsJob < CronJob + class ClearOldSessionsJob < ApplicationJob include ::RakeJob - # runs at 1:15 nightly - self.cron_expression = '15 1 * * *' - def perform super('db:sessions:expire', 7) end diff --git a/app/workers/cron/clear_tmp_cache_job.rb b/app/workers/cron/clear_tmp_cache_job.rb index 8ac9a6c21016..74cb6ce1bd5a 100644 --- a/app/workers/cron/clear_tmp_cache_job.rb +++ b/app/workers/cron/clear_tmp_cache_job.rb @@ -27,12 +27,9 @@ #++ module Cron - class ClearTmpCacheJob < CronJob + class ClearTmpCacheJob < ApplicationJob include ::RakeJob - # runs at 02:45 sundays - self.cron_expression = '45 2 * * 7' - def perform super('tmp:cache:clear') end diff --git a/app/workers/cron/clear_uploaded_files_job.rb b/app/workers/cron/clear_uploaded_files_job.rb index 458615f98b07..e0733843e8ff 100644 --- a/app/workers/cron/clear_uploaded_files_job.rb +++ b/app/workers/cron/clear_uploaded_files_job.rb @@ -27,12 +27,9 @@ #++ module Cron - class ClearUploadedFilesJob < CronJob + class ClearUploadedFilesJob < ApplicationJob include ::RakeJob - # Runs 23pm fridays - self.cron_expression = '0 23 * * 5' - def perform super('attachments:clear') end diff --git a/app/workers/cron/cron_job.rb b/app/workers/cron/cron_job.rb deleted file mode 100644 index a2afe9f260d6..000000000000 --- a/app/workers/cron/cron_job.rb +++ /dev/null @@ -1,76 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Cron - class CronJob < ApplicationJob - class_attribute :cron_expression - - # List of registered jobs, requires eager load in dev(!) - class_attribute :registered_jobs, default: [] - - include ScheduledJob - - class << self - ## - # Register new job class(es) - def register!(*job_classes) - Array(job_classes).each do |clz| - raise ArgumentError, "Needs to be subclass of ::Cron::CronJob" unless clz.ancestors.include?(self) - - registered_jobs << clz - end - end - - def schedule_registered_jobs! - registered_jobs.each do |job_class| - job_class.ensure_scheduled! - end - end - - ## - # Ensure the job is scheduled unless it is already - def ensure_scheduled! - # Ensure scheduled only once - return if scheduled? - - Rails.logger.info { "Scheduling #{name} recurrent background job." } - set(cron: cron_expression).perform_later - end - - ## - # Remove the scheduled job, if any - def remove - delayed_job&.destroy - end - - def delayed_job - delayed_job_query.first - end - end - end -end diff --git a/app/workers/ldap/synchronization_job.rb b/app/workers/ldap/synchronization_job.rb index e584179d931e..a100d5b87898 100644 --- a/app/workers/ldap/synchronization_job.rb +++ b/app/workers/ldap/synchronization_job.rb @@ -27,10 +27,7 @@ #++ module Ldap - class SynchronizationJob < ::Cron::CronJob - # Run once per night at 11:30pm - self.cron_expression = '30 23 * * *' - + class SynchronizationJob < ApplicationJob def perform run_user_sync end diff --git a/app/workers/notifications/schedule_date_alerts_notifications_job.rb b/app/workers/notifications/schedule_date_alerts_notifications_job.rb index 43c5bd810bd4..a351ec983fbf 100644 --- a/app/workers/notifications/schedule_date_alerts_notifications_job.rb +++ b/app/workers/notifications/schedule_date_alerts_notifications_job.rb @@ -28,15 +28,11 @@ module Notifications # Creates date alert jobs for users whose local time is 1:00 am. - class ScheduleDateAlertsNotificationsJob < Cron::CronJob - # runs every quarter of an hour, so 00:00, 00:15,..., 15:30, 15:45, 16:00, ... - self.cron_expression = '*/15 * * * *' - + class ScheduleDateAlertsNotificationsJob < ApplicationJob def perform return unless EnterpriseToken.allows_to?(:date_alerts) - service = Service.new(times_from_scheduled_to_execution) - service.call + Service.new(times_from_scheduled_to_execution).call end # Returns times from scheduled execution time to current time in 15 minutes @@ -57,7 +53,7 @@ def times_from_scheduled_to_execution end def scheduled_time - self.class.delayed_job.run_at.then { |t| t.change(min: t.min / 15 * 15) } + good_job_scheduled_at.then { |t| t.change(min: t.min / 15 * 15) } end end end diff --git a/app/workers/notifications/schedule_reminder_mails_job.rb b/app/workers/notifications/schedule_reminder_mails_job.rb index 0932594fbda0..6c985161b753 100644 --- a/app/workers/notifications/schedule_reminder_mails_job.rb +++ b/app/workers/notifications/schedule_reminder_mails_job.rb @@ -27,18 +27,13 @@ #++ module Notifications - class ScheduleReminderMailsJob < Cron::CronJob - # runs every quarter of an hour, so 00:00, 00:15... - self.cron_expression = '*/15 * * * *' - + class ScheduleReminderMailsJob < ApplicationJob def perform - User.having_reminder_mail_to_send(run_at).pluck(:id).each do |user_id| + User.having_reminder_mail_to_send(good_job_scheduled_at) + .pluck(:id) + .each do |user_id| Mails::ReminderJob.perform_later(user_id) end end - - def run_at - self.class.delayed_job.run_at - end end end diff --git a/app/workers/oauth/cleanup_job.rb b/app/workers/oauth/cleanup_job.rb index 3295cb0e82b4..8ada62fb0b13 100644 --- a/app/workers/oauth/cleanup_job.rb +++ b/app/workers/oauth/cleanup_job.rb @@ -27,12 +27,9 @@ #++ module OAuth - class CleanupJob < ::Cron::CronJob + class CleanupJob < ApplicationJob include ::RakeJob - # runs at 1:52 nightly - self.cron_expression = '52 1 * * *' - queue_with_priority :low def perform diff --git a/app/workers/paper_trail_audits/cleanup_job.rb b/app/workers/paper_trail_audits/cleanup_job.rb index 634f240c7641..5d33de1a0be4 100644 --- a/app/workers/paper_trail_audits/cleanup_job.rb +++ b/app/workers/paper_trail_audits/cleanup_job.rb @@ -27,10 +27,7 @@ #++ module PaperTrailAudits - class CleanupJob < ::Cron::CronJob - # runs at 4:03 on Saturday - self.cron_expression = '3 4 * * 6' - + class CleanupJob < ApplicationJob # Clean any paper trails older than 60 days def perform ::PaperTrailAudit diff --git a/app/workers/work_packages/apply_working_days_change_job.rb b/app/workers/work_packages/apply_working_days_change_job.rb index db79c3c43019..619acf4e9073 100644 --- a/app/workers/work_packages/apply_working_days_change_job.rb +++ b/app/workers/work_packages/apply_working_days_change_job.rb @@ -28,7 +28,6 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob queue_with_priority :above_normal - include ::ScheduledJob def perform(user_id:, previous_working_days:, previous_non_working_days:) user = User.find(user_id) diff --git a/bin/delayed_job b/bin/delayed_job deleted file mode 100755 index edf195985f69..000000000000 --- a/bin/delayed_job +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env ruby - -require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) -require 'delayed/command' -Delayed::Command.new(ARGV).daemonize diff --git a/config/application.rb b/config/application.rb index 3c92d6857a32..2290c287a969 100644 --- a/config/application.rb +++ b/config/application.rb @@ -212,7 +212,18 @@ class Application < Rails::Application # This allows for setting the root either via config file or via environment variable. config.action_controller.relative_url_root = OpenProject::Configuration['rails_relative_url_root'] - config.active_job.queue_adapter = :delayed_job + config.active_job.queue_adapter = :good_job + + config.good_job.preserve_job_records = true + config.good_job.retry_on_unhandled_error = false + # config.good_job.on_thread_error = -> (exception) { Rails.error.report(exception) } + config.good_job.execution_mode = :external + config.good_job.queues = '*' + config.good_job.max_threads = 20 + config.good_job.poll_interval = 30 + config.good_job.shutdown_timeout = 25 + config.good_job.enable_cron = true + config.good_job.smaller_number_is_higher_priority = false config.action_controller.asset_host = OpenProject::Configuration::AssetHost.value diff --git a/config/initializers/cronjobs.rb b/config/initializers/cronjobs.rb index 6dda0beb89ed..ba22d94574e8 100644 --- a/config/initializers/cronjobs.rb +++ b/config/initializers/cronjobs.rb @@ -1,15 +1,71 @@ -# Register "Cron-like jobs" +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ -Rails.application.configure do |application| - application.config.to_prepare do - Cron::CronJob.register! Cron::ClearOldSessionsJob, - Cron::ClearTmpCacheJob, - Cron::ClearUploadedFilesJob, - OAuth::CleanupJob, - PaperTrailAudits::CleanupJob, - Attachments::CleanupUncontaineredJob, - Notifications::ScheduleDateAlertsNotificationsJob, - Notifications::ScheduleReminderMailsJob, - Ldap::SynchronizationJob - end +# Register "Cron-like jobs" +OpenProject::Application.configure do |application| + application.config.good_job.cron.merge!( + { + 'Cron::ClearOldSessionsJob': { + cron: '15 1 * * *', # runs at 1:15 nightly + class: 'Cron::ClearOldSessionsJob' + }, + 'Cron::ClearTmpCacheJob': { + cron: '45 2 * * 7', # runs at 02:45 sundays + class: 'Cron::ClearTmpCacheJob' + }, + 'Cron::ClearUploadedFilesJob': { + cron: '0 23 * * 5', # runs 23:00 fridays + class: 'Cron::ClearUploadedFilesJob' + }, + 'OAuth::CleanupJob': { + cron: '52 1 * * *', + class: 'OAuth::CleanupJob' + }, + 'PaperTrailAudits::CleanupJob': { + cron: '3 4 * * 6', + class: 'PaperTrailAudits::CleanupJob' + }, + 'Attachments::CleanupUncontaineredJob': { + cron: '03 22 * * *', + class: 'Attachments::CleanupUncontaineredJob' + }, + 'Notifications::ScheduleDateAlertsNotificationsJob': { + cron: '*/15 * * * *', + class: 'Notifications::ScheduleDateAlertsNotificationsJob' + }, + 'Notifications::ScheduleReminderMailsJob': { + cron: '*/15 * * * *', + class: 'Notifications::ScheduleReminderMailsJob' + }, + 'Ldap::SynchronizationJob': { + cron: '30 23 * * *', + class: 'Ldap::SynchronizationJob' + } + } + ) end diff --git a/config/initializers/database_pool_size.rb b/config/initializers/database_pool_size.rb index 8809f2439de9..ac707aeef26f 100644 --- a/config/initializers/database_pool_size.rb +++ b/config/initializers/database_pool_size.rb @@ -1,3 +1,31 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + config = Rails.env.production? && Rails.application.config.database_configuration[Rails.env] pool_size = config && [OpenProject::Configuration.web_max_threads + 1, config['pool'].to_i].max diff --git a/config/initializers/delayed_job_config.rb b/config/initializers/delayed_job_config.rb deleted file mode 100644 index 89c3af7505ce..000000000000 --- a/config/initializers/delayed_job_config.rb +++ /dev/null @@ -1,55 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -# Disable delayed_job's own logging as we have activejob -Delayed::Worker.logger = nil - -# By default bypass worker queue and execute asynchronous tasks at once -Delayed::Worker.delay_jobs = true - -# Prevent loading ApplicationJob during initialization -Rails.application.reloader.to_prepare do - # Set default priority (lower = higher priority) - # Example ordering, see ApplicationJob.priority_number - Delayed::Worker.default_priority = ApplicationJob.priority_number(:default) -end - -# Do not retry jobs from delayed_job -# instead use 'retry_on' activejob functionality -Delayed::Worker.max_attempts = 1 - -# Remember DJ id in the payload object -class Delayed::ProviderJobIdPlugin < Delayed::Plugin - callbacks do |lifecycle| - lifecycle.before(:invoke_job) do |job| - job.payload_object.job_data['provider_job_id'] = job.id if job.payload_object.respond_to?(:job_data) - end - end -end - -Delayed::Worker.plugins << Delayed::ProviderJobIdPlugin diff --git a/config/initializers/time_with_zone_as_json.rb b/config/initializers/time_with_zone_as_json.rb deleted file mode 100644 index 117f21ffaa4d..000000000000 --- a/config/initializers/time_with_zone_as_json.rb +++ /dev/null @@ -1,33 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class ActiveSupport::TimeWithZone - def as_json(_options = {}) - time.strftime('%m/%d/%Y/ %H:%M %p').to_s - end -end diff --git a/config/routes.rb b/config/routes.rb index 11de0c2a584b..f90494edc42d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -639,4 +639,8 @@ if OpenProject::Configuration.lookbook_enabled? mount Lookbook::Engine, at: "/lookbook" end + + constraints(->(req) { User.exists?(id: req.session[:user_id], admin: true) }) do + mount GoodJob::Engine => 'good_job' + end end diff --git a/db/migrate/20201125121949_remove_renamed_cron_job.rb b/db/migrate/20201125121949_remove_renamed_cron_job.rb deleted file mode 100644 index c9d08ea8a7ed..000000000000 --- a/db/migrate/20201125121949_remove_renamed_cron_job.rb +++ /dev/null @@ -1,38 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class RemoveRenamedCronJob < ActiveRecord::Migration[6.0] - def up - # The job has been renamed to JobStatus::Cron::ClearOldJobStatusJob - # the new job will be added on restarting the application but the old will still be in the database - # and will cause 'uninitialized constant' errors. - Delayed::Job - .where('handler LIKE ?', "%job_class: Cron::ClearOldJobStatusJob%") - .delete_all - end -end diff --git a/db/migrate/20220202140507_reorder_project_children.rb b/db/migrate/20220202140507_reorder_project_children.rb deleted file mode 100644 index d964d0d077e8..000000000000 --- a/db/migrate/20220202140507_reorder_project_children.rb +++ /dev/null @@ -1,33 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class ReorderProjectChildren < ActiveRecord::Migration[6.1] - def up - ::Projects::ReorderHierarchyJob.perform_later - end -end diff --git a/db/migrate/20220804112533_remove_notification_cleanup_job.rb b/db/migrate/20220804112533_remove_notification_cleanup_job.rb deleted file mode 100644 index 956032ddad79..000000000000 --- a/db/migrate/20220804112533_remove_notification_cleanup_job.rb +++ /dev/null @@ -1,41 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class RemoveNotificationCleanupJob < ActiveRecord::Migration[7.0] - def up - # Remove the cron job no longer desired. - # The code itself is removed but keeping it in the database would lead to UninitializedConstant errors. - Delayed::Job - .where('handler LIKE ?', "%job_class: Notifications::CleanupJob%") - .delete_all - - Setting - .where(name: 'notification_retention_period_days') - .delete_all - end -end diff --git a/db/migrate/20221026132134_bigint_primary_and_foreign_keys.rb b/db/migrate/20221026132134_bigint_primary_and_foreign_keys.rb index c641d892044f..9a60233b59ac 100644 --- a/db/migrate/20221026132134_bigint_primary_and_foreign_keys.rb +++ b/db/migrate/20221026132134_bigint_primary_and_foreign_keys.rb @@ -60,7 +60,6 @@ class BigintPrimaryAndForeignKeys < ActiveRecord::Migration[7.0] CustomStyle => [:id], CustomValue => %i[id customized_id custom_field_id], Journal::CustomizableJournal => %i[id journal_id custom_field_id], - Delayed::Job => [:id], DesignColor => [:id], Journal::DocumentJournal => %i[id project_id category_id], Document => %i[id project_id category_id], diff --git a/db/migrate/20230105073117_remove_renamed_date_alert_job.rb b/db/migrate/20230105073117_remove_renamed_date_alert_job.rb deleted file mode 100644 index 9ae61bc1923b..000000000000 --- a/db/migrate/20230105073117_remove_renamed_date_alert_job.rb +++ /dev/null @@ -1,37 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class RemoveRenamedDateAlertJob < ActiveRecord::Migration[6.0] - def up - # The job has been renamed to Notifications::ScheduleDateAlertsNotificationsJob. - # The new job will be added on restarting the application. - Delayed::Job - .where('handler LIKE ?', "%job_class: Notifications::CreateDateAlertsNotificationsJob%") - .delete_all - end -end diff --git a/db/migrate/20240123151246_create_good_jobs.rb b/db/migrate/20240123151246_create_good_jobs.rb new file mode 100644 index 000000000000..83cb7893fae8 --- /dev/null +++ b/db/migrate/20240123151246_create_good_jobs.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class CreateGoodJobs < ActiveRecord::Migration[7.0] + def change + # Uncomment for Postgres v12 or earlier to enable gen_random_uuid() support + # enable_extension 'pgcrypto' + + create_table :good_jobs, id: :uuid do |t| + t.text :queue_name + t.integer :priority + t.jsonb :serialized_params + t.datetime :scheduled_at + t.datetime :performed_at + t.datetime :finished_at + t.text :error + + t.timestamps + + t.uuid :active_job_id + t.text :concurrency_key + t.text :cron_key + t.uuid :retried_good_job_id + t.datetime :cron_at + end + + create_table :good_job_processes, id: :uuid do |t| + t.timestamps + t.jsonb :state + end + + add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)", name: :index_good_jobs_on_scheduled_at + add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)", name: :index_good_jobs_on_queue_name_and_scheduled_at + add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at + add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished + add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at + add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true + add_index :good_jobs, [:active_job_id], name: :index_good_jobs_on_active_job_id + add_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at + end +end diff --git a/db/migrate/20240123151247_create_good_job_settings.rb b/db/migrate/20240123151247_create_good_job_settings.rb new file mode 100644 index 000000000000..fa3540174a62 --- /dev/null +++ b/db/migrate/20240123151247_create_good_job_settings.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateGoodJobSettings < ActiveRecord::Migration[7.0] + def change + reversible do |dir| + dir.up do + # Ensure this incremental update migration is idempotent + # with monolithic install migration. + return if connection.table_exists?(:good_job_settings) + end + end + + create_table :good_job_settings, id: :uuid do |t| + t.timestamps + t.text :key + t.jsonb :value + t.index :key, unique: true + end + end +end diff --git a/db/migrate/20240123151248_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb b/db/migrate/20240123151248_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb new file mode 100644 index 000000000000..7b9dbab3816f --- /dev/null +++ b/db/migrate/20240123151248_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateIndexGoodJobsJobsOnPriorityCreatedAtWhenUnfinished < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + reversible do |dir| + dir.up do + # Ensure this incremental update migration is idempotent + # with monolithic install migration. + return if connection.index_name_exists?(:good_jobs, :index_good_jobs_jobs_on_priority_created_at_when_unfinished) + end + end + + add_index :good_jobs, [:priority, :created_at], order: { priority: "DESC NULLS LAST", created_at: :asc }, + where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished, + algorithm: :concurrently + end +end diff --git a/db/migrate/20240123151249_create_good_job_batches.rb b/db/migrate/20240123151249_create_good_job_batches.rb new file mode 100644 index 000000000000..ac6c5b37e982 --- /dev/null +++ b/db/migrate/20240123151249_create_good_job_batches.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CreateGoodJobBatches < ActiveRecord::Migration[7.0] + def change + reversible do |dir| + dir.up do + # Ensure this incremental update migration is idempotent + # with monolithic install migration. + return if connection.table_exists?(:good_job_batches) + end + end + + create_table :good_job_batches, id: :uuid do |t| + t.timestamps + t.text :description + t.jsonb :serialized_properties + t.text :on_finish + t.text :on_success + t.text :on_discard + t.text :callback_queue_name + t.integer :callback_priority + t.datetime :enqueued_at + t.datetime :discarded_at + t.datetime :finished_at + end + + change_table :good_jobs do |t| + t.uuid :batch_id + t.uuid :batch_callback_id + + t.index :batch_id, where: "batch_id IS NOT NULL" + t.index :batch_callback_id, where: "batch_callback_id IS NOT NULL" + end + end +end diff --git a/db/migrate/20240123151250_create_good_job_executions.rb b/db/migrate/20240123151250_create_good_job_executions.rb new file mode 100644 index 000000000000..32723220ccec --- /dev/null +++ b/db/migrate/20240123151250_create_good_job_executions.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class CreateGoodJobExecutions < ActiveRecord::Migration[7.0] + def change + reversible do |dir| + dir.up do + # Ensure this incremental update migration is idempotent + # with monolithic install migration. + return if connection.table_exists?(:good_job_executions) + end + end + + create_table :good_job_executions, id: :uuid do |t| + t.timestamps + + t.uuid :active_job_id, null: false + t.text :job_class + t.text :queue_name + t.jsonb :serialized_params + t.datetime :scheduled_at + t.datetime :finished_at + t.text :error + + t.index [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at + end + + change_table :good_jobs do |t| + t.boolean :is_discrete + t.integer :executions_count + t.text :job_class + end + end +end diff --git a/db/migrate/20240123151251_create_good_jobs_error_event.rb b/db/migrate/20240123151251_create_good_jobs_error_event.rb new file mode 100644 index 000000000000..b07e0f14e7f1 --- /dev/null +++ b/db/migrate/20240123151251_create_good_jobs_error_event.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateGoodJobsErrorEvent < ActiveRecord::Migration[7.0] + def change + reversible do |dir| + dir.up do + # Ensure this incremental update migration is idempotent + # with monolithic install migration. + return if connection.column_exists?(:good_jobs, :error_event) + end + end + + add_column :good_jobs, :error_event, :integer, limit: 2 + add_column :good_job_executions, :error_event, :integer, limit: 2 + end +end diff --git a/db/migrate/20240123151252_recreate_good_job_cron_indexes_with_conditional.rb b/db/migrate/20240123151252_recreate_good_job_cron_indexes_with_conditional.rb new file mode 100644 index 000000000000..aff2d4eae9a8 --- /dev/null +++ b/db/migrate/20240123151252_recreate_good_job_cron_indexes_with_conditional.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class RecreateGoodJobCronIndexesWithConditional < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + reversible do |dir| + dir.up do + unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond) + add_index :good_jobs, [:cron_key, :created_at], where: "(cron_key IS NOT NULL)", + name: :index_good_jobs_on_cron_key_and_created_at_cond, algorithm: :concurrently + end + unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at_cond) + add_index :good_jobs, [:cron_key, :cron_at], where: "(cron_key IS NOT NULL)", unique: true, + name: :index_good_jobs_on_cron_key_and_cron_at_cond, algorithm: :concurrently + end + + if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at) + remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_created_at + end + if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at) + remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_cron_at + end + end + + dir.down do + unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at) + add_index :good_jobs, [:cron_key, :created_at], + name: :index_good_jobs_on_cron_key_and_created_at, algorithm: :concurrently + end + unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at) + add_index :good_jobs, [:cron_key, :cron_at], unique: true, + name: :index_good_jobs_on_cron_key_and_cron_at, algorithm: :concurrently + end + + if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond) + remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_created_at_cond + end + if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at_cond) + remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_cron_at_cond + end + end + end + end +end diff --git a/docker-compose.yml b/docker-compose.yml index 5c77c0aa4a30..9e6ccbfdd840 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,7 +68,7 @@ services: worker: <<: *backend - command: bundle exec rake jobs:work + command: bundle exec good_job start depends_on: - db - cache diff --git a/lib/open_project/patches/delayed_job_adapter.rb b/lib/open_project/patches/delayed_job_adapter.rb deleted file mode 100644 index 1a3af2f472b1..000000000000 --- a/lib/open_project/patches/delayed_job_adapter.rb +++ /dev/null @@ -1,48 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -# This patch adds our job status extension to background jobs carried out when mailing with -# perform_later. - -module OpenProject - module Patches - module DelayedJobAdapter - module AllowNonExistingJobClass - def log_arguments? - super - rescue NameError - false - end - end - end - end -end - -ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.prepend( - OpenProject::Patches::DelayedJobAdapter::AllowNonExistingJobClass -) diff --git a/lib/open_project/patches/delivery_job.rb b/lib/open_project/patches/delivery_job.rb deleted file mode 100644 index 99f29caa0472..000000000000 --- a/lib/open_project/patches/delivery_job.rb +++ /dev/null @@ -1,40 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -# This patch adds our job status extension to background jobs carried out when mailing with -# perform_later. - -module OpenProject - module Patches - module DeliveryJob - # include ::JobStatus::ApplicationJobWithStatus - end - end -end - -ActionMailer::MailDeliveryJob.include JobStatus::ApplicationJobWithStatus diff --git a/lib/open_project/plugins/acts_as_op_engine.rb b/lib/open_project/plugins/acts_as_op_engine.rb index b7549c164d37..473d88bac8c4 100644 --- a/lib/open_project/plugins/acts_as_op_engine.rb +++ b/lib/open_project/plugins/acts_as_op_engine.rb @@ -293,13 +293,9 @@ def activity_provider(event_type, options = {}) OpenProject::Activity.register(event_type, options) end - ## - # Register a "cron"-like background job - def add_cron_jobs + def add_cron_jobs(&block) config.to_prepare do - Array(yield).each do |clz| - ::Cron::CronJob.register!(clz.is_a?(Class) ? clz : clz.to_s.constantize) - end + Rails.application.config.good_job.cron.merge!(block.call) end end diff --git a/lib/tasks/cron.rake b/lib/tasks/cron.rake index 8f76dff98809..2d153f1df01b 100644 --- a/lib/tasks/cron.rake +++ b/lib/tasks/cron.rake @@ -27,15 +27,8 @@ #++ namespace 'openproject:cron' do - desc 'An hourly cron job hook for plugin functionality' - task :hourly do - # Does nothing by default - end - - # This task will be automatically called when running jobs:work or jobs:workoff - # making sure cron jobs are scheduled. See lib/tasks/delayed_job.rake. - desc 'Ensure the cron-like background jobs are actively scheduled' + desc 'Ensure the cron-like background jobs are properly unscheduled if needed' task schedule: [:environment] do - Cron::CronJob.schedule_registered_jobs! + Storages::ManageNextcloudIntegrationJob.disable_cron_job_if_needed end end diff --git a/lib/tasks/delayed_job.rake b/lib/tasks/delayed_job.rake deleted file mode 100644 index 8ea28800017e..000000000000 --- a/lib/tasks/delayed_job.rake +++ /dev/null @@ -1,43 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -## -# Enhance the delayed_job prerequisites rake task to load the environment -unless Rake::Task.task_defined?('jobs:environment_options') && - Rake::Task['jobs:work'].prerequisites == %w(environment_options) - raise "Trying to load the full environment for delayed_job, but jobs:work seems to have changed." -end - -Rake::Task['jobs:environment_options'] - .clear_prerequisites - .enhance(['environment:full']) - -# Enhance delayed job workers to use cron -load 'lib/tasks/cron.rake' -Rake::Task["jobs:work"].enhance [:'openproject:cron:schedule'] -Rake::Task["jobs:workoff"].enhance [:'openproject:cron:schedule'] diff --git a/modules/github_integration/app/workers/cron/clear_old_pull_requests_job.rb b/modules/github_integration/app/workers/cron/clear_old_pull_requests_job.rb index f43df184e8e6..242892c5e321 100644 --- a/modules/github_integration/app/workers/cron/clear_old_pull_requests_job.rb +++ b/modules/github_integration/app/workers/cron/clear_old_pull_requests_job.rb @@ -27,12 +27,9 @@ #++ module Cron - class ClearOldPullRequestsJob < CronJob + class ClearOldPullRequestsJob < ApplicationJob priority_number :low - # runs at 1:25 nightly - self.cron_expression = '25 1 * * *' - def perform GithubPullRequest.without_work_package .find_each(&:destroy!) diff --git a/modules/github_integration/lib/open_project/github_integration/engine.rb b/modules/github_integration/lib/open_project/github_integration/engine.rb index baede67326cb..14cd9a03d6d4 100644 --- a/modules/github_integration/lib/open_project/github_integration/engine.rb +++ b/modules/github_integration/lib/open_project/github_integration/engine.rb @@ -85,9 +85,13 @@ class Engine < ::Rails::Engine mount ::API::V3::GithubPullRequests::GithubPullRequestsAPI end - config.to_prepare do - # Register the cron job to clean up old github pull requests - ::Cron::CronJob.register! ::Cron::ClearOldPullRequestsJob + add_cron_jobs do + { + ClearOldPullRequestsJob: { + cron: '25 1 * * *', # runs at 1:25 nightly + class: 'ClearOldPullRequestsJob' + } + } end end end diff --git a/modules/job_status/app/workers/job_status/cron/clear_old_job_status_job.rb b/modules/job_status/app/workers/job_status/cron/clear_old_job_status_job.rb index 2b5e87b3d183..5069d9b39ea6 100644 --- a/modules/job_status/app/workers/job_status/cron/clear_old_job_status_job.rb +++ b/modules/job_status/app/workers/job_status/cron/clear_old_job_status_job.rb @@ -28,15 +28,12 @@ module JobStatus module Cron - class ClearOldJobStatusJob < ::Cron::CronJob - # runs at 4:15 nightly - self.cron_expression = '15 4 * * *' - + class ClearOldJobStatusJob < ApplicationJob RETENTION_PERIOD = 2.days.freeze def perform ::JobStatus::Status - .where(::JobStatus::Status.arel_table[:updated_at].lteq(Time.now - RETENTION_PERIOD)) + .where(::JobStatus::Status.arel_table[:updated_at].lteq(Time.zone.now - RETENTION_PERIOD)) .destroy_all end end diff --git a/modules/job_status/lib/open_project/job_status/engine.rb b/modules/job_status/lib/open_project/job_status/engine.rb index 66b9fe7afb85..e5147bde3889 100644 --- a/modules/job_status/lib/open_project/job_status/engine.rb +++ b/modules/job_status/lib/open_project/job_status/engine.rb @@ -46,10 +46,16 @@ class Engine < ::Rails::Engine "#{root}/job_statuses/#{uuid}" end - config.to_prepare do - # Register the cron job to clear statuses periodically - ::Cron::CronJob.register! ::JobStatus::Cron::ClearOldJobStatusJob + add_cron_jobs do + { + 'JobStatus::Cron::ClearOldJobStatusJob': { + cron: '15 4 * * *', # runs at 4:15 nightly + class: '::JobStatus::Cron::ClearOldJobStatusJob' + } + } + end + config.to_prepare do # Extends the ActiveJob adapter in use (DelayedJob) by a Status which lives # indenpendently from the job itself (which is deleted once successful or after max attempts). # That way, the result of a background job is available even after the original job is gone. diff --git a/modules/ldap_groups/app/workers/ldap_groups/synchronization_job.rb b/modules/ldap_groups/app/workers/ldap_groups/synchronization_job.rb index e2fdf2525925..afde20b08a36 100644 --- a/modules/ldap_groups/app/workers/ldap_groups/synchronization_job.rb +++ b/modules/ldap_groups/app/workers/ldap_groups/synchronization_job.rb @@ -27,10 +27,7 @@ #++ module LdapGroups - class SynchronizationJob < ::Cron::CronJob - # Run every 30 minutes - self.cron_expression = '*/30 * * * *' - + class SynchronizationJob < ApplicationJob def perform return unless EnterpriseToken.allows_to?(:ldap_groups) return if skipped? diff --git a/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb b/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb index e462c7464ea8..6bb2faff47dd 100644 --- a/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb +++ b/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb @@ -19,7 +19,14 @@ class Engine < ::Rails::Engine enterprise_feature: 'ldap_groups' end - add_cron_jobs { LdapGroups::SynchronizationJob } + add_cron_jobs do + { + 'Ldap::SynchronizationJob': { + cron: '30 23 * * *', # Run once per night at 11:30pm + class: 'Ldap::SynchronizationJob' + } + } + end patches %i[LdapAuthSource Group User] end diff --git a/modules/storages/app/workers/storages/cleanup_uncontainered_file_links_job.rb b/modules/storages/app/workers/storages/cleanup_uncontainered_file_links_job.rb index 0f47e0d6ef77..cc5dbf1048c1 100644 --- a/modules/storages/app/workers/storages/cleanup_uncontainered_file_links_job.rb +++ b/modules/storages/app/workers/storages/cleanup_uncontainered_file_links_job.rb @@ -26,15 +26,13 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Storages::CleanupUncontaineredFileLinksJob < Cron::CronJob +class Storages::CleanupUncontaineredFileLinksJob < ApplicationJob queue_with_priority :low - self.cron_expression = '06 22 * * *' - def perform Storages::FileLink .where(container: nil) - .where('created_at <= ?', Time.current - OpenProject::Configuration.attachments_grace_period.minutes) + .where("created_at <= ?", Time.current - OpenProject::Configuration.attachments_grace_period.minutes) .delete_all end end diff --git a/modules/storages/app/workers/storages/manage_nextcloud_integration_cron_job.rb b/modules/storages/app/workers/storages/manage_nextcloud_integration_cron_job.rb deleted file mode 100644 index 8114d355a664..000000000000 --- a/modules/storages/app/workers/storages/manage_nextcloud_integration_cron_job.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Storages - class ManageNextcloudIntegrationCronJob < Cron::CronJob - include ManageNextcloudIntegrationJobMixin - - queue_with_priority :low - - self.cron_expression = '*/5 * * * *' - - def self.ensure_scheduled! - if ::Storages::ProjectStorage.active_nextcloud_automatically_managed.exists? - super - else - remove - end - end - end -end diff --git a/modules/storages/app/workers/storages/manage_nextcloud_integration_events_job.rb b/modules/storages/app/workers/storages/manage_nextcloud_integration_events_job.rb deleted file mode 100644 index 89485d35f3c0..000000000000 --- a/modules/storages/app/workers/storages/manage_nextcloud_integration_events_job.rb +++ /dev/null @@ -1,63 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Storages - class ManageNextcloudIntegrationEventsJob < ApplicationJob - include ManageNextcloudIntegrationJobMixin - - SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds.freeze - MULTI_THREAD_DEBOUNCE_TIME = 5.seconds.freeze - KEY = :manage_nextcloud_integration_events_job_debounce_happend_at - - queue_with_priority :above_normal - - class << self - def debounce - unless debounce_happend_in_current_thread_recently? - Rails.cache.fetch(KEY, expires_in: MULTI_THREAD_DEBOUNCE_TIME) do - set(wait: MULTI_THREAD_DEBOUNCE_TIME).perform_later - RequestStore.store[KEY] = Time.current - end - end - end - - private - - def debounce_happend_in_current_thread_recently? - timestamp = RequestStore.store[KEY] - timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current - end - end - - def perform - lock_obtained = super - self.class.debounce unless lock_obtained - lock_obtained - end - end -end diff --git a/modules/storages/app/workers/storages/manage_nextcloud_integration_job.rb b/modules/storages/app/workers/storages/manage_nextcloud_integration_job.rb new file mode 100644 index 000000000000..70d0e54126e9 --- /dev/null +++ b/modules/storages/app/workers/storages/manage_nextcloud_integration_job.rb @@ -0,0 +1,95 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + class ManageNextcloudIntegrationJob < ApplicationJob + include GoodJob::ActiveJobExtensions::Concurrency + using ::Storages::Peripherals::ServiceResultRefinements + + good_job_control_concurrency_with( + total_limit: 2, + enqueue_limit: 1, + perform_limit: 1, + key: "ManageNextcloudIntegrationJob" + ) + SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds.freeze + KEY = :manage_nextcloud_integration_job_debounce_happend_at + CRON_JOB_KEY = :'Storages::ManageNextcloudIntegrationJob' + + queue_with_priority :above_normal + + class << self + def debounce + if debounce_happend_in_current_thread_recently? + false + else + # TODO: + # Why there is 5 seconds delay? + # it is like that because for 1 thread and if there is no delay more than + # SINGLE_THREAD_DEBOUNCE_TIME(4.seconds) + # then some events can be lost + # + # Possibly "true" solutions are: + # 1. have after_request middleware to schedule one job after a request cycle + # 2. use concurrent ruby to have 'true' debounce. + result = set(wait: 5.seconds).perform_later + RequestStore.store[KEY] = Time.current + result + end + end + + def disable_cron_job_if_needed + if ::Storages::ProjectStorage.active_nextcloud_automatically_managed.exists? + GoodJob::Setting.cron_key_enable(CRON_JOB_KEY) + else + GoodJob::Setting.cron_key_disable(CRON_JOB_KEY) + end + end + + private + + def debounce_happend_in_current_thread_recently? + timestamp = RequestStore.store[KEY] + timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current + end + end + + def perform + ::Storages::NextcloudStorage + .automatic_management_enabled + .includes(:oauth_client) + .find_each do |storage| + result = NextcloudGroupFolderPropertiesSyncService.call(storage) + result.match( + on_success: ->(_) { storage.mark_as_healthy }, + on_failure: ->(errors) { storage.mark_as_unhealthy(reason: errors.to_s) } + ) + end + end + end +end diff --git a/modules/storages/app/workers/storages/manage_nextcloud_integration_job_mixin.rb b/modules/storages/app/workers/storages/manage_nextcloud_integration_job_mixin.rb deleted file mode 100644 index 4808269c41c7..000000000000 --- a/modules/storages/app/workers/storages/manage_nextcloud_integration_job_mixin.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Storages - module ManageNextcloudIntegrationJobMixin - using Peripherals::ServiceResultRefinements - - def perform - OpenProject::Mutex.with_advisory_lock( - ::Storages::NextcloudStorage, - 'sync_all_group_folders', - timeout_seconds: 0, - transaction: false - ) do - ::Storages::NextcloudStorage.automatic_management_enabled.includes(:oauth_client).find_each do |storage| - result = NextcloudGroupFolderPropertiesSyncService.call(storage) - result.match( - on_success: ->(_) do - storage.mark_as_healthy - end, - on_failure: ->(errors) do - storage.mark_as_unhealthy(reason: errors.to_s) - end - ) - end - true - end - end - end -end diff --git a/modules/storages/db/migrate/20231009135807_remove_renamed_cronjobs.rb b/modules/storages/db/migrate/20231009135807_remove_renamed_cronjobs.rb deleted file mode 100644 index b4042f391e2e..000000000000 --- a/modules/storages/db/migrate/20231009135807_remove_renamed_cronjobs.rb +++ /dev/null @@ -1,36 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class RemoveRenamedCronjobs < ActiveRecord::Migration[7.0] - def up - Delayed::Job.where("handler LIKE '%job_class: CleanupUncontaineredFileLinksJob%'").delete_all - Delayed::Job.where("handler LIKE '%job_class: ManageNextcloudIntegrationJob%'").delete_all - end - - def down; end -end diff --git a/modules/storages/lib/open_project/storages/engine.rb b/modules/storages/lib/open_project/storages/engine.rb index 146879d293ec..82f14fa49701 100644 --- a/modules/storages/lib/open_project/storages/engine.rb +++ b/modules/storages/lib/open_project/storages/engine.rb @@ -46,11 +46,11 @@ def self.permissions # please see comments inside ActsAsOpEngine class include OpenProject::Plugins::ActsAsOpEngine - initializer 'openproject_storages.feature_decisions' do + initializer "openproject_storages.feature_decisions" do OpenProject::FeatureDecisions.add :storage_file_picking_select_all end - initializer 'openproject_storages.event_subscriptions' do + initializer "openproject_storages.event_subscriptions" do Rails.application.config.after_initialize do [ OpenProject::Events::MEMBER_CREATED, @@ -62,29 +62,29 @@ def self.permissions OpenProject::Events::PROJECT_UNARCHIVED ].each do |event| OpenProject::Notifications.subscribe(event) do |_payload| - ::Storages::ManageNextcloudIntegrationEventsJob.debounce + ::Storages::ManageNextcloudIntegrationJob.debounce end end OpenProject::Notifications.subscribe( OpenProject::Events::OAUTH_CLIENT_TOKEN_CREATED ) do |payload| - if payload[:integration_type] == 'Storages::Storage' - ::Storages::ManageNextcloudIntegrationEventsJob.debounce + if payload[:integration_type] == "Storages::Storage" + ::Storages::ManageNextcloudIntegrationJob.debounce end end OpenProject::Notifications.subscribe( OpenProject::Events::ROLE_UPDATED ) do |payload| if payload[:permissions_diff]&.intersect?(OpenProject::Storages::Engine.permissions) - ::Storages::ManageNextcloudIntegrationEventsJob.debounce + ::Storages::ManageNextcloudIntegrationJob.debounce end end OpenProject::Notifications.subscribe( OpenProject::Events::ROLE_DESTROYED ) do |payload| if payload[:permissions]&.intersect?(OpenProject::Storages::Engine.permissions) - ::Storages::ManageNextcloudIntegrationEventsJob.debounce + ::Storages::ManageNextcloudIntegrationJob.debounce end end @@ -95,8 +95,8 @@ def self.permissions ].each do |event| OpenProject::Notifications.subscribe(event) do |payload| if payload[:project_folder_mode] == :automatic - ::Storages::ManageNextcloudIntegrationEventsJob.debounce - ::Storages::ManageNextcloudIntegrationCronJob.ensure_scheduled! + ::Storages::ManageNextcloudIntegrationJob.debounce + ::Storages::ManageNextcloudIntegrationJob.disable_cron_job_if_needed end end end @@ -106,8 +106,8 @@ def self.permissions # For documentation see the definition of register in "ActsAsOpEngine" # This corresponds to the openproject-storage.gemspec # Pass a block to the plugin (for defining permissions, menu items and the like) - register 'openproject-storages', - author_url: 'https://www.openproject.org', + register "openproject-storages", + author_url: "https://www.openproject.org", bundled: true, settings: {} do # Defines permission constraints used in the module (controller, etc.) @@ -142,14 +142,14 @@ def self.permissions # condition ("if:"), caption and icon. menu :admin_menu, :storages_admin_settings, - { controller: '/storages/admin/storages', action: :index }, + { controller: "/storages/admin/storages", action: :index }, if: Proc.new { User.current.admin? }, caption: :project_module_storages, - icon: 'hosting' + icon: "hosting" menu :project_menu, :settings_project_storages, - { controller: '/storages/admin/project_storages', action: 'index' }, + { controller: "/storages/admin/project_storages", action: "index" }, caption: :project_module_storages, parent: :settings @@ -273,21 +273,28 @@ def self.permissions end # Add api endpoints specific to this module - add_api_endpoint 'API::V3::Root' do + add_api_endpoint "API::V3::Root" do mount ::API::V3::Storages::StoragesAPI mount ::API::V3::ProjectStorages::ProjectStoragesAPI mount ::API::V3::FileLinks::FileLinksAPI end - add_api_endpoint 'API::V3::WorkPackages::WorkPackagesAPI', :id do + add_api_endpoint "API::V3::WorkPackages::WorkPackagesAPI", :id do mount ::API::V3::FileLinks::WorkPackagesFileLinksAPI end add_cron_jobs do - [ - Storages::CleanupUncontaineredFileLinksJob, - Storages::ManageNextcloudIntegrationCronJob - ] + { + 'Storages::CleanupUncontaineredFileLinksJob': { + cron: "06 22 * * *", + class: "Storages::CleanupUncontaineredFileLinksJob" + }, + + 'Storages::ManageNextcloudIntegrationJob': { + cron: "*/5 * * * *", + class: "Storages::ManageNextcloudIntegrationJob" + } + } end end end diff --git a/modules/storages/spec/workers/storages/cleanup_uncontainered_file_links_job_spec.rb b/modules/storages/spec/workers/storages/cleanup_uncontainered_file_links_job_spec.rb index 7f6ee5ad25d2..a5924b5a7464 100644 --- a/modules/storages/spec/workers/storages/cleanup_uncontainered_file_links_job_spec.rb +++ b/modules/storages/spec/workers/storages/cleanup_uncontainered_file_links_job_spec.rb @@ -32,39 +32,37 @@ require_module_spec_helper RSpec.describe Storages::CleanupUncontaineredFileLinksJob, type: :job do - it 'has a schedule set' do - expect(described_class.cron_expression).to eq('06 22 * * *') - end - - it 'removes uncontainered file_links which are old enough' do - grace_period = 10 - allow(OpenProject::Configuration) - .to receive(:attachments_grace_period) - .and_return(grace_period) + describe '#perfrom' do + it 'removes uncontainered file_links which are old enough' do + grace_period = 10 + allow(OpenProject::Configuration) + .to receive(:attachments_grace_period) + .and_return(grace_period) - expect(Storages::FileLink.count).to eq(0) + expect(Storages::FileLink.count).to eq(0) - uncontainered_old = create(:file_link, - container_id: nil, - container_type: nil, - created_at: Time.current - grace_period.minutes - 1.second) - uncontainered_young = create(:file_link, + uncontainered_old = create(:file_link, container_id: nil, - container_type: nil) - containered_old = create(:file_link, - container_id: 1, - created_at: Time.current - grace_period.minutes - 1.second) - containered_young = create(:file_link, - container_id: 1) + container_type: nil, + created_at: Time.current - grace_period.minutes - 1.second) + uncontainered_young = create(:file_link, + container_id: nil, + container_type: nil) + containered_old = create(:file_link, + container_id: 1, + created_at: Time.current - grace_period.minutes - 1.second) + containered_young = create(:file_link, + container_id: 1) - expect(Storages::FileLink.count).to eq(4) + expect(Storages::FileLink.count).to eq(4) - described_class.new.perform + described_class.new.perform - expect(Storages::FileLink.count).to eq(3) - file_link_ids = Storages::FileLink.pluck(:id).sort - expected_file_link_ids = [uncontainered_young.id, containered_old.id, containered_young.id].sort - expect(file_link_ids).to eq(expected_file_link_ids) - expect(file_link_ids).not_to include(uncontainered_old.id) + expect(Storages::FileLink.count).to eq(3) + file_link_ids = Storages::FileLink.pluck(:id).sort + expected_file_link_ids = [uncontainered_young.id, containered_old.id, containered_young.id].sort + expect(file_link_ids).to eq(expected_file_link_ids) + expect(file_link_ids).not_to include(uncontainered_old.id) + end end end diff --git a/modules/storages/spec/workers/storages/manage_nextcloud_integration_cron_job_spec.rb b/modules/storages/spec/workers/storages/manage_nextcloud_integration_cron_job_spec.rb deleted file mode 100644 index 0548e9f4e2af..000000000000 --- a/modules/storages/spec/workers/storages/manage_nextcloud_integration_cron_job_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require 'spec_helper' -require_module_spec_helper - -RSpec.describe Storages::ManageNextcloudIntegrationCronJob, :webmock, type: :job do - it 'has a schedule set' do - expect(described_class.cron_expression).to eq('*/5 * * * *') - end - - describe '.ensure_scheduled!' do - before { ActiveJob::Base.disable_test_adapter } - - subject { described_class.ensure_scheduled! } - - context 'when there is active nextcloud project storage' do - shared_let(:storage1) { create(:nextcloud_storage, :as_automatically_managed) } - shared_let(:project_storage) { create(:project_storage, :as_automatically_managed, storage: storage1) } - - it 'schedules cron_job if not scheduled' do - expect(described_class.scheduled?).to be(false) - expect(described_class.delayed_job_query.count).to eq(0) - - subject - - expect(described_class.scheduled?).to be(true) - expect(described_class.delayed_job_query.count).to eq(1) - end - - it 'does not schedules cron_job if already scheduled' do - described_class.ensure_scheduled! - expect(described_class.scheduled?).to be(true) - expect(described_class.delayed_job_query.count).to eq(1) - - subject - - expect(described_class.scheduled?).to be(true) - expect(described_class.delayed_job_query.count).to eq(1) - end - end - - context 'when there is no active nextcloud project storage' do - it 'does nothing but removes cron_job' do - described_class.set(cron: described_class.cron_expression).perform_later - expect(described_class.scheduled?).to be(true) - expect(described_class.delayed_job_query.count).to eq(1) - - subject - - expect(described_class.scheduled?).to be(false) - expect(described_class.delayed_job_query.count).to eq(0) - end - end - end - - describe '.perform' do - subject { described_class.new.perform } - - context 'when lock is free' do - it 'responds with true' do - expect(subject).to be(true) - end - - it 'calls GroupFolderPropertiesSyncService for each automatically managed storage' do - storage1 = create(:nextcloud_storage, :as_automatically_managed) - storage2 = create(:nextcloud_storage, :as_not_automatically_managed) - - allow(Storages::NextcloudGroupFolderPropertiesSyncService) - .to receive(:call).with(storage1).and_return(ServiceResult.success) - - expect(subject).to be(true) - - expect(Storages::NextcloudGroupFolderPropertiesSyncService).to have_received(:call).with(storage1).once - expect(Storages::NextcloudGroupFolderPropertiesSyncService).not_to have_received(:call).with(storage2) - end - - it 'marks storage as healthy if sync was successful' do - storage1 = create(:nextcloud_storage, :as_automatically_managed) - - allow(Storages::NextcloudGroupFolderPropertiesSyncService) - .to receive(:call).with(storage1).and_return(ServiceResult.success) - - Timecop.freeze('2023-03-14T15:17:00Z') do - expect do - subject - storage1.reload - end.to( - change(storage1, :health_changed_at).to(Time.now.utc) - .and(change(storage1, :health_status).from('pending').to('healthy')) - ) - end - end - - it 'marks storage as unhealthy if sync was unsuccessful' do - storage1 = create(:nextcloud_storage, :as_automatically_managed) - - allow(Storages::NextcloudGroupFolderPropertiesSyncService) - .to receive(:call).with(storage1).and_return(ServiceResult.failure(errors: Storages::StorageError.new(code: :not_found))) - - Timecop.freeze('2023-03-14T15:17:00Z') do - expect do - subject - storage1.reload - end.to( - change(storage1, :health_changed_at).to(Time.now.utc) - .and(change(storage1, :health_status).from('pending').to('unhealthy')) - .and(change(storage1, :health_reason).from(nil).to('not_found')) - ) - end - end - end - - context 'when lock is unfree' do - it 'responds with false' do - allow(ApplicationRecord).to receive(:with_advisory_lock).and_return(false) - - expect(subject).to be(false) - expect(ApplicationRecord).to have_received(:with_advisory_lock).with( - 'sync_all_group_folders', - timeout_seconds: 0, - transaction: false - ).once - end - end - end -end diff --git a/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb b/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb deleted file mode 100644 index 4a2001334264..000000000000 --- a/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require 'spec_helper' -require_module_spec_helper - -RSpec.describe Storages::ManageNextcloudIntegrationEventsJob, type: :job do - describe '.priority' do - it 'has a maximum priority' do - expect(described_class.priority).to eq(7) - end - end - - describe '.debounce' do - context 'when has been debounced by other thread' do - before do - Rails.cache.write(described_class::KEY, Time.current) - end - - it 'does nothing' do - expect { described_class.debounce }.not_to change(enqueued_jobs, :count) - end - end - - context 'when has not been debounced by other thread' do - it 'schedules a job' do - expect { described_class.debounce }.to change(enqueued_jobs, :count).from(0).to(1) - end - - it 'hits cache once when called 1000 times in a short period of time' do - allow(Rails.cache).to receive(:fetch).and_call_original - - expect do - 1000.times { described_class.debounce } - end.to change(enqueued_jobs, :count).from(0).to(1) - - expect(Rails.cache).to have_received(:fetch).once - end - end - end - - describe '#perform' do - it 'responds with true when parent perform responds with true' do - allow(OpenProject::Mutex).to receive(:with_advisory_lock).and_return(true) - allow(described_class).to receive(:debounce) - - expect(described_class.new.perform).to be(true) - - expect(described_class).not_to have_received(:debounce) - end - - it 'debounces itself when parent perform responds with false' do - allow(OpenProject::Mutex).to receive(:with_advisory_lock).and_return(false) - allow(described_class).to receive(:debounce) - - expect(described_class.new.perform).to be(false) - - expect(described_class).to have_received(:debounce).once - end - end -end diff --git a/modules/storages/spec/workers/storages/manage_nextcloud_integration_job_spec.rb b/modules/storages/spec/workers/storages/manage_nextcloud_integration_job_spec.rb new file mode 100644 index 000000000000..4e47dbb821de --- /dev/null +++ b/modules/storages/spec/workers/storages/manage_nextcloud_integration_job_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require_module_spec_helper + +RSpec.describe Storages::ManageNextcloudIntegrationJob, :webmock, type: :job do + describe '.debounce' do + context 'when has been debounced by other thread' do + before { ActiveJob::Base.disable_test_adapter } + + it 'does not change the number of enqueued jobs' do + expect(GoodJob::Job.count).to eq(0) + expect(described_class.perform_later.successfully_enqueued?).to be(true) + expect(described_class.perform_later).to be(false) + expect(GoodJob::Job.count).to eq(1) + + expect { described_class.debounce }.not_to change(GoodJob::Job, :count) + end + end + + context 'when has not been debounced by other thread' do + it 'schedules a job' do + expect { described_class.debounce }.to change(enqueued_jobs, :count).from(0).to(1) + end + + it 'tries to schedule once when called 1000 times in a short period of time' do + expect_any_instance_of(ActiveJob::ConfiguredJob) + .to receive(:perform_later).once.and_call_original + + expect do + 1000.times { described_class.debounce } + end.to change(enqueued_jobs, :count).from(0).to(1) + end + end + end + + describe '.disable_cron_job_if_needed' do + before { ActiveJob::Base.disable_test_adapter } + + subject { described_class.disable_cron_job_if_needed } + + context 'when there is an active nextcloud project storage' do + shared_let(:storage1) { create(:nextcloud_storage, :as_automatically_managed) } + shared_let(:project_storage) { create(:project_storage, :as_automatically_managed, storage: storage1) } + + it 'enables the cron_job if was disabled before' do + GoodJob::Setting.cron_key_disable(described_class::CRON_JOB_KEY) + + good_job_setting = GoodJob::Setting.first + expect(good_job_setting.key).to eq("cron_keys_disabled") + expect(good_job_setting.value).to eq(["Storages::ManageNextcloudIntegrationJob"]) + + expect { subject }.not_to change(GoodJob::Setting, :count).from(1) + + good_job_setting.reload + expect(good_job_setting.key).to eq("cron_keys_disabled") + expect(good_job_setting.value).to eq([]) + end + + it 'does nothing if the cron_job is not disabled' do + expect(GoodJob::Setting.cron_key_enabled?(described_class::CRON_JOB_KEY)).to be(true) + + expect { subject }.not_to change(GoodJob::Setting, :count).from(0) + + expect(GoodJob::Setting.cron_key_enabled?(described_class::CRON_JOB_KEY)).to be(true) + end + end + + context 'when there is no active nextcloud project storage' do + it 'disables the cron job' do + expect { subject }.to change(GoodJob::Setting, :count).from(0).to(1) + + good_job_setting = GoodJob::Setting.first + expect(good_job_setting.key).to eq("cron_keys_disabled") + expect(good_job_setting.value).to eq(["Storages::ManageNextcloudIntegrationJob"]) + end + end + end + + describe '.perform' do + subject { described_class.new.perform } + + it 'calls NextcloudGroupFolderPropertiesSyncService for each automatically managed storage' do + storage1 = create(:nextcloud_storage, :as_automatically_managed) + storage2 = create(:nextcloud_storage, :as_not_automatically_managed) + + allow(Storages::NextcloudGroupFolderPropertiesSyncService) + .to receive(:call).with(storage1).and_return(ServiceResult.success) + + subject + + expect(Storages::NextcloudGroupFolderPropertiesSyncService).to have_received(:call).with(storage1).once + expect(Storages::NextcloudGroupFolderPropertiesSyncService).not_to have_received(:call).with(storage2) + end + + it 'marks storage as healthy if sync was successful' do + storage1 = create(:nextcloud_storage, :as_automatically_managed) + + allow(Storages::NextcloudGroupFolderPropertiesSyncService) + .to receive(:call).with(storage1).and_return(ServiceResult.success) + + Timecop.freeze('2023-03-14T15:17:00Z') do + expect do + subject + storage1.reload + end.to( + change(storage1, :health_changed_at).to(Time.now.utc) + .and(change(storage1, :health_status).from('pending').to('healthy')) + ) + end + end + + it 'marks storage as unhealthy if sync was unsuccessful' do + storage1 = create(:nextcloud_storage, :as_automatically_managed) + + allow(Storages::NextcloudGroupFolderPropertiesSyncService) + .to receive(:call).with(storage1).and_return(ServiceResult.failure(errors: Storages::StorageError.new(code: :not_found))) + + Timecop.freeze('2023-03-14T15:17:00Z') do + expect do + subject + storage1.reload + end.to( + change(storage1, :health_changed_at).to(Time.now.utc) + .and(change(storage1, :health_status).from('pending').to('unhealthy')) + .and(change(storage1, :health_reason).from(nil).to('not_found')) + ) + end + end + end +end diff --git a/modules/webhooks/app/workers/cleanup_webhook_logs_job.rb b/modules/webhooks/app/workers/cleanup_webhook_logs_job.rb index 4a59ae264cef..c3b310737107 100644 --- a/modules/webhooks/app/workers/cleanup_webhook_logs_job.rb +++ b/modules/webhooks/app/workers/cleanup_webhook_logs_job.rb @@ -26,10 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CleanupWebhookLogsJob < Cron::CronJob - # runs at 5:28 on Sunday - self.cron_expression = '28 5 * * 7' - +class CleanupWebhookLogsJob < ApplicationJob # Clean any logs older than 7 days def perform ::Webhooks::Log diff --git a/modules/webhooks/lib/open_project/webhooks/engine.rb b/modules/webhooks/lib/open_project/webhooks/engine.rb index 5bd5cd54914d..ead276ce7706 100644 --- a/modules/webhooks/lib/open_project/webhooks/engine.rb +++ b/modules/webhooks/lib/open_project/webhooks/engine.rb @@ -51,6 +51,13 @@ class Engine < ::Rails::Engine end end - add_cron_jobs { CleanupWebhookLogsJob } + add_cron_jobs do + { + CleanupWebhookLogsJob: { + cron: '28 5 * * 7', # runs at 5:28 on Sunday + class: 'CleanupWebhookLogsJob' + } + } + end end end diff --git a/spec/contracts/settings/working_days_params_contract_spec.rb b/spec/contracts/settings/working_days_params_contract_spec.rb index 79b995423c68..f67f074e0a21 100644 --- a/spec/contracts/settings/working_days_params_contract_spec.rb +++ b/spec/contracts/settings/working_days_params_contract_spec.rb @@ -37,13 +37,6 @@ let(:contract) do described_class.new(setting, current_user, params:) end - let(:apply_job_scheduled) { false } - - before do - allow(WorkPackages::ApplyWorkingDaysChangeJob) - .to receive(:scheduled?) - .and_return(apply_job_scheduled) - end it_behaves_like 'contract is valid for active admins and invalid for regular users' @@ -55,7 +48,10 @@ context 'with an ApplyWorkingDaysChangeJob already existing' do let(:params) { { working_days: [1, 2, 3] } } - let(:apply_job_scheduled) { true } + before do + ActiveJob::Base.disable_test_adapter + WorkPackages::ApplyWorkingDaysChangeJob.perform_later + end include_examples 'contract is invalid', base: :previous_working_day_changes_unprocessed end diff --git a/spec/controllers/roles_controller_spec.rb b/spec/controllers/roles_controller_spec.rb index 74cb660e332d..b0ed566a7809 100644 --- a/spec/controllers/roles_controller_spec.rb +++ b/spec/controllers/roles_controller_spec.rb @@ -387,7 +387,7 @@ subject expect(enqueued_jobs.count).to eq(1) - expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationJob) expect(response).to redirect_to roles_path expect(Role.count).to eq(0) end diff --git a/spec/features/admin/working_days_spec.rb b/spec/features/admin/working_days_spec.rb index aa049ff7d01f..5b7a0c33e86e 100644 --- a/spec/features/admin/working_days_spec.rb +++ b/spec/features/admin/working_days_spec.rb @@ -163,11 +163,8 @@ def working_days_setting it 'shows an error when a previous change to the working days configuration isn\'t processed yet' do # Have a job already scheduled - # Attempting to set the job via simply using the UI would require to change the test setup of how - # delayed jobs are handled. - ActiveJob::QueueAdapters::DelayedJobAdapter - .new - .enqueue(WorkPackages::ApplyWorkingDaysChangeJob.new(user_id: 5)) + ActiveJob::Base.disable_test_adapter + WorkPackages::ApplyWorkingDaysChangeJob.perform_later(user_id: 5) uncheck 'Tuesday' click_on 'Apply changes' diff --git a/spec/features/notifications/reminder_mail_spec.rb b/spec/features/notifications/reminder_mail_spec.rb index 1ffc320e21a0..680b6e36fe5c 100644 --- a/spec/features/notifications/reminder_mail_spec.rb +++ b/spec/features/notifications/reminder_mail_spec.rb @@ -9,10 +9,10 @@ let(:work_package) { create(:work_package, project:) } let(:watched_work_package) { create(:work_package, project:, watcher_users: [current_user]) } let(:involved_work_package) { create(:work_package, project:, assigned_to: current_user) } - # The run_at time of the delayed job used for scheduling the reminder mails + # GoodJob::Job#scheduled_at is used for scheduling the reminder mails # needs to be within a time frame eligible for sending out mails for the chose # time zone. For the time zone Hawaii (UTC-10) this means between 8:00:00 and 8:14:59 UTC. - # The job is scheduled to run every 15 min so the run_at will in production always move between the quarters of an hour. + # The job is scheduled to run every 15 min so the scheduled_at will in production always move between the quarters of an hour. # The current time can be way behind that. let(:current_utc_time) { ActiveSupport::TimeZone['Pacific/Honolulu'].parse("2021-09-30T08:34:10").utc } let(:job_run_at) { ActiveSupport::TimeZone['Pacific/Honolulu'].parse("2021-09-30T08:00:00").utc } @@ -57,13 +57,10 @@ work_package involved_work_package - ActiveJob::Base.queue_adapter.enqueued_jobs.clear + ActiveJob::Base.disable_test_adapter - # There is no delayed_job associated when using the testing backend of ActiveJob - # so we have to mock it. - allow(Notifications::ScheduleReminderMailsJob) - .to receive(:delayed_job) - .and_return(instance_double(Delayed::Backend::ActiveRecord::Job, run_at: job_run_at)) + scheduled_job = Notifications::ScheduleReminderMailsJob.perform_later + GoodJob::Job.where(id: scheduled_job.job_id).update_all(scheduled_at: job_run_at) end it 'sends a digest mail based on the configuration', with_settings: { journal_aggregation_time_minutes: 0 } do @@ -88,13 +85,9 @@ involved_work_package.save! end - # The Job is triggered by time so we mock it and the jobs started by it being triggered - Notifications::ScheduleReminderMailsJob.perform_later - 2.times { perform_enqueued_jobs } - - expect(ActionMailer::Base.deliveries.length) - .to be 1 + 2.times { GoodJob.perform_inline } + expect(ActionMailer::Base.deliveries.length).to be 1 expect(ActionMailer::Base.deliveries.first.subject) .to eql "OpenProject - 1 unread notification including a mention" end diff --git a/spec/features/projects/destroy_spec.rb b/spec/features/projects/destroy_spec.rb index ff5e8e45b303..ab8ad34473f2 100644 --- a/spec/features/projects/destroy_spec.rb +++ b/spec/features/projects/destroy_spec.rb @@ -35,26 +35,17 @@ current_user { create(:admin) } - before do - # Disable background worker - allow(Delayed::Worker) - .to receive(:delay_jobs) - .and_return(false) - - project_page.visit! - end + before { project_page.visit! } it 'destroys the project' do # Confirm the deletion # Without confirmation, the button is disabled - expect(danger_zone) - .to be_disabled + expect(danger_zone).to be_disabled # With wrong confirmation, the button is disabled danger_zone.confirm_with("#{project.identifier}_wrong") - expect(danger_zone) - .to be_disabled + expect(danger_zone).to be_disabled # With correct confirmation, the button is enabled # and the project can be deleted @@ -63,6 +54,7 @@ danger_zone.danger_button.click expect(page).to have_css '.op-toast.-success', text: I18n.t('projects.delete.scheduled') + expect(project.reload).to eq(project) perform_enqueued_jobs diff --git a/spec/lib/open_project/events_spec.rb b/spec/lib/open_project/events_spec.rb index b56d87e5342b..80713097a8b0 100644 --- a/spec/lib/open_project/events_spec.rb +++ b/spec/lib/open_project/events_spec.rb @@ -37,7 +37,7 @@ def fire_event(event_constant_name) end before do - allow(Storages::ManageNextcloudIntegrationEventsJob).to receive(:debounce) + allow(Storages::ManageNextcloudIntegrationJob).to receive(:debounce) end %w[ @@ -53,7 +53,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).not_to have_received(:debounce) end end @@ -62,16 +62,13 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).to have_received(:debounce) end it do - # Check for message being called instead of checking the job arrival to the queue - # because it is not simple adding to the queue in ::Storages::ManageNextcloudIntegrationCronJob.ensure_scheduled! - # With this method cron_job can be actually removed if not needed. - allow(Storages::ManageNextcloudIntegrationCronJob).to receive(:ensure_scheduled!) + allow(Storages::ManageNextcloudIntegrationJob).to receive(:disable_cron_job_if_needed) subject - expect(Storages::ManageNextcloudIntegrationCronJob).to have_received(:ensure_scheduled!) + expect(Storages::ManageNextcloudIntegrationJob).to have_received(:disable_cron_job_if_needed) end end end @@ -93,7 +90,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).to have_received(:debounce) end end end @@ -106,7 +103,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).not_to have_received(:debounce) end end @@ -115,7 +112,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).to have_received(:debounce) end end end @@ -128,7 +125,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).not_to have_received(:debounce) end end @@ -137,7 +134,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).to have_received(:debounce) end end end @@ -150,7 +147,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).not_to have_received(:debounce) end end @@ -159,7 +156,7 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) + expect(Storages::ManageNextcloudIntegrationJob).to have_received(:debounce) end end end diff --git a/spec/seeders/setting_seeder_spec.rb b/spec/seeders/setting_seeder_spec.rb index f62a35adc8a0..0dcc57754564 100644 --- a/spec/seeders/setting_seeder_spec.rb +++ b/spec/seeders/setting_seeder_spec.rb @@ -36,11 +36,6 @@ let(:new_project_role) { basic_seed_data.find_reference(:default_role_project_admin) } let(:closed_status) { basic_seed_data.find_reference(:default_status_closed) } - before do - allow(ActionMailer::Base).to receive(:perform_deliveries).and_return(false) - allow(Delayed::Worker).to receive(:delay_jobs).and_return(false) - end - it 'applies initial settings' do expect(setting_seeder).to be_applicable diff --git a/spec/workers/non_existing_job_class_spec.rb b/spec/workers/non_existing_job_class_spec.rb deleted file mode 100644 index d63caa8cbee6..000000000000 --- a/spec/workers/non_existing_job_class_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require 'spec_helper' - -RSpec.describe "NonExistingJobClass" do - let!(:job_with_non_existing_class) do - handler = <<~JOB.strip - --- !ruby/object:ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper - job_data: - job_class: WhichShallNotBeNamedJob - job_id: 8f72c3c9-a1e0-4e46-b0f2-b517288bb76c - provider_job_id: - queue_name: default - priority: 5 - arguments: - - 42 - executions: 0 - exception_executions: {} - locale: en - timezone: UTC - enqueued_at: '2022-12-05T09:41:39Z' - JOB - Delayed::Job.create(handler:) - end - - before do - # allow to inspect the job is marked as failed after failure in the test - allow(Delayed::Worker).to receive(:destroy_failed_jobs).and_return(false) - end - - it 'does not crash the worker when processed' do - expect { Delayed::Worker.new(exit_on_complete: true).start } - .not_to raise_error - - job_with_non_existing_class.reload - expect(job_with_non_existing_class.last_error).to include("uninitialized constant WhichShallNotBeNamedJob") - expect(job_with_non_existing_class.failed_at).to be_within(1).of(Time.current) - end -end diff --git a/spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb b/spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb index fe29cb487635..eb434f549376 100644 --- a/spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb +++ b/spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb @@ -32,42 +32,30 @@ include ActiveSupport::Testing::TimeHelpers shared_let(:project) { create(:project, name: 'main') } - # Paris and Berlin are both UTC+01:00 (CET) or UTC+02:00 (CEST) shared_let(:timezone_paris) { ActiveSupport::TimeZone['Europe/Paris'] } # Kathmandu is UTC+05:45 (no DST) shared_let(:timezone_kathmandu) { ActiveSupport::TimeZone['Asia/Kathmandu'] } - shared_let(:user_paris) do - create( - :user, - firstname: 'Paris', - preferences: { time_zone: timezone_paris.name } - ) + create(:user, + firstname: 'Paris', + preferences: { time_zone: timezone_paris.name }) end shared_let(:user_kathmandu) do - create( - :user, - firstname: 'Kathmandu', - preferences: { time_zone: timezone_kathmandu.name } - ) + create(:user, + firstname: 'Kathmandu', + preferences: { time_zone: timezone_kathmandu.name }) end - let(:schedule_job) do - described_class.ensure_scheduled! - described_class.delayed_job - end + let(:scheduled_job) { described_class.perform_later } before do - # We need to access the job as stored in the database to get at the run_at time persisted there - allow(ActiveJob::Base) - .to receive(:queue_adapter) - .and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new) - schedule_job + ActiveJob::Base.disable_test_adapter + scheduled_job end - def set_scheduled_time(run_at) - schedule_job.update_column(:run_at, run_at) + def set_scheduled_time(scheduled_at) + GoodJob::Job.where(id: scheduled_job.job_id).update_all(scheduled_at:) end # Converts "hh:mm" into { hour: h, min: m } @@ -82,28 +70,25 @@ def timezone_time(time, timezone) def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris) set_scheduled_time(timezone_time(scheduled_at, timezone)) travel_to(timezone_time(local_time, timezone)) do - schedule_job.reload.invoke_job + GoodJob.perform_inline + # scheduled_job.reload.invoke_job yield if block_given? end end - def deserialized_of_job(job) - deserializer_class = Class.new do - include(ActiveJob::Arguments) - end - - deserializer = deserializer_class.new - - deserializer.deserialize(job.payload_object.job_data).to_h + def deserialize_job(job) + deserializer_class = Class.new { include(ActiveJob::Arguments) } + deserializer_class.new + .deserialize(job.serialized_params) + .to_h end - def expect_job(job, klass, *arguments) - job_data = deserialized_of_job(job) - expect(job_data['job_class']) - .to eql klass - expect(job_data['arguments']) - .to match_array arguments + def expect_job(job, *arguments) + job_data = deserialize_job(job) + expect(job_data['job_class']).to eql(job.job_class) + expect(job_data['arguments']).to match_array arguments + expect(job_data['executions']).to eq 0 end shared_examples_for 'job execution creates date alerts creation job' do @@ -115,9 +100,12 @@ def expect_job(job, klass, *arguments) it 'creates the job for the user' do expect do run_job(timezone:, scheduled_at:, local_time:) do - expect_job(Delayed::Job.last, "Notifications::CreateDateAlertsNotificationsJob", user) + j = GoodJob::Job.where(job_class: "Notifications::CreateDateAlertsNotificationsJob") + .order(created_at: :desc) + .last + expect_job(j, user) end - end.to change(Delayed::Job, :count).by 1 + end.to change(GoodJob::Job, :count).by 1 end end @@ -129,7 +117,7 @@ def expect_job(job, klass, *arguments) it 'creates no job' do expect do run_job(timezone:, scheduled_at:, local_time:) - end.not_to change(Delayed::Job, :count) + end.not_to change(GoodJob::Job, :count) end end diff --git a/spec/workers/notifications/schedule_reminder_mails_job_spec.rb b/spec/workers/notifications/schedule_reminder_mails_job_spec.rb index 99538d4f7b81..94c629da2ebd 100644 --- a/spec/workers/notifications/schedule_reminder_mails_job_spec.rb +++ b/spec/workers/notifications/schedule_reminder_mails_job_spec.rb @@ -29,69 +29,46 @@ require 'spec_helper' RSpec.describe Notifications::ScheduleReminderMailsJob, type: :job do - subject(:job) { scheduled_job.invoke_job } - - let(:scheduled_job) do - described_class.ensure_scheduled! - - Delayed::Job.first - end - + let(:scheduled_job) { described_class.perform_later } let(:ids) { [23, 42] } - let(:run_at) { scheduled_job.run_at } before do - # We need to access the job as stored in the database to get at the run_at time persisted there - allow(ActiveJob::Base) - .to receive(:queue_adapter) - .and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new) - - scheduled_job.update_column(:run_at, run_at) + # We need to access the job as stored in the database to get at the scheduled_at time persisted there + ActiveJob::Base.disable_test_adapter + scheduled_job scope = instance_double(ActiveRecord::Relation) - allow(User) - .to receive(:having_reminder_mail_to_send) - .and_return(scope) - - allow(scope) - .to receive(:pluck) - .with(:id) - .and_return(ids) + allow(User).to receive(:having_reminder_mail_to_send).and_return(scope) + allow(scope).to receive(:pluck).with(:id).and_return(ids) end describe '#perform' do shared_examples_for 'schedules reminder mails' do it 'schedules reminder jobs for every user with a reminder mails to be sent' do - expect { subject } - .to change(Delayed::Job, :count) - .by(2) - - jobs = Delayed::Job.all.map do |job| - YAML.safe_load(job.handler, permitted_classes: [ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper]) - end - - reminder_jobs = jobs.select { |job| job.job_data['job_class'] == "Mails::ReminderJob" } + expect { GoodJob.perform_inline }.to change(GoodJob::Job, :count).by(2) - expect(reminder_jobs[0].job_data['arguments']) - .to contain_exactly(23) - - expect(reminder_jobs[1].job_data['arguments']) - .to contain_exactly(42) + arguments_from_both_jobs = + GoodJob::Job.where(job_class: "Mails::ReminderJob") + .flat_map {|i| i.serialized_params["arguments"]} + .sort + expect(arguments_from_both_jobs).to eq(ids) end it 'queries with the intended job execution time (which might have been missed due to high load)' do - subject + GoodJob.perform_inline - expect(User) - .to have_received(:having_reminder_mail_to_send) - .with(run_at) + expect(User).to have_received(:having_reminder_mail_to_send).with(scheduled_job.good_job_scheduled_at) end end it_behaves_like 'schedules reminder mails' context 'with a job that missed some runs' do - let(:run_at) { scheduled_job.run_at - 3.hours } + before do + GoodJob::Job + .where(id: scheduled_job.job_id) + .update_all(scheduled_at: scheduled_job.good_job_scheduled_at - 3.hours) + end it_behaves_like 'schedules reminder mails' end diff --git a/spec/workers/work_packages/apply_working_days_change_job_spec.rb b/spec/workers/work_packages/apply_working_days_change_job_spec.rb index cfbf896d5155..5726bf8b7046 100644 --- a/spec/workers/work_packages/apply_working_days_change_job_spec.rb +++ b/spec/workers/work_packages/apply_working_days_change_job_spec.rb @@ -1811,26 +1811,4 @@ end end end - - describe '.scheduled?' do - context 'with a job already scheduled in the DB' do - before do - ActiveJob::QueueAdapters::DelayedJobAdapter - .new - .enqueue(described_class.new(user_id: 5, previous_non_working_days: [1, 2])) - end - - it 'is true' do - expect(described_class) - .to be_scheduled - end - end - - context 'without a job already scheduled in the DB' do - it 'is false' do - expect(described_class) - .not_to be_scheduled - end - end - end end