@@ -21,8 +21,12 @@ public function __construct(array $options = [])
21
21
parent ::__construct ($ options );
22
22
23
23
$ this ->options = array_merge ([
24
+ 'global ' => option ('bnomei.nitro.global ' ),
25
+ 'atomic ' => option ('bnomei.nitro.atomic ' ),
26
+ 'sleep ' => option ('bnomei.nitro.sleep ' ),
24
27
'auto-clean-cache ' => option ('bnomei.nitro.auto-clean-cache ' ),
25
28
'json-encode-flags ' => option ('bnomei.nitro.json-encode-flags ' ),
29
+ 'cacheDir ' => realpath (__DIR__ .'/../ ' ).'/cache ' , // must be here as well for when used without nitro like as uuid cache
26
30
'max-dirty-cache ' => intval (option ('bnomei.nitro.max-dirty-cache ' )), // @phpstan-ignore-line
27
31
'debug ' => option ('debug ' ),
28
32
], $ options );
@@ -36,11 +40,13 @@ public function __construct(array $options = [])
36
40
if ($ this ->options ['auto-clean-cache ' ]) {
37
41
$ this ->clean ();
38
42
}
43
+
44
+ $ this ->atomic ();
39
45
}
40
46
41
47
public function __destruct ()
42
48
{
43
- $ this ->write ();
49
+ $ this ->write (lock: false );
44
50
}
45
51
46
52
public function key (string |array $ key ): string
@@ -134,13 +140,15 @@ public function remove(string|array $key): bool
134
140
/**
135
141
* {@inheritDoc}
136
142
*/
137
- public function flush (): bool
143
+ public function flush (bool $ write = true ): bool
138
144
{
139
145
if (count ($ this ->data ) === 0 ) {
140
146
$ this ->isDirty ++;
141
147
}
142
148
$ this ->data = [];
143
- $ this ->write ();
149
+ if ($ write ) {
150
+ $ this ->write ();
151
+ }
144
152
145
153
return true ;
146
154
}
@@ -155,19 +163,34 @@ private function clean(): void
155
163
protected function file (?string $ key = null ): string
156
164
{
157
165
/** @var FileCache $cache */
158
- $ cache = kirby ()->cache ('bnomei.nitro.sfc ' );
166
+ if ($ this ->options ['global ' ]) {
167
+ $ cache = $ this ->options ['cacheDir ' ];
168
+ } else {
169
+ $ cache = kirby ()->cache ('bnomei.nitro.sfc ' )->root ();
170
+ }
159
171
160
- return $ cache-> root () .'/single-file-cache.json ' ;
172
+ return $ cache .'/single-file-cache.json ' ;
161
173
}
162
174
163
- public function write (): bool
175
+ public function write (bool $ lock = true ): bool
164
176
{
177
+ $ this ->unlock ();
178
+
165
179
if ($ this ->isDirty === 0 ) {
180
+ if ($ lock ) {
181
+ $ this ->unlock ();
182
+ }
183
+
166
184
return false ;
167
185
}
168
186
$ this ->isDirty = 0 ;
169
187
170
- return F::write ($ this ->file (), json_encode ($ this ->data , $ this ->options ['json-encode-flags ' ]));
188
+ $ success = F::write ($ this ->file (), json_encode ($ this ->data , $ this ->options ['json-encode-flags ' ]));
189
+ if ($ lock ) {
190
+ $ this ->lock ();
191
+ }
192
+
193
+ return $ success ;
171
194
}
172
195
173
196
private static function isCallable (mixed $ value ): bool
@@ -203,4 +226,58 @@ public function count(): int
203
226
{
204
227
return count ($ this ->data );
205
228
}
229
+
230
+ private function isLocked ()
231
+ {
232
+ if (! $ this ->options ['atomic ' ]) {
233
+ return false ;
234
+ }
235
+
236
+ return F::exists ($ this ->file ().'.lock ' );
237
+ }
238
+
239
+ public function lock (): bool
240
+ {
241
+ if (! $ this ->options ['atomic ' ]) {
242
+ return false ;
243
+ }
244
+
245
+ return F::write ($ this ->file ().'.lock ' , date ('c ' ));
246
+ }
247
+
248
+ public function unlock (): bool
249
+ {
250
+ if (! $ this ->options ['atomic ' ]) {
251
+ return false ;
252
+ }
253
+
254
+ return F::remove ($ this ->file ().'.lock ' );
255
+ }
256
+
257
+ private function atomic (): bool
258
+ {
259
+ if (! $ this ->options ['atomic ' ]) {
260
+ return false ;
261
+ }
262
+
263
+ // this is what makes it atomic
264
+ // get php max execution time
265
+ $ maxExecutionTime = (int ) ini_get ('max_execution_time ' );
266
+ if ($ maxExecutionTime === 0 ) {
267
+ $ maxExecutionTime = 30 ; // default, might happen in xdebug mode
268
+ }
269
+ $ maxCycles = $ maxExecutionTime * 1000 * 1000 ; // seconds to microseconds
270
+ $ sleep = $ this ->options ['sleep ' ];
271
+
272
+ while ($ this ->isLocked ()) {
273
+ $ maxCycles -= $ sleep ;
274
+ if ($ maxCycles <= 0 ) {
275
+ throw new \Exception ('Something is very wrong. SingleFileCache could not get lock within ' .$ maxExecutionTime .' seconds! Are using xdebug breakpoints or maybe you need to forcibly `kirby nitro:unlock`? ' );
276
+ }
277
+
278
+ usleep ($ sleep );
279
+ }
280
+
281
+ return $ this ->lock ();
282
+ }
206
283
}
0 commit comments