The `inboxes#index` API endpoint was firing 3 queries per inbox while rendering the response — one each for the polymorphic channel, avatar attachment, and working hours. For accounts with many inboxes this turned a routine list call into a multi-second request — measured at 35 seconds on an account with 5,000 inboxes. Two root causes: 1. `policy_scope` was silently discarding the controller's `.includes(...)` chain because `InboxPolicy::Scope#resolve` returns a fresh `user.assigned_inboxes` relation, ignoring whatever scope is passed in. So the eager loading never actually applied. 2. `Inbox#weekly_schedule` called `working_hours.order(...).select(...)` which fires a new query per inbox even when the association is preloaded. The fix chains the eager loads and ordering after `policy_scope` so they apply to the relation the policy actually returns, adds `:portal` and `:working_hours` to the preload list, and refactors `weekly_schedule` to sort the preloaded collection in Ruby and build the hash directly. Result: queries drop from O(n) to a constant ~15 regardless of inbox count, and total request time drops 5–6× across every account size tested. Response payload is byte-identical. ## How to test 1. Open the agent dashboard for an account with a large number of inboxes (1000+). On a stock dev DB you can seed quickly with `Seeders::AccountSeeder` or by creating a few hundred `Channel::Api` inboxes via the rails console. 2. Hit any UI surface that lists inboxes (settings → inboxes, sidebar inbox list, agent assignment dropdowns). Should feel near-instant where it previously hung. 3. Confirm every inbox shows the same data as before — channel type, working hours, avatar, portal/help-center link. No fields should be missing or different. ## Benchmarks Real HTTP requests via curl against a dev Rails server (median of 5 timed trials after warmup; "Server total" is the time reported in Rails' `Completed 200 OK in <X>ms` log line). | Account | Channel mix | OLD | NEW | Speedup | |---|---|---|---|---| | 1000 inboxes | 3 types, no portals/avatars | 5.87 s | 0.96 s | **6.1×** | | 5000 inboxes | 3 types, no portals/avatars | 33.07 s | 6.93 s | **4.8×** | | 5000 inboxes | 10 types, 25% portals, 10% avatars | 35.04 s | 6.55 s | **5.4×** | Detailed breakdown for the realistic 5000-inbox case: | | OLD | NEW | Δ | |-----------------|-----------|----------|----------| | Server total | 35,042 ms | 6,550 ms | −81% | | Views | 32,034 ms | 6,452 ms | −80% | | ActiveRecord | 2,948 ms | 92 ms | −97% | | Allocations | 42.3 M | 11.3 M | −73% | | Queries | ~15,000 | 15 | −99.9% | ## What changed - `app/controllers/api/v1/accounts/inboxes_controller.rb` — chain `.includes(:channel, :portal, :working_hours, avatar_attachment: :blob).order_by_name` *after* `policy_scope(...)`. The previous code passed them into `policy_scope` but the policy resolver dropped the chain. - `app/models/concerns/out_of_offisable.rb` — `weekly_schedule` now sorts the preloaded `working_hours` collection in Ruby and constructs the result hashes inline, avoiding both the per-inbox query from `.order(...).select(...)` and the `as_json` reflection overhead. Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
64 lines
2.2 KiB
Ruby
64 lines
2.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module OutOfOffisable
|
|
extend ActiveSupport::Concern
|
|
|
|
OFFISABLE_ATTRS = %w[day_of_week closed_all_day open_hour open_minutes close_hour close_minutes open_all_day].freeze
|
|
|
|
included do
|
|
has_many :working_hours, dependent: :destroy_async
|
|
after_create :create_default_working_hours
|
|
end
|
|
|
|
def out_of_office?
|
|
working_hours_enabled? && working_hours.today.closed_now?
|
|
end
|
|
|
|
def working_now?
|
|
!out_of_office?
|
|
end
|
|
|
|
def weekly_schedule
|
|
working_hours.sort_by(&:day_of_week).map do |wh|
|
|
{
|
|
'day_of_week' => wh.day_of_week,
|
|
'closed_all_day' => wh.closed_all_day,
|
|
'open_hour' => wh.open_hour,
|
|
'open_minutes' => wh.open_minutes,
|
|
'close_hour' => wh.close_hour,
|
|
'close_minutes' => wh.close_minutes,
|
|
'open_all_day' => wh.open_all_day
|
|
}
|
|
end
|
|
end
|
|
|
|
# accepts an array of hashes similiar to the format of weekly_schedule
|
|
# [
|
|
# { "day_of_week"=>1,
|
|
# "closed_all_day"=>false,
|
|
# "open_hour"=>9,
|
|
# "open_minutes"=>0,
|
|
# "close_hour"=>17,
|
|
# "close_minutes"=>0,
|
|
# "open_all_day=>false" },...]
|
|
def update_working_hours(params)
|
|
ActiveRecord::Base.transaction do
|
|
params.each do |working_hour|
|
|
working_hours.find_by(day_of_week: working_hour['day_of_week']).update(working_hour.slice(*OFFISABLE_ATTRS))
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def create_default_working_hours
|
|
working_hours.create!(day_of_week: 0, closed_all_day: true, open_all_day: false)
|
|
working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
|
working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
|
working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
|
working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
|
working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
|
working_hours.create!(day_of_week: 6, closed_all_day: true, open_all_day: false)
|
|
end
|
|
end
|