Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimisation du traitement de l'obsolescence des connexions #1885

Merged
merged 17 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/jobs/communication/clean_website_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Communication::CleanWebsiteJob < ApplicationJob
queue_as :long_cleanup

def perform(website_id)
website = Communication::Website.find_by(id: website_id)
return unless website.present?
website.delete_obsolete_connections_for_self_and_direct_sources
website.destroy_obsolete_git_files
end
end
11 changes: 0 additions & 11 deletions app/jobs/communication/clean_websites_job.rb

This file was deleted.

23 changes: 16 additions & 7 deletions app/models/communication/website/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,27 @@ def self.websites_for(object)
for_object(object).distinct(:website).collect(&:website).uniq
end

def destroy_if_obsolete
destroy if obsolete?
def self.delete_useless_connections(connections, dependencies)
deletable_connection_ids = []
connections.find_each do |connection|
deletable_connection_ids << connection.id if connection.obsolete_in?(dependencies)
end
# On utilise delete_all pour supprimer les connexions obsolètes en une unique requête DELETE FROM
# Cependant, on peut le faire car les connexions n'ont pas de callback.
# Dans le cas où on en rajoute au destroy, il faut repasser sur un appel de destroy sur chaque
connections.where(id: deletable_connection_ids).delete_all
end
handle_asynchronously :destroy_if_obsolete, queue: :cleanup

def to_s
"#{id.split('-').first}"
end

protected

def obsolete?
!indirect_object.in?(direct_source.recursive_dependencies)
# On passe les dépendances pour ne pas les recharger et préserver la RAM
def obsolete_in?(dependencies)
!dependencies.detect do |dependency|
dependency.class.name == indirect_object_type &&
dependency.id == indirect_object_id
end
end

end
58 changes: 41 additions & 17 deletions app/models/communication/website/with_connected_objects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,30 @@ def clean_and_rebuild
end
handle_asynchronously :clean_and_rebuild, queue: :cleanup

# Le site fait le ménage de ses connexions directes uniquement
def delete_obsolete_connections
Communication::Website::Connection.delete_useless_connections(
# On ne liste pas toutes les connexions du website,
# mais juste les connexions pour lesquelles le site est la source.
connections.where(direct_source: self),
# On prend l'about et ses dépendances récursives.
# On ne prend pas toutes les dépendances parce qu'on s'intéresse
# uniquement à la connexion via about.
about_dependencies
)
end

# Le site fait son ménage de printemps
# Appelé
# - par un objet avec des connexions lorsqu'il est destroyed
# - par le website lui-même au changement du about
def destroy_obsolete_connections
up_to_date_dependencies = recursive_dependencies(follow_direct: true)
deletable_connection_ids = []
connections.find_each do |connection|
has_living_connection = up_to_date_dependencies.detect { |dependency|
dependency.class.name == connection.indirect_object_type &&
dependency.id == connection.indirect_object_id
}
deletable_connection_ids << connection.id unless has_living_connection
def delete_obsolete_connections_for_self_and_direct_sources
direct_source_ids_per_type_through_connections.each do |direct_source_type, direct_source_ids|
# On récupère une liste d'objets directs d'une même classe
direct_sources = direct_source_type.safe_constantize.where(id: direct_source_ids)
# On exécute en synchrone pour chaque objet
direct_sources.find_each(&:delete_obsolete_connections)
end
# On utilise delete_all pour supprimer les connexions obsolètes en une unique requête DELETE FROM
# Cependant, on peut le faire car les connexions n'ont pas de callback.
# Dans le cas où on en rajoute au destroy, il faut repasser sur un appel de destroy sur chaque
connections.where(id: deletable_connection_ids).delete_all
# On traite ensuite chaque connexion, pour vérifier qu'elle encore pertinente
connections.reload.find_each &:destroy_if_obsolete
end

def has_connected_object?(indirect_object)
Expand Down Expand Up @@ -119,7 +124,7 @@ def clean_and_rebuild_safely
public_send(association_name).find_each(&:connect_dependencies)
end
connect(about, self) if about.present?
destroy_obsolete_connections
delete_obsolete_connections_for_self_and_direct_sources
# In the same job
create_missing_special_pages
initialize_menus
Expand All @@ -131,7 +136,12 @@ def clean_and_rebuild_safely

def connect_about
self.connect(about, self) if about.present? && about.try(:is_indirect_object?)
destroy_obsolete_connections
delay(queue: :long_cleanup).delete_obsolete_connections
end

def about_dependencies
about.present? ? [about] + about.recursive_dependencies
: []
end

def connect_object(indirect_object, direct_source, direct_source_type: nil)
Expand Down Expand Up @@ -160,4 +170,18 @@ def should_connect?(indirect_object, direct_source)
# On ne connecte pas des objets qui ne sont pas issus de modèles ActiveRecord (comme les composants des blocs)
indirect_object.is_a?(ActiveRecord::Base)
end

# On passe par les connexions pour éviter d'analyser des objets directs qui n'ont pas d'objets indirects du tout
# Le website lui même est inclus dans le retour (s'il a un about qui déclenche des connexions)
def direct_source_ids_per_type_through_connections
# {
# 'Communication::Website::Post': ['ID1', 'ID2', ...],
# 'Communication::Website::Page': ['ID1', 'ID2', ...],
# 'Communication::Website': ['ID1'],
# ...
# }
connections.group(:direct_source_type)
.pluck(:direct_source_type, Arel.sql('array_agg(DISTINCT direct_source_id)'))
.to_h
end
end
13 changes: 12 additions & 1 deletion app/models/concerns/as_direct_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,15 @@ def connect_dependencies
website.connect(dependency, self)
end
end
end

# L'objet fait son ménage
# TODO Cette méthode devrait être appelée dès qu'on enregistre un objet indirect,
# sur chaque `direct_source` connectée (via les connexions).
def delete_obsolete_connections
Communication::Website::Connection.delete_useless_connections(
connections,
recursive_dependencies
)
end

end
4 changes: 3 additions & 1 deletion app/models/concerns/with_dependencies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ def clean_websites_if_necessary
def clean_websites(websites_ids)
# Les objets directs et les objets indirects (et les websites) répondent !
return unless respond_to?(:is_direct_object?)
Communication::CleanWebsitesJob.perform_later(websites_ids)
websites_ids.each do |website_id|
Communication::CleanWebsiteJob.perform_later(website_id)
end
end

def websites_to_clean
Expand Down
Loading
Loading