Skip to content

Commit ae72707

Browse files
committed
Merge branch 'release/0.6.35'
2 parents 3481baa + d906423 commit ae72707

File tree

10 files changed

+419
-5
lines changed

10 files changed

+419
-5
lines changed

.version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"strategy": "semver",
33
"major": 0,
44
"minor": 6,
5-
"patch": 34,
5+
"patch": 35,
66
"build": 0
77
}

examples/config/config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ cache:
1717
markdown: true
1818
json: false
1919
xml: false
20+
# Garbage collection settings (optional, defaults shown)
21+
# gc_probability: 0.01 # 1% chance to run GC on cache write
22+
# gc_divisor: 100 # Used with probability for fine-tuning
2023

2124
system:
2225
timezone: US/Eastern

src/Bootstrap.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,14 @@ function Dispatch( Application $App ) : void
6060
echo 'Ouch.';
6161
}
6262
}
63+
64+
/**
65+
* Clear expired cache entries
66+
*
67+
* @param Application $App
68+
* @return int Number of entries removed
69+
*/
70+
function ClearExpiredCache( Application $App ) : int
71+
{
72+
return $App->clearExpiredCache();
73+
}

src/Mvc/Application.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,5 +366,47 @@ function( $Parameters )
366366
}
367367
);
368368
}
369+
370+
/**
371+
* Clear expired cache entries
372+
*
373+
* @return int Number of entries removed
374+
*/
375+
public function clearExpiredCache(): int
376+
{
377+
$Cache = Registry::getInstance()->get( 'ViewCache' );
378+
379+
if( $Cache instanceof \Neuron\Mvc\Cache\ViewCache )
380+
{
381+
return $Cache->gc();
382+
}
383+
384+
// Try to initialize cache from settings if not already loaded
385+
$Settings = $this->getSettingManager();
386+
387+
if( $Settings )
388+
{
389+
try
390+
{
391+
$Config = \Neuron\Mvc\Cache\CacheConfig::fromSettings( $Settings->getSource() );
392+
393+
if( $Config->isEnabled() )
394+
{
395+
$BasePath = $this->getBasePath();
396+
$CachePath = $BasePath . DIRECTORY_SEPARATOR . $Config->getCachePath();
397+
398+
$Storage = new \Neuron\Mvc\Cache\Storage\FileCacheStorage( $CachePath );
399+
400+
return $Storage->gc();
401+
}
402+
}
403+
catch( \Exception $e )
404+
{
405+
// Unable to initialize cache
406+
}
407+
}
408+
409+
return 0;
410+
}
369411
}
370412

src/Mvc/Cache/CacheConfig.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,41 @@ public static function fromSettings( ISettingSource $Settings ): self
122122
$CacheSettings['views'] = $ViewSettings;
123123
}
124124

125+
// Get GC settings if present
126+
$GcProbability = $Settings->get( 'cache', 'gc_probability' );
127+
if( $GcProbability !== null )
128+
{
129+
$CacheSettings['gc_probability'] = (float) $GcProbability;
130+
}
131+
132+
$GcDivisor = $Settings->get( 'cache', 'gc_divisor' );
133+
if( $GcDivisor !== null )
134+
{
135+
$CacheSettings['gc_divisor'] = (int) $GcDivisor;
136+
}
137+
125138
return new self( $CacheSettings );
126139
}
140+
141+
/**
142+
* Get garbage collection probability
143+
*
144+
* @return float
145+
*/
146+
public function getGcProbability(): float
147+
{
148+
// Default: 1% chance (0.01)
149+
return (float) ( $this->_Settings['gc_probability'] ?? 0.01 );
150+
}
151+
152+
/**
153+
* Get garbage collection divisor
154+
*
155+
* @return int
156+
*/
157+
public function getGcDivisor(): int
158+
{
159+
// Default divisor for probability calculation
160+
return (int) ( $this->_Settings['gc_divisor'] ?? 100 );
161+
}
127162
}

src/Mvc/Cache/Storage/FileCacheStorage.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,25 @@ public function isExpired( string $Key ): bool
173173
return time() > $MetaData['expires'];
174174
}
175175

176+
/**
177+
* Run garbage collection to remove expired cache entries
178+
*
179+
* @return int Number of entries removed
180+
*/
181+
public function gc(): int
182+
{
183+
$Count = 0;
184+
185+
if( !is_dir( $this->_BasePath ) )
186+
{
187+
return 0;
188+
}
189+
190+
$this->scanAndClean( $this->_BasePath, $Count );
191+
192+
return $Count;
193+
}
194+
176195
/**
177196
* Get file path for cache key
178197
*
@@ -267,4 +286,68 @@ private function recursiveDelete( string $Path, bool $DeleteSelf = true ): bool
267286

268287
return true;
269288
}
289+
290+
/**
291+
* Scan directory and clean expired entries
292+
*
293+
* @param string $Dir
294+
* @param int $Count
295+
* @return void
296+
*/
297+
private function scanAndClean( string $Dir, int &$Count ): void
298+
{
299+
$Items = scandir( $Dir );
300+
301+
if( $Items === false )
302+
{
303+
return;
304+
}
305+
306+
foreach( $Items as $Item )
307+
{
308+
if( $Item === '.' || $Item === '..' )
309+
{
310+
continue;
311+
}
312+
313+
$ItemPath = $Dir . DIRECTORY_SEPARATOR . $Item;
314+
315+
if( is_dir( $ItemPath ) )
316+
{
317+
// Recursively scan subdirectories
318+
$this->scanAndClean( $ItemPath, $Count );
319+
}
320+
elseif( substr( $Item, -5 ) === '.meta' )
321+
{
322+
// Check if this meta file indicates an expired entry
323+
$MetaContent = file_get_contents( $ItemPath );
324+
325+
if( $MetaContent !== false )
326+
{
327+
$MetaData = json_decode( $MetaContent, true );
328+
329+
if( $MetaData && isset( $MetaData['expires'] ) && time() > $MetaData['expires'] )
330+
{
331+
// Remove the meta file
332+
@unlink( $ItemPath );
333+
334+
// Remove the corresponding cache file
335+
$CachePath = substr( $ItemPath, 0, -5 ) . '.cache';
336+
if( file_exists( $CachePath ) )
337+
{
338+
@unlink( $CachePath );
339+
}
340+
341+
$Count++;
342+
}
343+
}
344+
}
345+
}
346+
347+
// Try to remove empty subdirectories (but not the base directory)
348+
if( $Dir !== $this->_BasePath )
349+
{
350+
@rmdir( $Dir );
351+
}
352+
}
270353
}

src/Mvc/Cache/ViewCache.php

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,24 @@
77
class ViewCache
88
{
99
private bool $_Enabled;
10-
private string $_CachePath;
1110
private int $_DefaultTtl;
1211
private ICacheStorage $_Storage;
12+
private ?CacheConfig $_Config;
1313

1414
/**
1515
* ViewCache constructor
1616
*
1717
* @param ICacheStorage $Storage
1818
* @param bool $Enabled
1919
* @param int $DefaultTtl
20+
* @param CacheConfig|null $Config
2021
*/
21-
public function __construct( ICacheStorage $Storage, bool $Enabled = true, int $DefaultTtl = 3600 )
22+
public function __construct( ICacheStorage $Storage, bool $Enabled = true, int $DefaultTtl = 3600, ?CacheConfig $Config = null )
2223
{
2324
$this->_Storage = $Storage;
2425
$this->_Enabled = $Enabled;
2526
$this->_DefaultTtl = $DefaultTtl;
27+
$this->_Config = $Config;
2628
}
2729

2830
/**
@@ -59,7 +61,15 @@ public function set( string $Key, string $Content, ?int $Ttl = null ): bool
5961

6062
$Ttl = $Ttl ?? $this->_DefaultTtl;
6163

62-
return $this->_Storage->write( $Key, $Content, $Ttl );
64+
$Result = $this->_Storage->write( $Key, $Content, $Ttl );
65+
66+
// Run garbage collection based on probability
67+
if( $Result && $this->shouldRunGc() )
68+
{
69+
$this->gc();
70+
}
71+
72+
return $Result;
6373
}
6474

6575
/**
@@ -140,6 +150,16 @@ public function setEnabled( bool $Enabled ): void
140150
$this->_Enabled = $Enabled;
141151
}
142152

153+
/**
154+
* Run garbage collection to remove expired cache entries
155+
*
156+
* @return int Number of entries removed
157+
*/
158+
public function gc(): int
159+
{
160+
return $this->_Storage->gc();
161+
}
162+
143163
/**
144164
* Hash data array for cache key
145165
*
@@ -152,4 +172,37 @@ private function hashData( array $Data ): string
152172

153173
return md5( serialize( $Data ) );
154174
}
175+
176+
/**
177+
* Check if garbage collection should run
178+
*
179+
* @return bool
180+
*/
181+
private function shouldRunGc(): bool
182+
{
183+
if( !$this->_Config )
184+
{
185+
// If no config, use default 1% probability
186+
return mt_rand( 1, 100 ) === 1;
187+
}
188+
189+
$Probability = $this->_Config->getGcProbability();
190+
191+
// If probability is 0, GC is disabled
192+
if( $Probability <= 0 )
193+
{
194+
return false;
195+
}
196+
197+
// If probability is 1 or higher, always run
198+
if( $Probability >= 1 )
199+
{
200+
return true;
201+
}
202+
203+
$Divisor = $this->_Config->getGcDivisor();
204+
205+
// Roll the dice
206+
return mt_rand( 1, $Divisor ) <= ( $Probability * $Divisor );
207+
}
155208
}

src/Mvc/Views/CacheableView.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ private function initializeCache(): ?ViewCache
151151
$CachePath = $BasePath . DIRECTORY_SEPARATOR . $Config->getCachePath();
152152

153153
$Storage = new FileCacheStorage( $CachePath );
154-
$Cache = new ViewCache( $Storage, true, $Config->getDefaultTtl() );
154+
$Cache = new ViewCache( $Storage, true, $Config->getDefaultTtl(), $Config );
155155

156156
$Registry->set( 'ViewCache', $Cache );
157157

0 commit comments

Comments
 (0)