Class: Phone::QueueRoster
- Inherits:
-
Object
- Object
- Phone::QueueRoster
- Defined in:
- app/services/phone/queue_roster.rb
Overview
Service object: queue roster.
The cached per-queue roster the switchboard renders: each queue's live ring
order as the PBX sees it, merged with the CRM employee behind each member.
The raw PBX snapshot is fresh for CACHE_TTL and also retained as a
last-known fallback (the PBX round trip is one getCurrentStatus per queue).
The employee merge runs on every read so a roster change in the CRM shows up
without waiting for the cache. An expired snapshot is served as stale while
the worker refreshes it; a cold cache renders empty/loading state while the
same worker prewarms it. A browser request never calls the PBX.
PullPhoneQueueStatusWorker is the only path that pays the per-queue round
trip. Keep CACHE_TTL above the worker's interval, and keep QueueRoster.cache_snapshot
merging rather than replacing.
Constant Summary collapse
- CACHE_KEY =
Rails.cache key for the raw PBX snapshot.
'phone/queue_roster/v1'- LAST_KNOWN_CACHE_KEY =
Long-lived fallback key for the last successfully refreshed snapshot.
"#{CACHE_KEY}/last_known"- LAST_KNOWN_CACHE_TTL =
The fallback tier was written without expires_in on the assumption that
meant "never expires" — but Rails.cache carries a global 1-hour default
(config/initializers/150_redis_cache.rb), so the roster went cold an hour
after the worker's last 18:00 run: guaranteed "phone system could not be
reached" every morning before 07:00 and all weekend. Write it with an
explicit long TTL instead; a stale roster (flagged as such) beats a blank
page, and the worker's next run replaces it anyway. 2.weeks
- REFRESH_REQUESTED_CACHE_KEY =
Short-lived gate that coalesces request-triggered background refreshes.
"#{CACHE_KEY}/refresh_requested"- REFRESH_REQUEST_TTL =
Minimum interval between request-triggered refresh attempts.
5.minutes
- CACHE_WRITE_LOCK_KEY =
PostgreSQL advisory-lock key for cache read-modify-write operations.
'phone_queue_roster_cache'- CACHE_WRITE_LOCK_TIMEOUT =
Maximum seconds a cache writer waits for another writer to finish.
10- CACHE_TTL =
How long the raw PBX snapshot is considered fresh before a read serves the
last-known copy and asynchronously requests a refresh. Must outlive
PullPhoneQueueStatusWorker's hourly interval,
or the worker's write expires between runs and the roster page pays the
whole per-queue round trip inside a request — 28-34s, AppSignal #6601.
The worker rewrites every hour, so real staleness stays under that;
this window only decides who does the pulling. 65.minutes
Class Method Summary collapse
-
.apply_member_order(queue_account_id, ordered_member_ids) ⇒ void
Writes a just-applied ring order back into the cached snapshot, so a successful reorder re-renders from cache instead of busting it and paying one getCurrentStatus per queue on the very next read.
- .bust_cache ⇒ void
-
.cached_member_order(queue_account_id) ⇒ Array<String>
The last cached ring order for one queue, WITHOUT a PBX round trip.
-
.refresh(pbx_snapshot = nil) ⇒ Hash
Force-writes the cache.
-
.snapshot ⇒ Hash{Integer => Hash}
Queue account id => { 'name', 'strategy', 'queue' => PhoneQueue, 'members' => [{ 'account_id', 'fullname', 'order', 'employee_id', 'employee_name' }], 'stale' => Boolean }; {} when a cold cache is waiting for its asynchronous prewarm.
Class Method Details
.apply_member_order(queue_account_id, ordered_member_ids) ⇒ void
This method returns an undefined value.
Writes a just-applied ring order back into the cached snapshot, so a
successful reorder re-renders from cache instead of busting it and
paying one getCurrentStatus per queue on the very next read. No-op on a
cold cache or when the cached membership no longer matches (the next
background refresh repairs it in that case). A stale snapshot is updated
only in the last-known tier and requests reconciliation; it is never
promoted into the fresh tier.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'app/services/phone/queue_roster.rb', line 122 def apply_member_order(queue_account_id, ordered_member_ids) with_cache_write_lock do fresh_snapshot = Rails.cache.read(CACHE_KEY) snapshot = (fresh_snapshot || Rails.cache.read(LAST_KNOWN_CACHE_KEY))&.deep_dup members = snapshot&.dig(queue_account_id, 'members') next if members.blank? by_id = members.index_by { |member| member['account_id'].to_s } reordered = ordered_member_ids.map { |id| by_id[id.to_s] } next if reordered.any?(&:nil?) || reordered.size != members.size reordered.each_with_index { |member, index| member['order'] = index + 1 } snapshot[queue_account_id]['members'] = reordered if fresh_snapshot write_snapshot(snapshot) else Rails.cache.write(LAST_KNOWN_CACHE_KEY, snapshot, expires_in: LAST_KNOWN_CACHE_TTL) enqueue_refresh end end rescue WithAdvisoryLock::FailedToAcquireLock => e Rails.logger.error "[Phone::QueueRoster] apply_member_order lock failed for queue #{queue_account_id}: #{e.}" end |
.bust_cache ⇒ void
This method returns an undefined value.
94 95 96 97 98 |
# File 'app/services/phone/queue_roster.rb', line 94 def bust_cache Rails.cache.delete(CACHE_KEY) Rails.cache.delete(LAST_KNOWN_CACHE_KEY) Rails.cache.delete(REFRESH_REQUESTED_CACHE_KEY) end |
.cached_member_order(queue_account_id) ⇒ Array<String>
The last cached ring order for one queue, WITHOUT a PBX round trip.
Used to audit a manual reorder against what the switchboard actually
showed the user when they dragged.
106 107 108 109 |
# File 'app/services/phone/queue_roster.rb', line 106 def cached_member_order(queue_account_id) snapshot = cached_snapshot snapshot&.dig(queue_account_id, 'members')&.pluck('account_id') || [] end |
.refresh(pbx_snapshot = nil) ⇒ Hash
Force-writes the cache. Called by PullPhoneQueueStatusWorker after each
pull so the switchboard's first render never pays the per-queue round
trip itself. An INCOMPLETE pull (get_queues_snapshot silently skips
errored queues) is MERGED over the cache rather than dropped: a queue
that didn't answer keeps its last known entry, so the page never loses
it and the drift audit keeps its baseline. Completeness still gates that
audit — PullPhoneQueueStatusWorker checks it on the returned snapshot —
which is the only place it was ever load-bearing.
84 85 86 87 88 89 90 91 |
# File 'app/services/phone/queue_roster.rb', line 84 def refresh(pbx_snapshot = nil) pbx_snapshot = (pbx_snapshot || fetch_pbx_snapshot).slice(*PhoneQueue::REGISTRY.keys) cache_snapshot(pbx_snapshot) pbx_snapshot rescue StandardError => e Rails.logger.error "[Phone::QueueRoster] refresh failed: #{e.class}: #{e.}" {} end |
.snapshot ⇒ Hash{Integer => Hash}
Returns queue account id => { 'name', 'strategy',
'queue' => PhoneQueue, 'members' => [{ 'account_id', 'fullname', 'order',
'employee_id', 'employee_name' }], 'stale' => Boolean }; {} when a cold
cache is waiting for its asynchronous prewarm.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'app/services/phone/queue_roster.rb', line 52 def snapshot if (pbx_snapshot = Rails.cache.read(CACHE_KEY)) Rails.cache.write(LAST_KNOWN_CACHE_KEY, pbx_snapshot, unless_exist: true, expires_in: LAST_KNOWN_CACHE_TTL) return merge_employees(pbx_snapshot.deep_dup, stale: false) end if (pbx_snapshot = Rails.cache.read(LAST_KNOWN_CACHE_KEY)) enqueue_refresh return merge_employees(pbx_snapshot.deep_dup, stale: true) end enqueue_refresh {} rescue StandardError => e Rails.logger.error "[Phone::QueueRoster] snapshot failed: #{e.class}: #{e.}" {} end |