fix: cast cached post count to int and align local cache default to redis
Production crashed on every Inertia request after the PostHog branch landed: TypeError: App\Models\Account::cachedPostCount(): Return value must be of type int, string returned at app/Models/Traits/HasUsage.php:80 Root cause: Laravel's RedisStore optimises is_numeric values by storing them raw (not serialised) so they remain INCR/DECR-able atomically. The side effect is that an int written via Cache::put comes back as a string on read. The strict ': int' return type on cachedPostCount then threw a TypeError. Local dev and CI used the file/array/database drivers respectively, which serialise everything blindly and preserve the int type, so the bug never surfaced before deploy. Fixes: - Cast the Cache::remember result to (int) — defensive, survives any driver-specific behaviour. Documented inline so the cast is not later removed as redundant. - Change config/cache.php default from 'database' to 'redis' so local dev matches prod by default and similar driver-specific bugs surface before merge instead of after deploy. - Regression test that seeds the cache with a literal string (mimics the production Redis read) and asserts cachedPostCount still returns an int.
This commit is contained in:
parent
a829553a24
commit
e35b8df86a
3 changed files with 30 additions and 2 deletions
|
|
@ -66,6 +66,15 @@ public function featureLimits(): array
|
|||
}
|
||||
|
||||
/**
|
||||
* The `(int)` cast on the cached value is load-bearing: Laravel's
|
||||
* RedisStore skips serialize()/unserialize() for is_numeric values so
|
||||
* they can be incremented atomically — the side effect is that an int
|
||||
* stored via `Cache::put` comes back as a string on read. Without the
|
||||
* cast, the strict `: int` return type throws a TypeError under the
|
||||
* Redis cache driver (production). File/array/database drivers don't
|
||||
* have this optimisation and preserve the type, which is why the bug
|
||||
* never surfaced in tests or local dev.
|
||||
*
|
||||
* @param array<int, string> $workspaceIds
|
||||
*/
|
||||
private function cachedPostCount(array $workspaceIds): int
|
||||
|
|
@ -74,7 +83,7 @@ private function cachedPostCount(array $workspaceIds): int
|
|||
return 0;
|
||||
}
|
||||
|
||||
return Cache::remember(
|
||||
return (int) Cache::remember(
|
||||
"account:{$this->id}:posts_count",
|
||||
self::POST_COUNT_CACHE_TTL,
|
||||
fn () => Post::whereIn('workspace_id', $workspaceIds)->count(),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
'default' => env('CACHE_STORE', 'redis'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -104,3 +104,22 @@
|
|||
// No cache entry should be written for the empty case.
|
||||
expect(Cache::has("account:{$this->account->id}:posts_count"))->toBeFalse();
|
||||
});
|
||||
|
||||
test('postCount survives a string-typed cache value (Redis serializer quirk)', function () {
|
||||
// Laravel's RedisStore stores numeric values raw (not serialised) so they
|
||||
// can be INCRemented atomically. The side effect: an int written via
|
||||
// Cache::put comes back as a string on read. The test driver is `array`
|
||||
// which preserves type, so we seed the cache with a literal string here
|
||||
// to mimic what production sees and assert the return type stays int.
|
||||
Workspace::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'user_id' => $this->owner->id,
|
||||
]);
|
||||
|
||||
Cache::put("account:{$this->account->id}:posts_count", '42', 300);
|
||||
|
||||
$usage = $this->account->usage();
|
||||
|
||||
expect($usage['postCount'])->toBe(42);
|
||||
expect($usage['postCount'])->toBeInt();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue