BF cache-key slug drift on ws_socket_auth_*; resilient User column in watch -v
Handler::cacheAuthenticatedUser() and ::cleanupChannelConnections() were
writing/forgetting ws_socket_auth_<rawSocketId> while
WebsocketService::getAuth() and ::setUserAuthed() have always slugged
("123.456" → "123-456"). Result: the cache write was reachable from the
package's own writer path but not from the service-layer reader, so the
admin tooling (websockets:watch -v) saw cache misses and rendered #<id>
instead of the configured IdentityFormatter output.
Also: WatchStats now batch-loads missing users via the configured auth
provider model in one query per render, so the User column still renders
the full formatter shape even when the per-socket cache blob predates
the writer or got evicted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a46243a706
commit
dd6be893a6
|
|
@ -114,9 +114,24 @@ class WatchStats extends Command
|
|||
return;
|
||||
}
|
||||
|
||||
// Verbose: one summary row per channel, then one sub-row per connection.
|
||||
// The sub-rows fill the User and Duration columns; the summary fills
|
||||
// Channel and Connections. Dashes mark "not applicable on this row".
|
||||
// Verbose: collect every authed userId whose per-socket cache blob is
|
||||
// missing, batch-load them from the auth provider's user model in one
|
||||
// query, and reuse the resolved subject when formatting each row. This
|
||||
// covers two real cases: connections that predate the cache writer
|
||||
// being added, and Redis evictions under memory pressure.
|
||||
$missingByUserId = [];
|
||||
foreach ($perChannel as $sockets) {
|
||||
foreach ($sockets as $socketId) {
|
||||
if (! isset($authedUsers[$socketId])) {
|
||||
continue;
|
||||
}
|
||||
if (! WebsocketService::getAuth($socketId)) {
|
||||
$missingByUserId[$authedUsers[$socketId]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$usersById = $this->loadAuthUsers(array_keys($missingByUserId));
|
||||
|
||||
$now = time();
|
||||
$rows = [];
|
||||
foreach ($perChannel as $channel => $sockets) {
|
||||
|
|
@ -131,7 +146,7 @@ class WatchStats extends Command
|
|||
$rows[] = [
|
||||
' <fg=gray>↳</>',
|
||||
"<fg=gray>{$socketId}</>",
|
||||
$this->formatUser($socketId, $authedUsers),
|
||||
$this->formatUser($socketId, $authedUsers, $usersById),
|
||||
$this->formatDuration($socketId, $now),
|
||||
];
|
||||
}
|
||||
|
|
@ -140,26 +155,55 @@ class WatchStats extends Command
|
|||
$this->table(['Channel', 'Connections', 'User', 'Duration'], $rows);
|
||||
}
|
||||
|
||||
private function formatUser(string $socketId, array $authedUsers): string
|
||||
/**
|
||||
* Load users by id from the configured auth provider's model.
|
||||
*
|
||||
* Returns [id => model] for found ids; absent ids simply don't appear.
|
||||
* Returns [] silently on any failure (no auth provider, model missing,
|
||||
* DB unreachable) — this is admin tooling, not a critical path.
|
||||
*/
|
||||
private function loadAuthUsers(array $ids): array
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$model = config('auth.providers.users.model');
|
||||
if (! is_string($model) || ! class_exists($model)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return $model::query()
|
||||
->whereIn('id', $ids)
|
||||
->get()
|
||||
->keyBy('id')
|
||||
->all();
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function formatUser(string $socketId, array $authedUsers, array $usersById): string
|
||||
{
|
||||
if (! isset($authedUsers[$socketId])) {
|
||||
return '<fg=gray>Guest</>';
|
||||
}
|
||||
|
||||
$subject = WebsocketService::getAuth($socketId);
|
||||
$userId = $authedUsers[$socketId];
|
||||
$subject = WebsocketService::getAuth($socketId) ?: ($usersById[$userId] ?? null);
|
||||
|
||||
if (! $subject) {
|
||||
// Authed-but-expired: the socket is in ws_socket_authed_users
|
||||
// but the per-socket user blob has fallen out of cache. Show the
|
||||
// bare id from authedUsers so the connection isn't mislabeled
|
||||
// as a Guest.
|
||||
return '<fg=yellow>#' . $authedUsers[$socketId] . '</>';
|
||||
// Auth provider couldn't resolve the user either (deleted? auth
|
||||
// model not Eloquent?). Fall back to the bare id rather than
|
||||
// mislabeling as Guest, so debugging stays accurate.
|
||||
return '<fg=yellow>#' . $userId . '</>';
|
||||
}
|
||||
|
||||
// Delegate formatting to the bound IdentityFormatter so apps with
|
||||
// non-User subjects (Company, ApiClient, etc.) can render their own
|
||||
// shape without forking the package.
|
||||
$formatter = app(IdentityFormatter::class);
|
||||
return $formatter->format($subject, $socketId);
|
||||
// Delegate to the bound IdentityFormatter so apps with non-User
|
||||
// subjects (Company, ApiClient, etc.) can render their own shape
|
||||
// without forking the package.
|
||||
return app(IdentityFormatter::class)->format($subject, $socketId);
|
||||
}
|
||||
|
||||
private function formatDuration(string $socketId, int $now): string
|
||||
|
|
|
|||
|
|
@ -963,7 +963,11 @@ class Handler implements MessageComponentInterface
|
|||
protected function cleanupChannelConnections(ConnectionInterface $connection): void
|
||||
{
|
||||
$cacheUpdates = [];
|
||||
$cacheDeletes = ['ws_socket_auth_' . $connection->socketId];
|
||||
// Slug socketId so this key matches what WebsocketService::getAuth()
|
||||
// reads and what setUserAuthed() writes; raw socketIds contain dots
|
||||
// ("123.456") that the slug() call strips ("123-456") and previously
|
||||
// produced read/write key drift that masked auth state on disconnect.
|
||||
$cacheDeletes = ['ws_socket_auth_' . str()->slug($connection->socketId)];
|
||||
$socketId = $connection->socketId;
|
||||
|
||||
foreach ($this->channel_connections as $channel => $connections) {
|
||||
|
|
@ -1350,9 +1354,13 @@ class Handler implements MessageComponentInterface
|
|||
$authed_users = cache()->get('ws_socket_authed_users') ?? [];
|
||||
$authed_users[$socketId] = $user->id;
|
||||
|
||||
// Single batched cache write - reduces 3 operations to 1
|
||||
// Single batched cache write - reduces 3 operations to 1.
|
||||
// Slug the socketId so this key matches WebsocketService::getAuth()
|
||||
// and setUserAuthed() — raw socketIds like "123.456" don't survive
|
||||
// a round-trip with slugged ("123-456") readers, and previously this
|
||||
// write was effectively unreachable from the rest of the codebase.
|
||||
cache()->setMultiple([
|
||||
'ws_socket_auth_' . $socketId => $user,
|
||||
'ws_socket_auth_' . str()->slug($socketId) => $user,
|
||||
'ws_socket_authed_users' => $authed_users
|
||||
]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue