From c45982eb64a42941aef7b9b0918e137014521745 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 18 Sep 2025 15:32:25 +0100 Subject: [PATCH] wip --- Gemfile | 3 +- Gemfile.lock | 3 + .../api/testing/teams_controller.rb | 2 + app/controllers/consent_forms_controller.rb | 8 ++ app/controllers/imports/issues_controller.rb | 3 +- .../consent_forms_controller.rb | 2 + app/forms/import_duplicate_form.rb | 8 +- app/forms/school_move_form.rb | 6 ++ app/jobs/commit_patient_changesets_job.rb | 8 ++ app/jobs/consent_form_matching_job.rb | 7 ++ app/lib/team_cached_counts.rb | 80 ++++++++++++++----- config/environments/production.rb | 16 +++- 12 files changed, 121 insertions(+), 25 deletions(-) diff --git a/Gemfile b/Gemfile index 86a1be286f..55ab28c0f3 100644 --- a/Gemfile +++ b/Gemfile @@ -48,13 +48,14 @@ gem "jwt" gem "mechanize" gem "notifications-ruby-client" gem "okcomputer" -gem "omniauth_openid_connect" gem "omniauth-rails_csrf_protection" +gem "omniauth_openid_connect" gem "pagy" gem "phonelib" gem "pundit" gem "rails_semantic_logger" gem "rainbow" +gem "redis" gem "ruby-progressbar" gem "rubyzip" gem "sentry-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 5d7f9dd4e5..95694ac532 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -520,6 +520,8 @@ GEM erb psych (>= 4.0.0) redcarpet (3.6.1) + redis (5.4.1) + redis-client (>= 0.22.0) redis-client (0.25.3) connection_pool redis-prescription (2.6.0) @@ -825,6 +827,7 @@ DEPENDENCIES rails (~> 8.0.3) rails_semantic_logger rainbow + redis rladr rspec rspec-html-matchers diff --git a/app/controllers/api/testing/teams_controller.rb b/app/controllers/api/testing/teams_controller.rb index 3c95a937d5..6b0f4b1ca7 100644 --- a/app/controllers/api/testing/teams_controller.rb +++ b/app/controllers/api/testing/teams_controller.rb @@ -67,6 +67,8 @@ def destroy VaccinationRecord.where(performed_ods_code: team.organisation.ods_code) ) + TeamCachedCounts.new(team).reset_all! + unless keep_itself log_destroy(Session.where(team:)) diff --git a/app/controllers/consent_forms_controller.rb b/app/controllers/consent_forms_controller.rb index 953f7d9862..2c64e44068 100644 --- a/app/controllers/consent_forms_controller.rb +++ b/app/controllers/consent_forms_controller.rb @@ -35,6 +35,8 @@ def edit_match def update_match @consent_form.match_with_patient!(@patient, current_user:) + reset_count! + session = @patient .sessions @@ -108,6 +110,8 @@ def create_patient school_move.confirm! @consent_form.match_with_patient!(patient, current_user:) + + reset_count! end if patient.nhs_number.nil? @@ -143,4 +147,8 @@ def set_patient def archive_params params.expect(consent_form: :notes).merge(archived_at: Time.current) end + + def reset_count! + TeamCachedCounts.new(current_team).reset_unmatched_consent_responses! + end end diff --git a/app/controllers/imports/issues_controller.rb b/app/controllers/imports/issues_controller.rb index 684ba85de6..1b3760c2c8 100644 --- a/app/controllers/imports/issues_controller.rb +++ b/app/controllers/imports/issues_controller.rb @@ -70,7 +70,8 @@ def set_patient def set_form apply_changes = params.dig(:import_duplicate_form, :apply_changes) - @form = ImportDuplicateForm.new(object: @record, apply_changes:) + @form = + ImportDuplicateForm.new(current_team:, object: @record, apply_changes:) end def set_type diff --git a/app/controllers/parent_interface/consent_forms_controller.rb b/app/controllers/parent_interface/consent_forms_controller.rb index 46cedb990e..abc741a032 100644 --- a/app/controllers/parent_interface/consent_forms_controller.rb +++ b/app/controllers/parent_interface/consent_forms_controller.rb @@ -55,6 +55,8 @@ def record session.delete(:consent_form_id) + TeamCachedCounts.new(@team).reset_unmatched_consent_responses! + send_consent_form_confirmation(@consent_form) ConsentFormMatchingJob.perform_later(@consent_form) diff --git a/app/forms/import_duplicate_form.rb b/app/forms/import_duplicate_form.rb index 27390a9872..b73aba592d 100644 --- a/app/forms/import_duplicate_form.rb +++ b/app/forms/import_duplicate_form.rb @@ -3,7 +3,7 @@ class ImportDuplicateForm include ActiveModel::Model - attr_accessor :object, :apply_changes + attr_accessor :current_team, :object, :apply_changes validates :apply_changes, inclusion: { in: :apply_changes_options } @@ -21,6 +21,8 @@ def save end end + reset_count! + true rescue ActiveRecord::RecordInvalid errors.add(:base, "Failed to save changes") @@ -64,4 +66,8 @@ def discard_pending_changes! def keep_both_changes! object.apply_pending_changes_to_new_record! if can_keep_both? && can_apply? end + + def reset_count! + TeamCachedCounts.new(current_team).reset_import_issues! + end end diff --git a/app/forms/school_move_form.rb b/app/forms/school_move_form.rb index 8e299d92fd..cf7ec50dba 100644 --- a/app/forms/school_move_form.rb +++ b/app/forms/school_move_form.rb @@ -20,6 +20,12 @@ def save @school_move.ignore! end + TeamCachedCounts.new(team).reset_school_moves! + true end + + private + + def team = current_user.selected_team end diff --git a/app/jobs/commit_patient_changesets_job.rb b/app/jobs/commit_patient_changesets_job.rb index 811d96a9f6..9b40f57815 100644 --- a/app/jobs/commit_patient_changesets_job.rb +++ b/app/jobs/commit_patient_changesets_job.rb @@ -29,6 +29,8 @@ def perform(import) import.postprocess_rows! + reset_counts(import) + import.update_columns( processed_at: Time.zone.now, status: :processed, @@ -186,4 +188,10 @@ def has_auto_confirmable_school_move?(school_move, import) academic_year: import.academic_year ) || school_move.patient.archived?(team: import.team) end + + def reset_counts(import) + cached_counts = TeamCachedCounts.new(import.team) + cached_counts.reset_import_issues! + cached_counts.reset_school_moves! + end end diff --git a/app/jobs/consent_form_matching_job.rb b/app/jobs/consent_form_matching_job.rb index 50b40413e3..4c50dff3bd 100644 --- a/app/jobs/consent_form_matching_job.rb +++ b/app/jobs/consent_form_matching_job.rb @@ -65,6 +65,8 @@ def match_with_exact_nhs_number patient.update_from_pds!(pds_patient) send_parental_contact_warning_if_needed(patient, @consent_form) @consent_form.match_with_patient!(patient, current_user: nil) + reset_counts + true end def location_patients @@ -106,5 +108,10 @@ def match_patient(patient) send_parental_contact_warning_if_needed(patient, @consent_form) @consent_form.match_with_patient!(patient, current_user: nil) + reset_counts + end + + def reset_counts + TeamCachedCounts.new(@consent_form.team).reset_unmatched_consent_responses! end end diff --git a/app/lib/team_cached_counts.rb b/app/lib/team_cached_counts.rb index 5f5e5520a6..76effaf029 100644 --- a/app/lib/team_cached_counts.rb +++ b/app/lib/team_cached_counts.rb @@ -8,45 +8,81 @@ def initialize(team) def import_issues return nil if current_user.nil? - vaccination_records_with_issues = - VaccinationRecordPolicy::Scope - .new(current_user, VaccinationRecord) - .resolve - .with_pending_changes - .pluck(:patient_id) + Rails + .cache + .fetch(import_issues_key) do + vaccination_records_with_issues = + VaccinationRecordPolicy::Scope + .new(current_user, VaccinationRecord) + .resolve + .with_pending_changes + .pluck(:patient_id) - patients_with_issues = - PatientPolicy::Scope - .new(current_user, Patient) - .resolve - .with_pending_changes - .pluck(:id) + patients_with_issues = + PatientPolicy::Scope + .new(current_user, Patient) + .resolve + .with_pending_changes + .pluck(:id) - (vaccination_records_with_issues + patients_with_issues).uniq.length + (vaccination_records_with_issues + patients_with_issues).uniq.length + end + end + + def reset_import_issues! + Rails.cache.delete(import_issues_key) end def school_moves return nil if current_user.nil? - SchoolMovePolicy::Scope.new(current_user, SchoolMove).resolve.count + Rails + .cache + .fetch(school_moves_key) do + SchoolMovePolicy::Scope.new(current_user, SchoolMove).resolve.count + end + end + + def reset_school_moves! + Rails.cache.delete(school_moves_key) end def unmatched_consent_responses return nil if current_user.nil? - ConsentFormPolicy::Scope - .new(current_user, ConsentForm) - .resolve - .unmatched - .recorded - .not_archived - .count + Rails + .cache + .fetch(unmatched_consent_responses_key) do + ConsentFormPolicy::Scope + .new(current_user, ConsentForm) + .resolve + .unmatched + .recorded + .not_archived + .count + end + end + + def reset_unmatched_consent_responses! + Rails.cache.delete(unmatched_consent_responses_key) + end + + def reset_all! + reset_import_issues! + reset_school_moves! + reset_unmatched_consent_responses! end private attr_reader :team + def import_issues_key = cache_key("import-issues") + + def school_moves_key = cache_key("school-moves") + + def unmatched_consent_responses_key = cache_key("unmatched-consent-responses") + def current_user # We can't use the policy_scope helper here as we're not in a controller. # Instead, we can mock what a `User` looks like from the perspective of a @@ -56,4 +92,6 @@ def current_user OpenStruct.new(selected_team: team, selected_organisation: organisation) end end + + def cache_key(type) = "cached-counts/#{type}/#{team.id}" end diff --git a/config/environments/production.rb b/config/environments/production.rb index 7cdd797223..a822c3dddb 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -72,7 +72,21 @@ ) # Replace the default in-process memory cache store with a durable alternative. - # config.cache_store = :mem_cache_store + config.cache_store = + :redis_cache_store, + { + url: ENV["REDIS_CACHE_URL"], + error_handler: ->(method:, returning:, exception:) do + Sentry.capture_exception( + exception, + level: "warning", + tags: { + method:, + returning: + } + ) + end + } # Replace the default in-process and non-durable queuing backend for Active Job. # config.active_job.queue_adapter = :resque