2024-11-15 13:49:37 -05:00
|
|
|
class Sync < ApplicationRecord
|
2025-05-15 10:19:56 -04:00
|
|
|
include AASM
|
|
|
|
|
2025-04-18 09:46:49 -04:00
|
|
|
Error = Class.new(StandardError)
|
|
|
|
|
2024-11-15 13:49:37 -05:00
|
|
|
belongs_to :syncable, polymorphic: true
|
|
|
|
|
2025-04-11 12:13:46 -04:00
|
|
|
belongs_to :parent, class_name: "Sync", optional: true
|
|
|
|
has_many :children, class_name: "Sync", foreign_key: :parent_id, dependent: :destroy
|
|
|
|
|
2024-11-15 13:49:37 -05:00
|
|
|
scope :ordered, -> { order(created_at: :desc) }
|
2025-05-15 10:19:56 -04:00
|
|
|
scope :incomplete, -> { where(status: [ :pending, :syncing ]) }
|
|
|
|
|
|
|
|
validate :window_valid
|
|
|
|
|
|
|
|
# Sync state machine
|
|
|
|
aasm column: :status, timestamps: true do
|
|
|
|
state :pending, initial: true
|
|
|
|
state :syncing
|
|
|
|
state :completed
|
|
|
|
state :failed
|
2024-11-15 13:49:37 -05:00
|
|
|
|
2025-05-15 10:19:56 -04:00
|
|
|
after_all_transitions :log_status_change
|
|
|
|
|
|
|
|
event :start, after_commit: :report_warnings do
|
|
|
|
transitions from: :pending, to: :syncing
|
|
|
|
end
|
|
|
|
|
|
|
|
event :complete do
|
|
|
|
transitions from: :syncing, to: :completed
|
|
|
|
end
|
|
|
|
|
|
|
|
event :fail do
|
|
|
|
transitions from: :syncing, to: :failed
|
|
|
|
end
|
2025-04-11 12:13:46 -04:00
|
|
|
end
|
|
|
|
|
2024-11-15 13:49:37 -05:00
|
|
|
def perform
|
2025-03-05 15:38:31 -05:00
|
|
|
Rails.logger.tagged("Sync", id, syncable_type, syncable_id) do
|
|
|
|
start!
|
|
|
|
|
|
|
|
begin
|
2025-05-15 10:19:56 -04:00
|
|
|
syncable.perform_sync(self)
|
|
|
|
rescue => e
|
|
|
|
fail!
|
|
|
|
update(error: e.message)
|
|
|
|
report_error(e)
|
|
|
|
ensure
|
|
|
|
finalize_if_all_children_finalized
|
2025-04-11 12:13:46 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2025-05-15 10:19:56 -04:00
|
|
|
# Finalizes the current sync AND parent (if it exists)
|
|
|
|
def finalize_if_all_children_finalized
|
|
|
|
Sync.transaction do
|
|
|
|
lock!
|
|
|
|
|
|
|
|
# If this is the "parent" and there are still children running, don't finalize.
|
|
|
|
return unless all_children_finalized?
|
|
|
|
|
|
|
|
if syncing?
|
|
|
|
if has_failed_children?
|
|
|
|
fail!
|
|
|
|
else
|
|
|
|
complete!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# If we make it here, the sync is finalized. Run post-sync, regardless of failure/success.
|
|
|
|
perform_post_sync
|
|
|
|
end
|
|
|
|
|
|
|
|
# If this sync has a parent, try to finalize it so the child status propagates up the chain.
|
|
|
|
parent&.finalize_if_all_children_finalized
|
|
|
|
end
|
|
|
|
|
2024-11-15 13:49:37 -05:00
|
|
|
private
|
2025-05-15 10:19:56 -04:00
|
|
|
def log_status_change
|
|
|
|
Rails.logger.info("changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})")
|
2024-11-15 13:49:37 -05:00
|
|
|
end
|
|
|
|
|
2025-05-15 10:19:56 -04:00
|
|
|
def has_failed_children?
|
|
|
|
children.failed.any?
|
2024-11-15 13:49:37 -05:00
|
|
|
end
|
|
|
|
|
2025-05-15 10:19:56 -04:00
|
|
|
def all_children_finalized?
|
|
|
|
children.incomplete.empty?
|
|
|
|
end
|
2025-03-05 15:38:31 -05:00
|
|
|
|
2025-05-15 10:19:56 -04:00
|
|
|
def perform_post_sync
|
|
|
|
Rails.logger.info("Performing post-sync for #{syncable_type} (#{syncable.id})")
|
|
|
|
syncable.perform_post_sync
|
|
|
|
syncable.broadcast_sync_complete
|
|
|
|
rescue => e
|
|
|
|
Rails.logger.error("Error performing post-sync for #{syncable_type} (#{syncable.id}): #{e.message}")
|
|
|
|
report_error(e)
|
|
|
|
end
|
|
|
|
|
|
|
|
def report_error(error)
|
|
|
|
Sentry.capture_exception(error) do |scope|
|
|
|
|
scope.set_tags(sync_id: id)
|
2024-12-02 12:04:54 -05:00
|
|
|
end
|
2025-05-15 10:19:56 -04:00
|
|
|
end
|
2024-12-02 12:04:54 -05:00
|
|
|
|
2025-05-15 10:19:56 -04:00
|
|
|
def report_warnings
|
|
|
|
todays_sync_count = syncable.syncs.where(created_at: Date.current.all_day).count
|
|
|
|
|
|
|
|
if todays_sync_count > 10
|
|
|
|
Sentry.capture_exception(
|
|
|
|
Error.new("#{syncable_type} (#{syncable.id}) has exceeded 10 syncs today (count: #{todays_sync_count})"),
|
|
|
|
level: :warning
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def window_valid
|
|
|
|
if window_start_date && window_end_date && window_start_date > window_end_date
|
|
|
|
errors.add(:window_end_date, "must be greater than window_start_date")
|
|
|
|
end
|
2024-11-15 13:49:37 -05:00
|
|
|
end
|
|
|
|
end
|