6
6
import tempfile
7
7
import os
8
8
import shutil
9
+ import threading
9
10
10
- from qiniu .compat import json , b as to_bytes
11
+ from qiniu .compat import json , b as to_bytes , is_windows , is_linux , is_macos
11
12
from qiniu .utils import io_md5 , dt2ts
12
13
13
14
from .endpoint import Endpoint
@@ -24,7 +25,7 @@ def __iter__(self):
24
25
"""
25
26
Returns
26
27
-------
27
- list [Region]
28
+ Generator [Region, None, None ]
28
29
"""
29
30
30
31
@@ -137,27 +138,112 @@ def __init__(self, message):
137
138
super (FileAlreadyLocked , self ).__init__ (message )
138
139
139
140
140
- class _FileLocker :
141
- def __init__ (self , origin_file ):
142
- self ._origin_file = origin_file
141
+ _file_threading_lockers_lock = threading .Lock ()
142
+ _file_threading_lockers = {}
143
+
144
+
145
+ class _FileThreadingLocker :
146
+ def __init__ (self , fd ):
147
+ self ._fd = fd
143
148
144
149
def __enter__ (self ):
145
- if os .access (self .lock_file_path , os .R_OK | os .W_OK ):
146
- raise FileAlreadyLocked ('File {0} already locked' .format (self ._origin_file ))
147
- with open (self .lock_file_path , 'w' ):
148
- pass
150
+ with _file_threading_lockers_lock :
151
+ global _file_threading_lockers
152
+ threading_lock = _file_threading_lockers .get (self ._file_path , threading .Lock ())
153
+ # Could use keyword style `acquire(blocking=False)` when min version of python update to >= 3
154
+ if not threading_lock .acquire (False ):
155
+ raise FileAlreadyLocked ('File {0} already locked' .format (self ._file_path ))
156
+ _file_threading_lockers [self ._file_path ] = threading_lock
149
157
150
158
def __exit__ (self , exc_type , exc_val , exc_tb ):
151
- os .remove (self .lock_file_path )
159
+ with _file_threading_lockers_lock :
160
+ global _file_threading_lockers
161
+ threading_lock = _file_threading_lockers .get (self ._file_path )
162
+ if threading_lock and threading_lock .locked ():
163
+ threading_lock .release ()
164
+ del _file_threading_lockers [self ._file_path ]
152
165
153
166
@property
154
- def lock_file_path (self ):
155
- """
156
- Returns
157
- -------
158
- str
159
- """
160
- return self ._origin_file + '.lock'
167
+ def _file_path (self ):
168
+ return self ._fd .name
169
+
170
+
171
+ if is_linux or is_macos :
172
+ import fcntl
173
+
174
+ # Use subclass of _FileThreadingLocker when min version of python update to >= 3
175
+ class _FileLocker :
176
+ def __init__ (self , fd ):
177
+ self ._fd = fd
178
+
179
+ def __enter__ (self ):
180
+ try :
181
+ fcntl .lockf (self ._fd , fcntl .LOCK_EX | fcntl .LOCK_NB )
182
+ except IOError :
183
+ # Use `raise ... from ...` when min version of python update to >= 3
184
+ raise FileAlreadyLocked ('File {0} already locked' .format (self ._file_path ))
185
+
186
+ def __exit__ (self , exc_type , exc_val , exc_tb ):
187
+ fcntl .lockf (self ._fd , fcntl .LOCK_UN )
188
+
189
+ @property
190
+ def _file_path (self ):
191
+ return self ._fd .name
192
+
193
+ elif is_windows :
194
+ import msvcrt
195
+
196
+
197
+ class _FileLocker :
198
+ def __init__ (self , fd ):
199
+ self ._fd = fd
200
+
201
+ def __enter__ (self ):
202
+ try :
203
+ # TODO(lihs): set `_nbyte` bigger?
204
+ msvcrt .locking (self ._fd , msvcrt .LK_LOCK | msvcrt .LK_NBLCK , 1 )
205
+ except OSError :
206
+ raise FileAlreadyLocked ('File {0} already locked' .format (self ._file_path ))
207
+
208
+ def __exit__ (self , exc_type , exc_val , exc_tb ):
209
+ msvcrt .locking (self ._fd , msvcrt .LK_UNLCK , 1 )
210
+
211
+ @property
212
+ def _file_path (self ):
213
+ return self ._fd .name
214
+
215
+ else :
216
+ class _FileLocker :
217
+ def __init__ (self , fd ):
218
+ self ._fd = fd
219
+
220
+ def __enter__ (self ):
221
+ try :
222
+ # Atomic file creation
223
+ open_flags = os .O_EXCL | os .O_RDWR | os .O_CREAT
224
+ fd = os .open (self .lock_file_path , open_flags )
225
+ os .close (fd )
226
+ except FileExistsError :
227
+ raise FileAlreadyLocked ('File {0} already locked' .format (self ._file_path ))
228
+
229
+ def __exit__ (self , exc_type , exc_val , exc_tb ):
230
+ try :
231
+ os .remove (self .lock_file_path )
232
+ except FileNotFoundError :
233
+ pass
234
+
235
+ @property
236
+ def _file_path (self ):
237
+ return self ._fd .name
238
+
239
+ @property
240
+ def lock_file_path (self ):
241
+ """
242
+ Returns
243
+ -------
244
+ str
245
+ """
246
+ return self ._file_path + '.lock'
161
247
162
248
163
249
# use dataclass instead namedtuple if min version of python update to 3.7
@@ -168,7 +254,8 @@ def lock_file_path(self):
168
254
'persist_path' ,
169
255
'last_shrink_at' ,
170
256
'shrink_interval' ,
171
- 'should_shrink_expired_regions'
257
+ 'should_shrink_expired_regions' ,
258
+ 'memo_cache_lock'
172
259
]
173
260
)
174
261
@@ -177,11 +264,12 @@ def lock_file_path(self):
177
264
memo_cache = {},
178
265
persist_path = os .path .join (
179
266
tempfile .gettempdir (),
180
- 'qn-regions-cache.jsonl'
267
+ 'qn-py-sdk- regions-cache.jsonl'
181
268
),
182
269
last_shrink_at = datetime .datetime .fromtimestamp (0 ),
183
- shrink_interval = datetime .timedelta (- 1 ), # useless for now
184
- should_shrink_expired_regions = False
270
+ shrink_interval = datetime .timedelta (days = 1 ),
271
+ should_shrink_expired_regions = False ,
272
+ memo_cache_lock = threading .Lock ()
185
273
)
186
274
187
275
@@ -323,7 +411,7 @@ def _parse_persisted_regions(persisted_data):
323
411
return parsed_data .get ('cacheKey' ), regions
324
412
325
413
326
- def _walk_persist_cache_file (persist_path , ignore_parse_error = False ):
414
+ def _walk_persist_cache_file (persist_path , ignore_parse_error = True ):
327
415
"""
328
416
Parameters
329
417
----------
@@ -394,23 +482,24 @@ def __init__(
394
482
self .base_regions_provider = base_regions_provider
395
483
396
484
persist_path = kwargs .get ('persist_path' , None )
485
+ last_shrink_at = datetime .datetime .fromtimestamp (0 )
397
486
if persist_path is None :
398
487
persist_path = _global_cache_scope .persist_path
488
+ last_shrink_at = _global_cache_scope .last_shrink_at
399
489
400
490
shrink_interval = kwargs .get ('shrink_interval' , None )
401
491
if shrink_interval is None :
402
- shrink_interval = datetime . timedelta ( days = 1 )
492
+ shrink_interval = _global_cache_scope . shrink_interval
403
493
404
494
should_shrink_expired_regions = kwargs .get ('should_shrink_expired_regions' , None )
405
495
if should_shrink_expired_regions is None :
406
- should_shrink_expired_regions = False
496
+ should_shrink_expired_regions = _global_cache_scope . should_shrink_expired_regions
407
497
408
- self ._cache_scope = CacheScope (
409
- memo_cache = _global_cache_scope .memo_cache ,
498
+ self ._cache_scope = _global_cache_scope ._replace (
410
499
persist_path = persist_path ,
411
- last_shrink_at = datetime . datetime . fromtimestamp ( 0 ) ,
500
+ last_shrink_at = last_shrink_at ,
412
501
shrink_interval = shrink_interval ,
413
- should_shrink_expired_regions = should_shrink_expired_regions ,
502
+ should_shrink_expired_regions = should_shrink_expired_regions
414
503
)
415
504
416
505
def __iter__ (self ):
@@ -423,7 +512,7 @@ def __iter__(self):
423
512
self .__get_regions_from_base_provider
424
513
]
425
514
426
- regions = None
515
+ regions = []
427
516
for get_regions in get_regions_fns :
428
517
regions = get_regions (fallback = regions )
429
518
if regions and all (r .is_live for r in regions ):
@@ -439,7 +528,8 @@ def set_regions(self, regions):
439
528
----------
440
529
regions: list[Region]
441
530
"""
442
- self ._cache_scope .memo_cache [self .cache_key ] = regions
531
+ with self ._cache_scope .memo_cache_lock :
532
+ self ._cache_scope .memo_cache [self .cache_key ] = regions
443
533
444
534
if not self ._cache_scope .persist_path :
445
535
return
@@ -469,8 +559,11 @@ def persist_path(self, value):
469
559
----------
470
560
value: str
471
561
"""
562
+ if value == self ._cache_scope .persist_path :
563
+ return
472
564
self ._cache_scope = self ._cache_scope ._replace (
473
- persist_path = value
565
+ persist_path = value ,
566
+ last_shrink_at = datetime .datetime .fromtimestamp (0 )
474
567
)
475
568
476
569
@property
@@ -586,7 +679,6 @@ def __get_regions_from_base_provider(self, fallback=None):
586
679
def __flush_file_cache_to_memo (self ):
587
680
for cache_key , regions in _walk_persist_cache_file (
588
681
persist_path = self ._cache_scope .persist_path
589
- # ignore_parse_error=True
590
682
):
591
683
if cache_key not in self ._cache_scope .memo_cache :
592
684
self ._cache_scope .memo_cache [cache_key ] = regions
@@ -609,12 +701,18 @@ def __should_shrink(self):
609
701
def __shrink_cache (self ):
610
702
# shrink memory cache
611
703
if self ._cache_scope .should_shrink_expired_regions :
612
- kept_memo_cache = {}
613
- for k , regions in self ._cache_scope .memo_cache .items ():
614
- live_regions = [r for r in regions if r .is_live ]
615
- if live_regions :
616
- kept_memo_cache [k ] = live_regions
617
- self ._cache_scope = self ._cache_scope ._replace (memo_cache = kept_memo_cache )
704
+ memo_cache_old = self ._cache_scope .memo_cache .copy ()
705
+ # Could use keyword style `acquire(blocking=False)` when min version of python update to >= 3
706
+ if self ._cache_scope .memo_cache_lock .acquire (False ):
707
+ try :
708
+ for k , regions in memo_cache_old .items ():
709
+ live_regions = [r for r in regions if r .is_live ]
710
+ if live_regions :
711
+ self ._cache_scope .memo_cache [k ] = live_regions
712
+ else :
713
+ del self ._cache_scope .memo_cache [k ]
714
+ finally :
715
+ self ._cache_scope .memo_cache_lock .release ()
618
716
619
717
# shrink file cache
620
718
if not self ._cache_scope .persist_path :
@@ -625,7 +723,7 @@ def __shrink_cache(self):
625
723
626
724
shrink_file_path = self ._cache_scope .persist_path + '.shrink'
627
725
try :
628
- with _FileLocker (shrink_file_path ):
726
+ with open (shrink_file_path , 'a' ) as f , _FileThreadingLocker ( f ), _FileLocker ( f ):
629
727
# filter data
630
728
shrunk_cache = {}
631
729
for cache_key , regions in _walk_persist_cache_file (
@@ -646,25 +744,36 @@ def __shrink_cache(self):
646
744
)
647
745
648
746
# write data
649
- with open (shrink_file_path , 'a' ) as f :
650
- for cache_key , regions in shrunk_cache .items ():
651
- f .write (
652
- json .dumps (
653
- {
654
- 'cacheKey' : cache_key ,
655
- 'regions' : [_persist_region (r ) for r in regions ]
656
- }
657
- ) + os .linesep
658
- )
747
+ for cache_key , regions in shrunk_cache .items ():
748
+ f .write (
749
+ json .dumps (
750
+ {
751
+ 'cacheKey' : cache_key ,
752
+ 'regions' : [_persist_region (r ) for r in regions ]
753
+ }
754
+ ) + os .linesep
755
+ )
756
+
757
+ # make the cache file available for all users
758
+ if is_linux or is_macos :
759
+ os .chmod (shrink_file_path , 0o666 )
659
760
660
761
# rename file
661
762
shutil .move (shrink_file_path , self ._cache_scope .persist_path )
763
+
764
+ # update last shrink time
765
+ self ._cache_scope = self ._cache_scope ._replace (
766
+ last_shrink_at = datetime .datetime .now ()
767
+ )
768
+ global _global_cache_scope
769
+ if _global_cache_scope .persist_path == self ._cache_scope .persist_path :
770
+ _global_cache_scope = _global_cache_scope ._replace (
771
+ last_shrink_at = self ._cache_scope .last_shrink_at
772
+ )
773
+
662
774
except FileAlreadyLocked :
775
+ # skip file shrink by another running
663
776
pass
664
- finally :
665
- self ._cache_scope = self ._cache_scope ._replace (
666
- last_shrink_at = datetime .datetime .now ()
667
- )
668
777
669
778
670
779
def get_default_regions_provider (
0 commit comments