fix(goals): clear state-dependent caches on AASM transition + harden sweep job

- Goal: `display_status` and `projection_summary` memoize a value that
  depends on the AASM state column. Without resetting them after a
  transition the same instance keeps returning the pre-transition value.
  Hook `after_all_transitions :reset_state_dependent_caches!` undoes the
  memos so post-`archive!` / post-`pause!` reads see the new state.
- SweepExpiredGoalPledgesJob: the inner rescue covered per-pledge failures
  but not cursor-phase failures (DB blip, OOM mid-batch). Add an outer
  rescue that reports + re-raises so Sentry sees the failure and Sidekiq
  retries the job.
This commit is contained in:
Guillem Arias
2026-05-18 21:00:18 +02:00
parent fb36ac319a
commit 67726f88a6
2 changed files with 20 additions and 0 deletions

View File

@@ -3,6 +3,10 @@ class SweepExpiredGoalPledgesJob < ApplicationJob
# Per-record rescue so one bad pledge (lock contention, missing FK,
# stale row) doesn't abort the sweep and leave the rest open forever.
# The outer rescue catches query-phase failures (DB blip, OOM mid-cursor)
# so a single bad batch surfaces to Sentry rather than disappearing into
# Sidekiq's generic retry log. Re-raise after reporting so the retry
# behaviour still kicks in.
def perform
GoalPledge.open_and_expired_now.find_each do |pledge|
pledge.expire!
@@ -10,5 +14,9 @@ class SweepExpiredGoalPledgesJob < ApplicationJob
Rails.logger.error("SweepExpiredGoalPledgesJob: pledge ##{pledge.id} expire failed: #{e.class}: #{e.message}")
Sentry.capture_exception(e) if defined?(Sentry)
end
rescue StandardError => e
Rails.logger.error("SweepExpiredGoalPledgesJob: cursor failed: #{e.class}: #{e.message}")
Sentry.capture_exception(e) if defined?(Sentry)
raise
end
end

View File

@@ -36,6 +36,8 @@ class Goal < ApplicationRecord
end
aasm column: :state do
after_all_transitions :reset_state_dependent_caches!
state :active, initial: true
state :paused
state :completed
@@ -398,6 +400,16 @@ class Goal < ApplicationRecord
end
private
# Cleared after every AASM transition. The state column drives the
# display_status / projection_summary memos; without this the same
# instance keeps returning the pre-transition value if a controller
# calls archive! / pause! and then renders without reload.
def reset_state_dependent_caches!
%i[@display_status @projection_summary].each do |ivar|
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
end
end
# K/M shorthand for narrow chart annotations (axis ticks, projection
# short-form, pending-pledge badge). Locale-aware currency symbol via
# Money so the chart matches the rest of the app for EUR/GBP families.