Skip to content

Commit

Permalink
Extracts the debounce logic to a module, so it can be reused by other…
Browse files Browse the repository at this point in the history
… jobs
  • Loading branch information
mereghost committed Jun 28, 2024
1 parent bd26047 commit 6801fad
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 40 deletions.
49 changes: 49 additions & 0 deletions app/workers/debounceable_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 DebounceableJob
# This module is generalizes the debounce logic that was originally used on {Storages::ManageStorageIntegrationsJob}
# Basically it ensures that a thread only queues one job per interval.

# it depends on the class method `key` being implemented. The method will receive all the arguments
# used to invoke the job to construct the RequestStore key.
SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds

def debounce(*, **)
store_key = key(*, **)
timestamp = RequestStore.store[store_key]

return false if timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current

result = set(wait: 5.seconds).perform_later(*, **)
RequestStore.store[store_key] = Time.current
result
end
end
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
# 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 AutomaticallyManagedStorageSyncJob < ApplicationJob
include GoodJob::ActiveJobExtensions::Concurrency
queue_with_priority :above_normal
extend ::DebounceableJob

SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds
queue_with_priority :above_normal

good_job_control_concurrency_with(
total_limit: 2,
Expand All @@ -27,18 +52,7 @@ class AutomaticallyManagedStorageSyncJob < ApplicationJob
end
end

class << self
def debounce(storage)
key = "sync-#{storage.short_provider_type}-#{storage.id}"
timestamp = RequestStore.store[key]

return false if timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current

result = set(wait: 5.seconds).perform_later(storage)
RequestStore.store[key] = Time.current
result
end
end
def self.key(storage) = "sync-#{storage.short_provider_type}-#{storage.id}"

def perform(storage)
return unless storage.configured? && storage.automatically_managed?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
module Storages
class ManageStorageIntegrationsJob < ApplicationJob
include GoodJob::ActiveJobExtensions::Concurrency
extend ::DebounceableJob

good_job_control_concurrency_with(
total_limit: 2,
Expand All @@ -42,32 +43,12 @@ class ManageStorageIntegrationsJob < ApplicationJob
wait: 5,
attempts: 20

SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds.freeze
KEY = :manage_nextcloud_integration_job_debounce_happened_at
CRON_JOB_KEY = :"Storages::ManageStorageIntegrationsJob"

queue_with_priority :above_normal

class << self
def debounce
if debounce_happened_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_automatically_managed.exists?
GoodJob::Setting.cron_key_enable(CRON_JOB_KEY) unless GoodJob::Setting.cron_key_enabled?(CRON_JOB_KEY)
Expand All @@ -76,12 +57,7 @@ def disable_cron_job_if_needed
end
end

private

def debounce_happened_in_current_thread_recently?
timestamp = RequestStore.store[KEY]
timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current
end
def key = KEY
end

def perform
Expand Down

0 comments on commit 6801fad

Please sign in to comment.