Skip to content

Commit b31a40a

Browse files
authored
Merge pull request #46 from iazaran/Scalibility-improvementsd
Scalibility improvementsd
2 parents e2187dd + c6e2b0f commit b31a40a

17 files changed

Lines changed: 996 additions & 90 deletions

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to the `iazaran/smart-cache` package will be documented in t
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.13.0] - 2026-06-16
9+
### Added
10+
- Declarative model invalidation rules via a protected `cacheInvalidation(): array` method. Existing fluent setters still work and are merged with declared rules.
11+
- `Model::flushCacheTags()` for explicit invalidation after `saveQuietly()`, query-builder updates/deletes, upserts, mass inserts, raw SQL, or any other path that bypasses Eloquent events.
12+
- `TagFlushed` event, including the tag name, live key count, and source (`manual`, `model`, or `model_helper`), when cache events are enabled.
13+
- Config options for metadata locks (`smart-cache.metadata_lock.*`) and transaction-aware model invalidation (`smart-cache.model_invalidation.after_commit`).
14+
15+
### Changed
16+
- Model auto-invalidation now defers cache flushing until the active database transaction commits by default. Rollbacks no longer flush cache, and nested transactions wait for the outer commit. Set `smart-cache.model_invalidation.after_commit` to `false` to restore immediate invalidation.
17+
- Tag metadata writes now register before the cache value is written and use a short Laravel cache lock when the store supports `LockProvider`, reducing lost tag-index updates under concurrent writers while preserving best-effort behavior for stores without locks.
18+
- Tag reads lazily prune expired or missing key references, and tag flushes correctly handle keys written under an active namespace.
19+
20+
### Fixed
21+
- `SmartCache::add()` no longer leaks active tags into the next write when the atomic add fails because the key already exists.
22+
- Cache DNA deduplicated writes now still refresh tag/managed-key metadata and only skip the value write when the cached value is still present.
23+
- Dependency invalidation now refreshes dependency metadata before traversal, so long-running workers do not miss relationships added by another process after the local map was loaded.
24+
825
## [1.12.2] - 2026-05-29
926
### Security
1027
- Upgraded every remaining `symfony/*` lockfile entry from `v8.0.8` to `v8.1.0` (>= patched lines `8.0.12` / `8.0.13`) to clear the rest of the open Dependabot advisories plus two pending CVEs surfaced by `composer audit`. Runtime: `symfony/mailer` (CVE-2026-45068, `SendmailTransport` argument injection via dash-prefixed recipient), `symfony/routing` (CVE-2026-45065, `UrlGenerator` route-requirement bypass via unanchored regex alternation; CVE-2026-48784, dot-segment encoding skip), `symfony/http-foundation` (CVE-2026-48736), `symfony/http-kernel` (CVE-2026-45075, `#[IsGranted(methods: ['GET'])]` filter bypass via `HEAD`). Dev: `symfony/yaml` (CVE-2026-45133 uncontrolled recursion, CVE-2026-45304 collection-alias "Billion Laughs", CVE-2026-45305 `Parser::cleanup()` ReDoS). `composer audit` is now clean across runtime and dev scopes. `composer.json` is unchanged — the existing ranges already permitted these versions; Dependabot was failing because of a stale resolver state on its side.

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ composer test
3434
composer test-coverage
3535
```
3636

37-
All pull requests must pass the existing test suite (425+ tests) before merging. If you add a new feature, include corresponding test cases.
37+
All pull requests must pass the existing test suite (485 tests) before merging. If you add a new feature, include corresponding test cases.
3838

3939
## Coding Style
4040

@@ -52,4 +52,4 @@ In short, when you submit code changes, your submissions are understood to be un
5252

5353
## License
5454

55-
By contributing, you agree that your contributions will be licensed under its MIT License.
55+
By contributing, you agree that your contributions will be licensed under its MIT License.

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,30 @@ class User extends Model
221221
{
222222
use CacheInvalidation;
223223

224-
public function getCacheKeysToInvalidate(): array
224+
protected function cacheInvalidation(): array
225225
{
226-
return ["user_{$this->id}_profile", "user_{$this->id}_posts", 'users_list_*'];
226+
return [
227+
'keys' => ['user_{id}_profile', 'user_{id}_posts'],
228+
'tags' => ['users', 'user_{id}', 'team_{team_id}'],
229+
'patterns' => ['users_list_*'],
230+
'dependencies' => ['team_{team_id}_summary'],
231+
];
227232
}
228233
}
234+
235+
// Event-blind writes can flush explicitly:
236+
User::where('status', 'inactive')->update(['archived' => true]);
237+
User::flushCacheTags(['users']);
229238
```
230239

240+
Model invalidation is deferred until the current database transaction commits by default (`smart-cache.model_invalidation.after_commit = true`). This prevents another request from re-caching pre-commit data between an Eloquent event and the final commit. Set the flag to `false` if you need the historical immediate behavior.
241+
242+
Eloquent events do not fire for `saveQuietly()`, query-builder `update()` / `delete()`, `upsert()`, mass `insert()`, or raw SQL. For those paths, call `flushCacheTags()` or `SmartCache::flushTags()` explicitly.
243+
244+
### Choosing Cache Tags
245+
246+
Tags should describe the data used to build a response, not the controller that built it. Start with the tables or models read by the endpoint: list endpoints usually use coarse tags such as `products`, while item endpoints can add instance tags such as `product_123`. Over-tagging causes extra refreshes; under-tagging leaves stale data behind. For hard-to-map endpoints, enable Laravel's query log in a test and compare the tables read during response generation with the tags declared for that cache entry.
247+
231248
### Encryption at Rest
232249

233250
```php
@@ -268,6 +285,10 @@ SmartCache::deleteMultiple(['key1', 'key2', 'key3']);
268285
config(['smart-cache.events.enabled' => true]);
269286
Event::listen(CacheHit::class, fn($e) => Log::info("Hit: {$e->key}"));
270287
Event::listen(CacheMissed::class, fn($e) => Log::warning("Miss: {$e->key}"));
288+
Event::listen(TagFlushed::class, fn($e) => Log::notice("Flushed {$e->tag}", [
289+
'keys' => $e->keyCount,
290+
'source' => $e->source, // manual, model, or model_helper
291+
]));
271292
```
272293

273294
### Monitoring & Dashboard
@@ -355,6 +376,8 @@ return [
355376
'self_healing' => ['enabled' => true], // Auto-evict corrupted entries
356377
'swr' => ['single_flight' => false], // v1.12.0: opt-in single-flight refresh
357378
'managed_keys' => ['max_tracked' => 0], // v1.12.0: 0 = unlimited (default)
379+
'metadata_lock' => ['enabled' => true, 'ttl' => 5, 'wait' => 1],
380+
'model_invalidation' => ['after_commit' => true],
358381
'dashboard' => ['enabled' => false, 'prefix' => 'smart-cache', 'middleware' => ['web']],
359382
'warmers' => [], // Cache warmer classes for smart-cache:warm
360383
];
@@ -383,7 +406,7 @@ $users = SmartCache::get('users');
383406
## Testing
384407

385408
```bash
386-
composer test # 471 tests, 1,936 assertions
409+
composer test # 485 tests, 1,972 assertions
387410
composer test-coverage # with code coverage
388411
```
389412

TESTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ vendor/bin/phpunit
2929
Current suite size:
3030

3131
```bash
32-
# 452 tests, 1,895 assertions
32+
# 485 tests, 1,972 assertions
3333
```
3434

3535
### Run Specific Test Suites

config/smart-cache.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,39 @@
214214
'max_tracked' => null,
215215
],
216216

217+
/*
218+
|--------------------------------------------------------------------------
219+
| Metadata Locks
220+
|--------------------------------------------------------------------------
221+
|
222+
| Tag indexes and dependency metadata are small shared cache entries. When
223+
| the selected cache store supports Laravel atomic locks, SmartCache wraps
224+
| metadata mutations in a short lock to reduce lost updates during concurrent
225+
| writes. Stores without lock support keep the historical best-effort
226+
| behavior.
227+
|
228+
*/
229+
'metadata_lock' => [
230+
'enabled' => true,
231+
'ttl' => 5,
232+
'wait' => 1,
233+
],
234+
235+
/*
236+
|--------------------------------------------------------------------------
237+
| Model Invalidation
238+
|--------------------------------------------------------------------------
239+
|
240+
| Eloquent model events fire while a database transaction is still open.
241+
| Deferring invalidation until commit prevents another request from
242+
| re-caching pre-commit data between the flush and the commit. Set
243+
| `after_commit` to false to restore immediate invalidation.
244+
|
245+
*/
246+
'model_invalidation' => [
247+
'after_commit' => true,
248+
],
249+
217250
/*
218251
|--------------------------------------------------------------------------
219252
| Chunk Registry
@@ -297,6 +330,7 @@
297330
'key_written' => true,
298331
'key_forgotten' => true,
299332
'optimization_applied' => true,
333+
'tag_flushed' => true,
300334
],
301335
],
302336

@@ -331,4 +365,4 @@
331365
// 'users' => App\CacheWarmers\UserCacheWarmer::class,
332366
// 'products' => App\CacheWarmers\ProductCacheWarmer::class,
333367
],
334-
];
368+
];

docs/index.html

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,19 @@ <h2>Configuration Options</h2>
10261026

10271027
'events' => [
10281028
'enabled' => false, // Enable for monitoring
1029+
'dispatch' => [
1030+
'tag_flushed' => true,
1031+
],
1032+
],
1033+
1034+
'metadata_lock' => [
1035+
'enabled' => true, // Lock tag/dependency metadata when supported
1036+
'ttl' => 5,
1037+
'wait' => 1,
1038+
],
1039+
1040+
'model_invalidation' => [
1041+
'after_commit' => true, // Flush after DB commit when inside a transaction
10291042
],
10301043

10311044
'monitoring' => [
@@ -1281,7 +1294,7 @@ <h2>📡 Cache Events</h2>
12811294
config(['smart-cache.events.enabled' => true]);
12821295

12831296
// Listen to cache operations
1284-
use SmartCache\Events\{CacheHit, CacheMissed, KeyWritten, KeyForgotten, OptimizationApplied};
1297+
use SmartCache\Events\{CacheHit, CacheMissed, KeyWritten, KeyForgotten, OptimizationApplied, TagFlushed};
12851298

12861299
Event::listen(CacheHit::class, function ($event) {
12871300
Log::info("Cache hit: {$event->key}", ['tags' => $event->tags]);
@@ -1293,6 +1306,13 @@ <h2>📡 Cache Events</h2>
12931306

12941307
Event::listen(OptimizationApplied::class, function ($event) {
12951308
Log::info("Optimized {$event->key}: {$event->strategy} - {$event->ratio}% reduction");
1309+
});
1310+
1311+
Event::listen(TagFlushed::class, function ($event) {
1312+
Log::notice("Flushed {$event->tag}", [
1313+
'keys' => $event->keyCount,
1314+
'source' => $event->source, // manual, model, or model_helper
1315+
]);
12961316
});</code></pre>
12971317

12981318
<div class="alert alert-warning">
@@ -1402,19 +1422,33 @@ <h3>Model Auto-Invalidation</h3>
14021422
{
14031423
use CacheInvalidation;
14041424

1405-
public function getCacheKeysToInvalidate(): array
1425+
protected function cacheInvalidation(): array
14061426
{
14071427
return [
1408-
"user_{$this->id}_profile",
1409-
"user_{$this->id}_posts",
1410-
'users_list_*'
1428+
'keys' => ['user_{id}_profile', 'user_{id}_posts'],
1429+
'tags' => ['users', 'user_{id}', 'team_{team_id}'],
1430+
'patterns' => ['users_list_*'],
1431+
'dependencies' => ['team_{team_id}_summary'],
14111432
];
14121433
}
14131434
}
14141435

14151436
// Cache automatically cleared when user changes!
14161437
$user = User::find(1);
1417-
$user->update(['name' => 'New Name']); // Cache cleared automatically</code></pre>
1438+
$user->update(['name' => 'New Name']); // Flushes after the transaction commits
1439+
1440+
// Event-blind writes should flush explicitly.
1441+
User::where('status', 'inactive')->update(['archived' => true]);
1442+
User::flushCacheTags(['users']);</code></pre>
1443+
1444+
<div class="alert alert-warning">
1445+
<strong>Eloquent event blind spots.</strong> Model auto-invalidation depends on Eloquent events. It does not run for <code>saveQuietly()</code>, query-builder <code>update()</code> / <code>delete()</code>, <code>upsert()</code>, mass <code>insert()</code>, or raw SQL. Use <code>Model::flushCacheTags()</code> or <code>SmartCache::flushTags()</code> in those paths.
1446+
</div>
1447+
1448+
<p>When an Eloquent event fires inside a database transaction, SmartCache defers invalidation until the outer transaction commits by default. This avoids flushing cache and then letting another request re-cache pre-commit data. Disable with <code>smart-cache.model_invalidation.after_commit = false</code> if your application needs immediate invalidation.</p>
1449+
1450+
<h3>Dependency Map Cookbook</h3>
1451+
<p>Declare tags for the data used to build the response, not for the controller or route name. Use coarse tags for list endpoints such as <code>products</code>, add instance tags for detail endpoints such as <code>product_123</code>, and prefer over-tagging to under-tagging. For opaque endpoints, capture Laravel's query log in a test and compare the selected tables with the cache tags you declared.</p>
14181452

14191453
<h2>Custom Optimization Strategies</h2>
14201454
<p>Create custom optimization strategies for your specific needs:</p>
@@ -1837,6 +1871,16 @@ <h2>Configuration Reference</h2>
18371871
'max_tracked' => 0, // v1.12.0: cap the in-memory managed-keys index (0 = unlimited)
18381872
],
18391873

1874+
'metadata_lock' => [
1875+
'enabled' => true, // Lock tag/dependency metadata when the store supports locks
1876+
'ttl' => 5,
1877+
'wait' => 1,
1878+
],
1879+
1880+
'model_invalidation' => [
1881+
'after_commit' => true, // Defer model invalidation until DB commit
1882+
],
1883+
18401884
'dashboard' => [
18411885
'enabled' => false,
18421886
'prefix' => 'smart-cache',
@@ -2177,11 +2221,14 @@ <h2 id="invalidation-api">🔗 Cache Invalidation & Dependencies</h2>
21772221

21782222
<div class="method">
21792223
<div class="method-name">SmartCache::flushTags()</div>
2180-
<div class="method-signature">SmartCache::flushTags(string|array $tags): bool</div>
2181-
<p>Flush all cache entries associated with given tags.</p>
2224+
<div class="method-signature">SmartCache::flushTags(string|array $tags, string $source = 'manual'): bool</div>
2225+
<p>Flush all cache entries associated with given tags. Tag metadata is pruned lazily when tags are read, so expired entries do not build up indefinitely in rarely flushed tags.</p>
21822226
<div class="parameter">
21832227
<span class="parameter-name">$tags</span> <span class="parameter-type">(string|array)</span> - Tag name(s)
21842228
</div>
2229+
<div class="parameter">
2230+
<span class="parameter-name">$source</span> <span class="parameter-type">(string)</span> - Optional source label exposed on the TagFlushed event
2231+
</div>
21852232
<div class="parameter">
21862233
<span class="return-type">Returns:</span> bool - True on success
21872234
</div>
@@ -2191,6 +2238,17 @@ <h2 id="invalidation-api">🔗 Cache Invalidation & Dependencies</h2>
21912238
</div>
21922239
</div>
21932240

2241+
<div class="method">
2242+
<div class="method-name">Model::flushCacheTags()</div>
2243+
<div class="method-signature">User::flushCacheTags(string|array|null $tags = null): bool</div>
2244+
<p>Flush model cache tags explicitly after writes that bypass Eloquent events, such as query-builder updates, upserts, mass inserts, quiet saves, or raw SQL.</p>
2245+
<div class="example">
2246+
<strong>Example:</strong>
2247+
<pre><code class="language-php">User::where('active', false)->delete();
2248+
User::flushCacheTags(['users']);</code></pre>
2249+
</div>
2250+
</div>
2251+
21942252
<div class="method">
21952253
<div class="method-name">SmartCache::dependsOn()</div>
21962254
<div class="method-signature">SmartCache::dependsOn(string $key, string|array $dependencies): static</div>
@@ -2903,7 +2961,7 @@ <h2>Package Test Suite</h2>
29032961
<p>SmartCache ships with automated coverage for the Laravel-compatible API surface, optimization strategies, SWR patterns, invalidation, memoization, dashboard command execution, and large-data recovery behavior.</p>
29042962

29052963
<pre><code class="language-bash">composer test
2906-
# 471 tests, 1,936 assertions
2964+
# 485 tests, 1,972 assertions
29072965

29082966
vendor/bin/phpunit tests/Unit/Strategies/ChunkingStrategyTest.php --testdox
29092967
vendor/bin/phpunit tests/Unit/SmartCacheTest.php --filter "missing_chunk|self_healing" --testdox</code></pre>

src/Contracts/SmartCache.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,4 +434,4 @@ public function rememberWithStampedeProtection(string $key, int $ttl, \Closure $
434434
* @return mixed
435435
*/
436436
public function rememberIf(string $key, mixed $ttl, \Closure $callback, callable $condition): mixed;
437-
}
437+
}

src/Events/TagFlushed.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace SmartCache\Events;
4+
5+
class TagFlushed
6+
{
7+
/**
8+
* The tag that was flushed.
9+
*
10+
* @var string
11+
*/
12+
public string $tag;
13+
14+
/**
15+
* Number of live keys associated with the tag when it was flushed.
16+
*
17+
* @var int
18+
*/
19+
public int $keyCount;
20+
21+
/**
22+
* Source of the flush operation, such as manual, model, or model_helper.
23+
*
24+
* @var string
25+
*/
26+
public string $source;
27+
28+
/**
29+
* Create a new event instance.
30+
*
31+
* @param string $tag
32+
* @param int $keyCount
33+
* @param string $source
34+
*/
35+
public function __construct(string $tag, int $keyCount, string $source = 'manual')
36+
{
37+
$this->tag = $tag;
38+
$this->keyCount = $keyCount;
39+
$this->source = $source;
40+
}
41+
}

src/Facades/SmartCache.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
* @method static mixed withFallback(callable $callback, mixed $fallback = null)
4848
* @method static mixed throttle(string $key, int $maxAttempts, int $decaySeconds, callable $callback)
4949
* @method static static tags(string|array $tags)
50-
* @method static bool flushTags(string|array $tags)
50+
* @method static bool flushTags(string|array $tags, string $source = 'manual')
5151
* @method static static dependsOn(string $key, string|array $dependencies)
5252
* @method static bool invalidate(string $key)
5353
* @method static int flushPatterns(array $patterns)
@@ -100,4 +100,4 @@ protected static function getFacadeAccessor()
100100
{
101101
return 'smart-cache';
102102
}
103-
}
103+
}

0 commit comments

Comments
 (0)