Class: SpreeCmCommissioner::WaitingRoomSessionCreator
- Inherits:
-
BaseInteractor
- Object
- BaseInteractor
- SpreeCmCommissioner::WaitingRoomSessionCreator
- Defined in:
- app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb
Constant Summary collapse
- SESSION_CREATION_LOCK_KEY =
'waiting_room_session_creation'.freeze
- WAITING_ROOM_SESSION_MAX_DURATION_IN_SECOND =
Soft ceiling on how long a session may keep renewing before it competes for capacity again (see #max_expired_at_ceiling and #renew_active_session). 600s default; HOLD_DURATION (8 min) + PAYMENT_LOCK_MIN_DURATION extension can push a genuinely active checkout to ~13 min, so this is a soft cap — past it, the session self-expires and the next call falls back to the full?-gated path, rather than being evicted outright.
(ENV['WAITING_ROOM_SESSION_MAX_DURATION_IN_SECOND'] || 600).to_i
- WAITING_ROOM_SESSION_EXPIRE_DURATION_IN_SECOND =
How long a session's JWT/expired_at is valid before it needs renewal. 3min default.
(ENV['WAITING_ROOM_SESSION_EXPIRE_DURATION_IN_SECOND'] || (60 * 3)).to_i
- PERMITTED_DEVICE_ATTRIBUTES =
Device attributes the client may set on the session. device_id and device_fingerprint are real columns; the rest are virtual columns backed by public/private metadata (see WaitingRoomSession). Anything else the client sends is dropped so we never persist arbitrary payloads.
%w[ device_id device_fingerprint os_name os_version device_model device_manufacturer device_type device_environment app_version app_build timezone network_type ].freeze
Instance Method Summary collapse
- #advisory_lock(lock_key) ⇒ Object
- #apply_session_attributes(session) ⇒ Object
- #assign_token_and_create_session_to_db ⇒ Object
-
#call ⇒ Object
Three ways in here:.
- #call_other_waiting_guests ⇒ Object
-
#create_new_session ⇒ Object
No active row and no ceiling breach (see #call): a true new admission, a bypass-path entry, or a row that's lapsed but still under its own max_expired_at — goes through the normal advisory-lock + full? gated path.
-
#expired_at(max_expired_at = nil) ⇒ Object
Capped at max_expired_at so a late renewal can't push expired_at (and the JWT's exp, see generate_jwt_token) past the ceiling the session is meant to respect.
- #firestore ⇒ Object
- #full? ⇒ Boolean
- #generate_jwt_token(max_expired_at = nil) ⇒ Object
- #max_expired_at_ceiling ⇒ Object
-
#renew_active_session(session) ⇒ Object
An already-reserved slot is already counted in WaitingRoomSession.active.count, so re-checking capacity on its own renewal can only ever reject the holder's own session — this is the eviction-bug fix.
-
#sanitized_device_attributes ⇒ Object
Whitelisted, flat device attributes.
- #service_account ⇒ Object
Instance Method Details
#advisory_lock(lock_key) ⇒ Object
101 102 103 104 105 106 107 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 101 def advisory_lock(lock_key) lock_id = Zlib.crc32(lock_key) SpreeCmCommissioner::WaitingRoomSession.transaction do SpreeCmCommissioner::WaitingRoomSession.connection.execute("SELECT pg_advisory_xact_lock(#{lock_id})") yield end end |
#apply_session_attributes(session) ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 129 def apply_session_attributes(session) session.assign_attributes( { jwt_token: context.jwt_token, expired_at: expired_at, remote_ip: remote_ip, page_path: page_path, tenant_id: tenant_id }.merge(sanitized_device_attributes) ) end |
#assign_token_and_create_session_to_db ⇒ Object
114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 114 def assign_token_and_create_session_to_db session = SpreeCmCommissioner::WaitingRoomSession.where(guest_identifier: waiting_guest_firebase_doc_id).first_or_initialize context.room_session = session # A brand-new row gets a fresh ceiling. A row that already had one (lapsed but not yet past # its ceiling — see #call) keeps that same ceiling: reviving it here must not silently grant # it a new one just because it lapsed. Computed before generate_jwt_token so expired_at is # always capped against the ceiling that's actually being persisted, not a different one. session.max_expired_at ||= max_expired_at_ceiling generate_jwt_token(session.max_expired_at) apply_session_attributes(session) session.save! end |
#call ⇒ Object
Three ways in here:
Condition #1 — queued: WaitingGuestsCaller job precreated the row when it called this guest, so they just find and renew that existing row (if necessary).
Condition #2 — the row exists but its max_expired_at ceiling has already passed. Reject outright rather than silently reviving it: the ceiling is a hard cap the server controls, not something a renewal retry should be able to extend (see WaitingRoomSession#past_ceiling?).
Condition #3 — bypassed the queue: lobby doc showed full: false / has_queue: false, so the app called createSession directly. The caller never touched this guest, no row exists yet, so they fall through to a normal create_new_session. A row that's lapsed (expired_at passed) but still under its own ceiling also lands here — the revival path in #create_new_session.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 44 def call return context.fail!(message: 'must_provide_waiting_guest_firebase_doc_id') if waiting_guest_firebase_doc_id.blank? return context.fail!(message: 'must_provide_remote_ip') if remote_ip.blank? existing = SpreeCmCommissioner::WaitingRoomSession.find_by(guest_identifier: waiting_guest_firebase_doc_id) if existing&.active? renew_active_session(existing) elsif existing&.past_ceiling? context.fail!(message: 'waiting_room_session_expired') else create_new_session end end |
#call_other_waiting_guests ⇒ Object
152 153 154 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 152 def call_other_waiting_guests SpreeCmCommissioner::WaitingGuestsCallerJob.perform_later end |
#create_new_session ⇒ Object
No active row and no ceiling breach (see #call): a true new admission, a bypass-path entry, or a row that's lapsed but still under its own max_expired_at — goes through the normal advisory-lock + full? gated path.
78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 78 def create_new_session # Advisory lock ensures the capacity check and session creation are atomic across all app instances. advisory_lock(SESSION_CREATION_LOCK_KEY) do return context.fail!(message: 'sessions_reach_it_maximum') if full? assign_token_and_create_session_to_db end # commented because of following bug: https://github.com/channainfo/commissioner/issues/2185 # this job is already run every 1mn, disabling it still work. # call_other_waiting_guests end |
#expired_at(max_expired_at = nil) ⇒ Object
Capped at max_expired_at so a late renewal can't push expired_at (and the JWT's exp, see generate_jwt_token) past the ceiling the session is meant to respect.
158 159 160 161 162 163 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 158 def expired_at(max_expired_at = nil) context.expired_at ||= [ WAITING_ROOM_SESSION_EXPIRE_DURATION_IN_SECOND.seconds.from_now, max_expired_at ].compact.min end |
#firestore ⇒ Object
169 170 171 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 169 def firestore @firestore ||= Google::Cloud::Firestore.new(project_id: service_account[:project_id], credentials: service_account) end |
#full? ⇒ Boolean
91 92 93 94 95 96 97 98 99 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 91 def full? max = Rails.cache.fetch('waiting_room/max_sessions_count', expires_in: 1.hour) do fetcher = SpreeCmCommissioner::WaitingRoomSystemMetadataFetcher.new(firestore: firestore) fetcher.load_document_data fetcher.max_sessions_count_with_min end SpreeCmCommissioner::WaitingRoomSession.active.count >= max end |
#generate_jwt_token(max_expired_at = nil) ⇒ Object
109 110 111 112 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 109 def generate_jwt_token(max_expired_at = nil) payload = { exp: expired_at(max_expired_at).to_i } context.jwt_token = JWT.encode(payload, ENV.fetch('WAITING_ROOM_SESSION_SIGNATURE'), 'HS256') end |
#max_expired_at_ceiling ⇒ Object
165 166 167 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 165 def max_expired_at_ceiling context.max_expired_at_ceiling ||= WAITING_ROOM_SESSION_MAX_DURATION_IN_SECOND.seconds.from_now end |
#renew_active_session(session) ⇒ Object
An already-reserved slot is already counted in WaitingRoomSession.active.count, so re-checking capacity on its own renewal can only ever reject the holder's own session — this is the eviction-bug fix. No lock, no full? guard.
expired_at is always capped at session.max_expired_at (see #expired_at), so a session past its soft cap self-expires here instead of being renewed: this write persists an expired_at in the past, which drops it out of WaitingRoomSession.active — the guest's next call then finds no active row and falls through to create_new_session to compete for capacity again.
67 68 69 70 71 72 73 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 67 def renew_active_session(session) context.room_session = session generate_jwt_token(session.max_expired_at) apply_session_attributes(session) session.save! end |
#sanitized_device_attributes ⇒ Object
Whitelisted, flat device attributes. Real columns (device_id, device_fingerprint) and virtual columns (StoreMetadata) are assigned the same way; the model routes each to the right store.
144 145 146 147 148 149 150 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 144 def sanitized_device_attributes attributes = device_attributes attributes = attributes.to_unsafe_h if attributes.respond_to?(:to_unsafe_h) return {} unless attributes.is_a?(Hash) attributes.symbolize_keys.slice(*PERMITTED_DEVICE_ATTRIBUTES.map(&:to_sym)) end |
#service_account ⇒ Object
173 174 175 |
# File 'app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb', line 173 def service_account Rails.application.credentials.cloud_firestore_service_account end |