@@ -21,6 +21,10 @@ class DatabaseEvictStrategy extends AbstractEvictStrategy
2121 protected DatabaseStore $ cacheStore ;
2222
2323 protected int $ deletedRecords = 0 ;
24+ /**
25+ * @var int
26+ * @deprecated Because this tool does not actually free table spaces, there is no need to track the amount of space freed from eviction.
27+ */
2428 protected int $ deletedRecordSizes = 0 ;
2529
2630 protected float $ elapsedTime = 0 ;
@@ -46,7 +50,6 @@ public function execute(): void
4650 {
4751 // read the cache config and set up targets
4852 $ this ->deletedRecords = 0 ;
49- $ this ->deletedRecordSizes = 0 ;
5053 $ this ->elapsedTime = 0 ;
5154
5255 // we use a memory-efficient way of deleting items.
@@ -62,32 +65,34 @@ public function execute(): void
6265 Partyline::info ("Found $ itemCount records; processing... " );
6366
6467 // create a progress bar to display our progress
65- /** @var ProgressBar $progressBar */
66- $ progressBar = $ this ->output ->createProgressBar ();
67- $ progressBar ->setMaxSteps ($ itemCount );
68- foreach ($ this ->yieldCacheTableItems () as $ cacheItem ) {
69- // read record details
70- $ currentActualKey = $ cacheItem ->key ;
71- $ currentExpiration = $ cacheItem ->expiration ;
72- // currently timestamps are 32-bit, so are 4 bytes
73- $ estimatedBytes = (int ) ($ cacheItem ->key_bytes + $ cacheItem ->value_bytes + 4 );
74- $ progressBar ->advance ();
75-
76- if (time () < $ currentExpiration ) {
77- // not expired yet
78- continue ;
68+ $ progressBar = $ this ->output ->createProgressBar ($ itemCount );
69+ foreach ($ this ->yieldCacheTableChunks () as $ chunk ) {
70+ $ progressBar ->advance (count ($ chunk ));
71+
72+ // identify the keys of the records that are potentially expired
73+ $ currentTimestamp = time ();
74+ $ possibleExpiredKeys = [];
75+ foreach ($ chunk as $ cacheItem ) {
76+ $ currentActualKey = $ cacheItem ->key ;
77+ $ currentExpiration = $ cacheItem ->expiration ;
78+ if ($ currentTimestamp < $ currentExpiration ) {
79+ // not expired yet
80+ continue ;
81+ }
82+ // item expired; put to the deletion queue
83+ $ possibleExpiredKeys [] = $ currentActualKey ;
7984 }
80- // item expired; try to issue a delete command to it
85+
86+ // try to issue a delete command to the database
8187 // this respects any potential new value written to the db while we were checking other things
8288 $ rowsAffected = $ this ->dbConn
8389 ->table ($ this ->dbTable )
84- ->where ('key ' , ' = ' , $ currentActualKey )
85- ->where ('expiration ' , '= ' , $ currentExpiration )
90+ ->whereIn ('key ' , $ possibleExpiredKeys )
91+ ->where ('expiration ' , '< = ' , $ currentTimestamp )
8692 ->delete ();
8793 if ($ rowsAffected ) {
88- // item really expired with no new updates
89- $ this ->deletedRecords += 1 ;
90- $ this ->deletedRecordSizes += $ estimatedBytes ;
94+ // items really expired with no new updates
95+ $ this ->deletedRecords += $ rowsAffected ;
9196 }
9297 }
9398
@@ -98,47 +103,65 @@ public function execute(): void
98103 // all is done; print some stats
99104 $ endUnix = microtime (true );
100105 $ this ->elapsedTime = $ endUnix - $ startUnix ;
101- // generate a human readable file size
102- $ readableFileSize = $ this ->bytesToHuman ($ this ->deletedRecordSizes );
106+ // note: the database evictor does not help reclaim free table spaces, so no need to print file size information
103107 Partyline::info ("Took {$ this ->elapsedTime } seconds. " );
104- Partyline::info ("Removed {$ this ->deletedRecords } expired cache records. Estimated total size: $ readableFileSize " );
108+ Partyline::info ("Removed {$ this ->deletedRecords } expired cache records. " );
105109 Partyline::info ("Note: no free space reclaimed; reclaiming free space should be done manually! " );
106110 }
107111
108112 /**
109113 * Yields the next item from the cache table that belongs to this cache.
110114 *
111115 * This method will return the actual key (with the cache prefix if exists) of the entry.
116+ * @deprecated This method is deprecated in favor of chunked deletion. Currently, it fetches and yields nothing.
112117 * @return \Generator<mixed, object, mixed, void>
113118 */
114119 protected function yieldCacheTableItems (): \Generator
120+ {
121+ yield new \stdClass ();
122+ }
123+
124+ /**
125+ * Yields the next chunk of many items from the cache table that belongs to this cache.
126+ *
127+ * This method will return the actual keys (with the cache prefix if exists) of the entries.
128+ * @return \Generator<mixed, array, mixed, void>
129+ */
130+ protected function yieldCacheTableChunks (): \Generator
115131 {
116132 // there might be a prefix for the cache store!
117133 $ cachePrefix = $ this ->cachePrefix ;
118134 // initialize the key to be just the cache prefix as the "zero string".
119135 $ currentActualKey = $ cachePrefix ;
120136 $ prefixLength = strlen ($ cachePrefix );
137+ // the cache table uses (MySQL) utf8mb4 collation (4 bytes) for its key column with max length 256
138+ // we estimate this should result in max allocation of about $chunkCount * 4 * 256 bytes throughout the eviction
139+ // remember to avoid excessive chunk sizes so that full-table locking is less likely to occur
140+ $ chunkCount = 100 ;
121141 // loop until no more items
122142 while (true ) {
123143 // find the next key
124144 // note: different SQL flavors have different interpretations of LIKE, so we use SUBSTRING instead.
125145 // with SUBSTRING, we are clear we want a case-sensitive match, and we might potentially get collation-correct matching
126- $ record = $ this ->dbConn
146+ $ recordsList = $ this ->dbConn
127147 ->table ($ this ->dbTable )
128- ->select (['key ' , 'expiration ' , DB :: raw ( ' LENGTH(`key`) AS key_bytes ' ), DB :: raw ( ' LENGTH(value) AS value_bytes ' ) ])
148+ ->select (['key ' , 'expiration ' ])
129149 ->where ('key ' , '> ' , $ currentActualKey )
130150 ->where (DB ::raw ("SUBSTRING(`key`, 1, $ prefixLength) " ), '= ' , $ cachePrefix )
131151 // PostgreSQL: if no sorting specified, then will ignore primary key index/ordering, which breaks the intended workflow
132152 ->orderBy ('key ' )
133- ->limit (1 )
134- ->first ();
135- if (! $ record ) {
153+ ->limit ($ chunkCount )
154+ ->get ();
155+ if ($ recordsList -> isEmpty () ) {
136156 // nothing more to get
137157 break ;
138158 }
139159
140- yield $ record ;
141- $ currentActualKey = $ record ->key ;
160+ $ theChunk = $ recordsList ->all ();
161+ yield $ theChunk ;
162+ // find the last element, and set it to be the current key to continue table-walking
163+ $ lastItem = end ($ theChunk );
164+ $ currentActualKey = $ lastItem ->key ;
142165 }
143166 // loop exit handled inside while loop
144167 }
0 commit comments