Reduce Meet poll load with shared cache, ETag, and slower clients.
Deploy Ladill Meet / deploy (push) Successful in 2m44s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user