Class: SpreeCmCommissioner::WaitingRoom::Admission::CallGuests

Inherits:
Object
  • Object
show all
Extended by:
ServiceModuleThrowable
Includes:
Spree::ServiceModule::Base, FirestoreConnection
Defined in:
app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb

Overview

Selects and admits the next cohort of waiting guests each tick. Runs as the second step of WaitingRoom::AdvanceQueue, immediately after Positions::Stamp, so in the common case every eligible doc already has a position by the time #eligible_guests_in queries. That's a throughput property, not a correctness guarantee though — Stamp's own per-tick budget (STAMP_LIMIT) can fall behind a burst of new arrivals, so #eligible_guests_in still keeps its own position != nil filter as the actual guard against calling a guest whose position is still nil; see that method for detail.

Admitting a guest is split into two phases: this class only claims up to MAX_CALLS_PER_RUN guests (a fast, synchronous Firestore batch write of call_dispatched_at) and hands the actual session-precreation + grant write off to CallGuestsChunkJob in slices of CALL_CHUNK_SIZE — see #claim_and_dispatch. The claim is what keeps that split correct:

- it stops next tick's #eligible_guests_in from re-selecting the same not-yet-written guests
while their chunk jobs are still draining (which would otherwise re-dispatch duplicate
chunk jobs every tick, growing unbounded);
- #fetch_available_slots subtracts still-claimed docs from capacity, so a slow-draining
backlog throttles new claims instead of overcommitting;
- #reclaim_stale_claims self-heals a guest orphaned by a permanently-failed chunk job.

Capacity fields (lobby:) and run telemetry (called_count, logs:) are returned in the result rather than written directly — WaitingRoom::AdvanceQueue owns the single combined lobby write and single combined logs write (merged with Positions::Stamp's own fields). records_path is likewise resolved once by AdvanceQueue and passed in, rather than this class reading the lobby doc itself.

Constant Summary collapse

FIRESTORE_BATCH_SIZE =

Firestore bounds a batch update by payload size (10 MiB); 500 ops/commit leaves us far under that.

(ENV['WAITING_ROOM_FIRESTORE_BATCH_SIZE'] || 500).to_i
MAX_CALLS_PER_RUN =

Release at most this many guests per run, even when far more slots are free. A capacity spike (cold start / lull → active_sessions low → available_slots ≈ max_sessions, e.g. 900) would otherwise release every queued guest at once, dumping 900 simultaneous session-token requests on the server. Capping paces entry at ~MAX_CALLS_PER_RUN per run (10s, per cm-market-server/config/schedule.yml); the rest stay queued and drain over the following runs.

Keeping this <= Positions::Stamp::STAMP_LIMIT is a throughput tuning goal, not a correctness requirement (#eligible_guests_in's own position != nil filter is what enforces correctness): raising this past STAMP_LIMIT just means some ticks call fewer than MAX_CALLS_PER_RUN guests, because Stamp hasn't yet caught up on positioning the rest of that tick's oldest-waiting cohort.

(ENV['WAITING_ROOM_MAX_CALLS_PER_RUN'] || 200).to_i
CALL_CHUNK_SIZE =

How many guests each CallGuestsChunkJob writes. MAX_CALLS_PER_RUN's per-tick claim gets split into chunks this size so one tick's admission work is spread across several small background jobs instead of one inline loop over up to MAX_CALLS_PER_RUN guests.

(ENV['WAITING_ROOM_CALL_CHUNK_SIZE'] || 50).to_i
CALL_CLAIM_TTL_SECONDS =

How long a claim (call_dispatched_at) may stand without allow_to_enter_room_at landing before #reclaim_stale_claims frees it back up — generous (~12 ticks), bounding worst-case recovery time for a permanently-failed chunk job without needing to be tight.

(ENV['WAITING_ROOM_CALL_CLAIM_TTL_SECONDS'] || 120).to_i

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ServiceModuleThrowable

call!

Methods included from FirestoreConnection

#firestore, #firestore_available?, #service_account

Instance Attribute Details

#records_pathObject (readonly)

Returns the value of attribute records_path.



60
61
62
# File 'app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb', line 60

def records_path
  @records_path
end

Instance Method Details

#call(records_path:, firestore: nil) ⇒ Object

records_path is resolved once by WaitingRoom::AdvanceQueue (one lobby-doc read shared across the tick) and handed in here, rather than this class reading the lobby doc itself — one less Firestore round trip, and this class no longer needs any lobby-doc mocking to test.



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
# File 'app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb', line 65

def call(records_path:, firestore: nil)
  started_at = Time.zone.now

  @firestore = firestore if firestore.present?
  @records_path = records_path

  reclaim_stale_claims

  available_slots = fetch_available_slots
  call_limit = [available_slots, MAX_CALLS_PER_RUN].min

  # Always run through to lobby_fields — even with no slots — so the lobby keeps a fresh
  # heartbeat during peak/full periods instead of going stale and looking like a dead cron.
  long_waiting_guests = call_limit.positive? ? fetch_long_waiting_guests(call_limit) : []
  claim_and_dispatch(long_waiting_guests) if long_waiting_guests.any?

  full = long_waiting_guests.size >= available_slots
  remaining_slots = available_slots - long_waiting_guests.size
  has_queue = waiting_exists?([previous_records_path, records_path])

  success(
    called_count: long_waiting_guests.size,
    lobby: lobby_fields(
      full: full,
      available_slots: remaining_slots,
      has_queue: has_queue,
      active_sessions: @active_sessions,
      max_sessions: @max_sessions
    ),
    logs: {
      called_count: long_waiting_guests.size,
      started_at: started_at,
      finished_at: Time.zone.now
    }
  )
rescue StandardError => e
  failure(nil, e.message)
end