Module: ModelExt::Reservations::Callbacks
- Extended by:
- ActiveSupport::Concern
- Includes:
- ElasticAPM::SpanHelpers
- Included in:
- Reservation
- Defined in:
- lib/model_ext/reservations/callbacks.rb
Overview
callbacks stuff
Instance Method Summary collapse
-
#push_booking_notification ⇒ Object
NOTE: Cache update methods (update_inventory_cache_inline, detect_and_refresh_stale_inventory_cache) have been removed.
- #push_inventory_notification ⇒ Object
-
#trigger_creation_sync(sync_type = 'full') ⇒ void
Manual sync trigger for reservation creation (immediate for admin/staff, delayed for users).
-
#trigger_destroy_sync ⇒ void
Manual sync trigger for destruction (async).
-
#trigger_immediate_sync(sync_type = 'full') ⇒ void
Manual sync trigger for immediate updates.
-
#trigger_priority_sync(sync_type = 'full') ⇒ void
Manual sync trigger for priority updates (non-blocking but fast).
-
#trigger_summary_sync(sync_type = 'full') ⇒ void
Manual sync trigger for general updates (async).
- #update_inv_relations ⇒ Object
Instance Method Details
#push_booking_notification ⇒ Object
NOTE: Cache update methods (update_inventory_cache_inline, detect_and_refresh_stale_inventory_cache) have been removed. Cache updates are now handled ASYNCHRONOUSLY via Kafka by the UpdateMirageCache concern which fires events in after_commit.
Flow: Reservation save → after_commit → UpdateMirageCache.clear_mirage_cache
→ Kafka EVENTS::INVENTORY::CACHE::TOPIC → Mirage::CacheConsumer
→ Inventory::UpdateCacheWorker → refresh ALL packages' cache
Benefits of async-only approach:
-
Faster DB transactions (no blocking cache updates)
-
Single source of truth for cache invalidation (Kafka)
-
No duplicate cache updates (sync + async were redundant)
-
DB-level lock (inv_lvl1_available?) prevents overbooking regardless of cache staleness
238 239 240 241 242 243 244 245 |
# File 'lib/model_ext/reservations/callbacks.rb', line 238 def push_booking_notification is_pending_confirmation = ack == false && confirmed_by.nil? && active == true not_temporary = is_temporary == false && for_locking_system == false return unless is_pending_confirmation && not_temporary NotificationWorkers::Partner::BookingWorker.perform_async(id) true end |
#push_inventory_notification ⇒ Object
247 248 249 250 |
# File 'lib/model_ext/reservations/callbacks.rb', line 247 def push_inventory_notification NotificationWorkers::Partner::InventoryWorker.perform_async(id) true end |
#trigger_creation_sync(sync_type = 'full') ⇒ void
This method returns an undefined value.
Manual sync trigger for reservation creation (immediate for admin/staff, delayed for users)
Call this method explicitly after reservation creation to ensure proper timing and avoid missed relations during the creation process.
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/model_ext/reservations/callbacks.rb', line 285 def trigger_creation_sync(sync_type = 'full') if created_by_admin_staff_or_owner? # Admin/Staff/Owner: Immediate sync for real-time partner portal updates begin Partner::ReservationSummarySyncWorker.new.perform(id, sync_type) rescue StandardError => e APMErrorHandler.report('Immediate creation sync failed', { reservation_id: id, sync_type: sync_type, created_by: created_by, error_message: e., exception: e, }) # Fallback to async Partner::ReservationSummarySyncWorker.perform_async(id, sync_type) end else # User bookings: Use delayed sync with protection against for_locking_system unless for_locking_system? Partner::ReservationSummarySyncWorker.perform_in(1.second, id, sync_type) end end rescue StandardError => e APMErrorHandler.report('Failed to trigger creation sync', { reservation_id: id, sync_type: sync_type, created_by: created_by, error_message: e., exception: e, }) end |
#trigger_destroy_sync ⇒ void
This method returns an undefined value.
Manual sync trigger for destruction (async)
Call this method when a reservation is being destroyed.
370 371 372 373 374 375 376 377 378 |
# File 'lib/model_ext/reservations/callbacks.rb', line 370 def trigger_destroy_sync Partner::ReservationSummarySyncWorker.perform_async(id, 'destroy') rescue StandardError => e APMErrorHandler.report('Failed to trigger destroy sync', { reservation_id: id, error_message: e., exception: e, }) end |
#trigger_immediate_sync(sync_type = 'full') ⇒ void
This method returns an undefined value.
Manual sync trigger for immediate updates
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/model_ext/reservations/callbacks.rb', line 341 def trigger_immediate_sync(sync_type = 'full') begin Partner::ReservationSummarySyncWorker.new.perform(id, sync_type) rescue StandardError => e APMErrorHandler.report('Immediate sync failed', { reservation_id: id, sync_type: sync_type, created_by: created_by, error_message: e., exception: e, }) # Fallback to async Partner::ReservationSummarySyncWorker.perform_async(id, sync_type) end rescue StandardError => e APMErrorHandler.report('Failed to trigger immediate sync', { reservation_id: id, sync_type: sync_type, created_by: created_by, error_message: e., exception: e, }) end |
#trigger_priority_sync(sync_type = 'full') ⇒ void
This method returns an undefined value.
Manual sync trigger for priority updates (non-blocking but fast)
Use this for critical updates that need fast processing but should not block the main application flow. Executes with minimal delay (0.1 seconds).
324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/model_ext/reservations/callbacks.rb', line 324 def trigger_priority_sync(sync_type = 'full') # Non-blocking but high priority sync with minimal delay Partner::ReservationSummarySyncWorker.perform_in(1.second, id, sync_type) rescue StandardError => e APMErrorHandler.report('Failed to trigger priority sync', { reservation_id: id, sync_type: sync_type, created_by: created_by, error_message: e., exception: e, }) end |
#trigger_summary_sync(sync_type = 'full') ⇒ void
This method returns an undefined value.
Manual sync trigger for general updates (async)
Use this for non-critical updates like package/add-on changes, general status updates, or when immediate sync is not required.
267 268 269 270 271 272 273 274 275 276 |
# File 'lib/model_ext/reservations/callbacks.rb', line 267 def trigger_summary_sync(sync_type = 'full') Partner::ReservationSummarySyncWorker.perform_async(id, sync_type) rescue StandardError => e APMErrorHandler.report('Failed to trigger summary sync', { reservation_id: id, sync_type: sync_type, error_message: e., exception: e, }) end |
#update_inv_relations ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/model_ext/reservations/callbacks.rb', line 61 def update_inv_relations # Guard against invalid states # Note: This callback runs in after_commit (AFTER transaction commits) to avoid FK constraint violations. # MySQL InnoDB only sees committed data for FK checks, so we must wait for commit. unless persisted? && !destroyed? APMErrorHandler.report('update_inv_relations called on invalid reservation state', context: { reservation_id: id, persisted: persisted?, destroyed: destroyed? }) return end # CRITICAL: Verify the reservation still exists in the database. # This guards against race conditions where the reservation was deleted/rolled back # after commit but before this callback executes. unless self.class.exists?(id) APMErrorHandler.report('update_inv_relations: reservation does not exist in DB', context: { reservation_id: id }) return end changed_attribute_keys = transaction_changed_attributes.keys # Early return if no relevant attributes changed relevant_changed_attrs = %w[active ack adult date start_time end_time for_locking_system is_temporary restaurant_id service_type].select do |key| changed_attribute_keys.include?(key) end if relevant_changed_attrs.blank? return end affect_old_data = %w[service_type date start_time end_time].select do |key| changed_attribute_keys.include?(key) end.present? inv_reservation_class = nil inv_class = nil if affect_old_data old_service_type = service_type if changed_attribute_keys.include?('service_type') old_service_type = transaction_changed_attributes['service_type'] inv_reservation_class = inventory_reservation_klass(old_service_type) inv_class = inventory_klass(old_service_type) else inv_reservation_class = inventory_reservation_klass # Use inventory_klass for consistency, but fall back to restaurant.selected_inventory_model # since the current service_type might determine a different inventory class inv_class = inventory_klass || restaurant.selected_inventory_model end # Query inventory reservations for the current reservation inventory_reservations = inv_reservation_class.where(restaurant_id: restaurant_id, reservation_id: id) # collect inventory IDs from the reservations inventory_ids = inventory_reservations.distinct.pluck(inv_reservation_class.inventory_id_column) # Delete all inventory reservations for this reservation as they will be recreated with updated data inventory_reservations.delete_all # Sort inventory IDs to ensure consistent lock ordering sorted_inventory_ids = inventory_ids.sort # No transaction needed here since this is an after_commit callback inv_class.where(id: sorted_inventory_ids).order(:id).each do |inv| inv.with_lock do # recalculate total booked seats for the inventory # it is needed to restore the seat left quota after this reservation has been updated # or created inv.calc_total_booked_seat inv.save! end end else inv_reservation_class = inventory_reservation_klass # Use inventory_klass for consistency, but fall back to restaurant.selected_inventory_model # since the current service_type might determine a different inventory class inv_class = inventory_klass || restaurant.selected_inventory_model end invs = inv_class.where(date: date, restaurant_id: restaurant_id). where('start_time >= ? AND start_time <= ? AND end_time >= ? AND end_time <= ?', start_time_format, end_time_format, start_time_format, end_time_format) inventory_reservations = [] inventory_relation = if restaurant.use_third_party_inventory? 'inventory_id' else "#{inv_class.to_s.underscore}_id" end booked_seat = if active? if ack? || (is_temporary? || for_locking_system?) adult else 0 end else 0 end ir_attributes_base = { reservation_id: id, booked_seat: booked_seat, restaurant_id: restaurant.id, } inv_class_as_string = inv_class.to_s invs.each do |inv| ir_attributes = ir_attributes_base.dup if service_type == 'dine_in' ir_attributes[inventory_relation] = inv.id[0] ir_attributes[:inventory_type] = inv_class_as_string else ir_attributes[inventory_relation] = inv.id end inventory_reservations.push ir_attributes end if inventory_reservations.present? Retriable.retriable( on: [ActiveRecord::Deadlocked, ActiveRecord::InvalidForeignKey], tries: 3, base_interval: 0.1, multiplier: 2, rand_factor: 0.5, on_retry: APMErrorHandler.report_retriable_event('reservation.inventory.update'), ) do inv_reservation_class.import!(inventory_reservations, on_duplicate_key_update: [:booked_seat], raise_error: true, validate: false, batch_size: 1000) end end # Sort inventories by ID to ensure consistent lock ordering across transactions sorted_invs = invs.sort_by(&:id) Retriable.retriable( on: ActiveRecord::Deadlocked, tries: 3, base_interval: 0.1, multiplier: 2, rand_factor: 0.5, on_retry: APMErrorHandler.report_retriable_event('reservation.inventory.update'), ) do sorted_invs.each do |inv| inv.with_lock do # recalculate total booked seats for the inventory # it is needed to consume the seat left quota after this reservation has been updated # or created inv.calc_total_booked_seat inv.save! end end end # NOTE: Cache update is handled asynchronously via Kafka by UpdateMirageCache concern. # The concern fires a Kafka event in after_commit which is consumed by # Mirage::CacheConsumer → Inventory::UpdateCacheWorker. # This provides eventual consistency while keeping the DB transaction fast. end |