Reduce Meet poll load with shared cache, ETag, and slower clients.
Deploy Ladill Meet / deploy (push) Successful in 2m44s

Heavy per-participant poll queries were saturating the shared host during
live rooms. Cache the shared room payload for 2s, return 304 when unchanged,
throttle breakout expiry and captions, and slow client polling (6s / 20s
when hidden) so concurrent attendees no longer fan out full Laravel work.
This commit is contained in:
isaacclad
2026-07-15 19:59:52 +00:00
parent cdb81b82da
commit 86fa37b9fd
3 changed files with 193 additions and 72 deletions
@@ -23,6 +23,8 @@ use App\Services\Meet\WebinarService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class MeetingRoomController extends Controller
@@ -387,7 +389,12 @@ class MeetingRoomController extends Controller
]);
}
public function poll(Request $request, Session $session): JsonResponse
/**
* Room state for clients. Shared payload is cached briefly so concurrent
* pollers (every participant every few seconds) do not each re-run the full
* query fan-out. Per-participant media is still computed fresh.
*/
public function poll(Request $request, Session $session): JsonResponse|Response
{
if (! $session->isLive()) {
$townHall = app(TownHallService::class);
@@ -419,77 +426,128 @@ class MeetingRoomController extends Controller
$participant = $this->currentParticipant($request, $session);
app(BreakoutService::class)->closeExpired($session);
// Breakout expiry is not latency-critical; run at most once per 30s/session.
if (Cache::add("meet:breakout-close:{$session->id}", 1, 30)) {
app(BreakoutService::class)->closeExpired($session);
}
$participant->refresh();
$session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]);
$shared = $this->sharedPollPayload($session);
$media = $this->sessions->mediaSessionState($participant);
$avatars = ParticipantPresenter::avatarMap($session->participants, $session->room->host_user_ref);
$etag = '"'.md5(($shared['_etag'] ?? '').'|'.$participant->id.'|'.json_encode($media)).'"';
$messages = $this->chat->recentMessages($session, 50);
$reactions = $session->reactions()->where('created_at', '>=', now()->subMinutes(5))->latest()->limit(20)->get();
$qa = app(\App\Services\Meet\QaService::class)->forSession($session, true);
$polls = $session->polls()->where('status', 'open')->with('votes')->get();
$breakouts = $session->breakoutRooms()->where('status', 'open')->get();
$broadcasts = $session->messages()->where('type', 'breakout_broadcast')->where('created_at', '>=', now()->subMinutes(30))->latest()->limit(5)->get();
$files = $session->sessionFiles()
->where(function ($q) {
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
})
->latest()
->limit(20)
->get();
if ($request->headers->get('If-None-Match') === $etag) {
return response('', 304)->withHeaders([
'ETag' => $etag,
'Cache-Control' => 'private, no-cache',
]);
}
return response()->json(array_merge([
'participants' => $session->participants->map(
fn ($p) => ParticipantPresenter::toArray($p, $avatars, $session->room->host_user_ref)
),
'waiting_count' => $session->participants->where('status', 'waiting')->count(),
'messages' => $messages->map(fn ($m) => [
'uuid' => $m->uuid,
'sender_name' => $m->sender_name,
'body' => $m->body,
'type' => $m->type,
'created_at' => $m->created_at->toIso8601String(),
]),
'reactions' => $reactions->map(fn ($r) => [
'sender_name' => $r->sender_name,
'emoji' => $r->emoji,
'created_at' => $r->created_at->toIso8601String(),
]),
'qa' => $qa->map(fn ($q) => [
'uuid' => $q->uuid,
'asker_name' => $q->asker_name,
'question' => $q->question,
'status' => $q->status,
'answer' => $q->answer,
'upvotes' => $q->upvotes,
]),
'polls' => $polls->map(fn ($p) => [
'uuid' => $p->uuid,
'question' => $p->question,
'options' => $p->options,
'votes' => $p->votes->count(),
]),
'breakouts' => $breakouts->map(fn ($b) => [
'uuid' => $b->uuid,
'name' => $b->name,
'closes_at' => $b->closes_at?->toIso8601String(),
]),
'broadcasts' => $broadcasts->map(fn ($m) => [
'body' => $m->body,
'sender_name' => $m->sender_name,
'created_at' => $m->created_at->toIso8601String(),
]),
'files' => $files->map(fn ($f) => [
'uuid' => $f->uuid,
'original_name' => $f->original_name,
'file_size' => $f->file_size,
]),
'media' => $this->sessions->mediaSessionState($participant),
'breakouts_active' => app(BreakoutService::class)->hasActiveBreakouts($session),
], app(StageLayoutService::class)->config($session->room)));
unset($shared['_etag']);
return response()
->json(array_merge($shared, ['media' => $media]))
->withHeaders([
'ETag' => $etag,
'Cache-Control' => 'private, no-cache',
]);
}
/**
* Session-wide poll fields shared by every participant (2s TTL).
*
* @return array<string, mixed>
*/
protected function sharedPollPayload(Session $session): array
{
$cacheKey = "meet:poll:shared:{$session->id}";
return Cache::remember($cacheKey, 2, function () use ($session) {
// Ensure room relation is available without re-querying when already loaded.
$session->loadMissing('room');
$session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]);
$hostRef = $session->room->host_user_ref;
$avatars = ParticipantPresenter::avatarMap($session->participants, $hostRef);
$messages = $this->chat->recentMessages($session, 30);
$reactions = $session->reactions()
->where('created_at', '>=', now()->subMinutes(2))
->latest()
->limit(15)
->get();
$qa = app(\App\Services\Meet\QaService::class)->forSession($session, true);
$polls = $session->polls()->where('status', 'open')->with('votes')->get();
$breakouts = $session->breakoutRooms()->where('status', 'open')->get();
$broadcasts = $session->messages()
->where('type', 'breakout_broadcast')
->where('created_at', '>=', now()->subMinutes(30))
->latest()
->limit(5)
->get();
$files = $session->sessionFiles()
->where(function ($q) {
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
})
->latest()
->limit(20)
->get();
$payload = array_merge([
'participants' => $session->participants->map(
fn ($p) => ParticipantPresenter::toArray($p, $avatars, $hostRef)
)->values()->all(),
'waiting_count' => $session->participants->where('status', 'waiting')->count(),
'messages' => $messages->map(fn ($m) => [
'uuid' => $m->uuid,
'sender_name' => $m->sender_name,
'body' => $m->body,
'type' => $m->type,
'created_at' => $m->created_at->toIso8601String(),
])->values()->all(),
'reactions' => $reactions->map(fn ($r) => [
'sender_name' => $r->sender_name,
'emoji' => $r->emoji,
'created_at' => $r->created_at->toIso8601String(),
])->values()->all(),
'qa' => $qa->map(fn ($q) => [
'uuid' => $q->uuid,
'asker_name' => $q->asker_name,
'question' => $q->question,
'status' => $q->status,
'answer' => $q->answer,
'upvotes' => $q->upvotes,
])->values()->all(),
'polls' => $polls->map(fn ($p) => [
'uuid' => $p->uuid,
'question' => $p->question,
'options' => $p->options,
'votes' => $p->votes->count(),
])->values()->all(),
'breakouts' => $breakouts->map(fn ($b) => [
'uuid' => $b->uuid,
'name' => $b->name,
'closes_at' => $b->closes_at?->toIso8601String(),
])->values()->all(),
'broadcasts' => $broadcasts->map(fn ($m) => [
'body' => $m->body,
'sender_name' => $m->sender_name,
'created_at' => $m->created_at->toIso8601String(),
])->values()->all(),
'files' => $files->map(fn ($f) => [
'uuid' => $f->uuid,
'original_name' => $f->original_name,
'file_size' => $f->file_size,
])->values()->all(),
'breakouts_active' => $breakouts->isNotEmpty(),
], app(StageLayoutService::class)->config($session->room));
$payload['_etag'] = md5(json_encode($payload));
return $payload;
});
}
public function mediaToken(Request $request, Session $session): JsonResponse