33import threading
44import logging
55import time
6- from _error import Timeout
6+ from . _error import Timeout
77from filelock ._api import AcquireReturnProxy , BaseFileLock
88from typing import Literal , Any
99from contextlib import contextmanager
@@ -62,8 +62,12 @@ def get_lock(cls, lock_file: str | os.PathLike[str],
6262 normalized = os .path .abspath (lock_file )
6363 with cls ._instances_lock :
6464 if normalized not in cls ._instances :
65- cls ._instances [normalized ] = cls (lock_file , timeout , blocking )
66- instance = cls ._instances [normalized ]
65+ # Create the instance with a strong reference first
66+ instance = super (_ReadWriteLockMeta , cls ).__call__ (lock_file , timeout , blocking , is_singleton = False )
67+ cls ._instances [normalized ] = instance
68+ else :
69+ instance = cls ._instances [normalized ]
70+
6771 if instance .timeout != timeout or instance .blocking != blocking :
6872 raise ValueError ("Singleton lock created with timeout=%s, blocking=%s, cannot be changed to timeout=%s, blocking=%s" , instance .timeout , instance .blocking , timeout , blocking )
6973 return instance
@@ -89,19 +93,6 @@ def __init__(
8993 # _lock_level is the reentrance counter.
9094 self ._lock_level = 0
9195 self .con = sqlite3 .connect (self .lock_file , check_same_thread = False )
92- # Using the legacy journal mode rather than more modern WAL mode because,
93- # apparently, in WAL mode it's impossible to enforce that read transactions
94- # (started with BEGIN TRANSACTION) are blocked if a concurrent write transaction,
95- # even EXCLUSIVE, is in progress, unless the read transactions actually read
96- # any pages modified by the write transaction. But in the legacy journal mode,
97- # it seems, it's possible to do this read-write locking without table data
98- # modification at each exclusive lock.
99- # See https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
100- # "MEMORY" journal mode is fine because no actual writes to the are happening in write-lock
101- # acquire, so crashes cannot adversely affect the DB. Even journal_mode=OFF would probably
102- # be fine, too, but the SQLite documentation says that ROLLBACK becomes *undefined behaviour*
103- # with journal_mode=OFF which sounds scarier.
104- self .con .execute ('PRAGMA journal_mode=MEMORY;' )
10596
10697 def acquire_read (self , timeout : float = - 1 , blocking : bool = True ) -> AcquireReturnProxy :
10798 """Acquire a read lock. If a lock is already held, it must be a read lock.
@@ -119,8 +110,6 @@ def acquire_read(self, timeout: float = -1, blocking: bool = True) -> AcquireRet
119110 self ._lock_level += 1
120111 return AcquireReturnProxy (lock = self )
121112
122- timeout_ms = timeout_for_sqlite (timeout , blocking )
123-
124113 start_time = time .perf_counter ()
125114 # Acquire the transaction lock so that the (possibly blocking) SQLite work
126115 # happens without conflicting with other threads' transaction work.
@@ -140,8 +129,31 @@ def acquire_read(self, timeout: float = -1, blocking: bool = True) -> AcquireRet
140129
141130 waited = time .perf_counter () - start_time
142131 timeout_ms = timeout_for_sqlite (timeout , blocking , waited )
143-
144- self .con .execute ('PRAGMA busy_timeout=?;' , (timeout_ms ,))
132+ self .con .execute ('PRAGMA busy_timeout=%d;' % timeout_ms )
133+ # WHY journal_mode=MEMORY?
134+ # Using the legacy journal mode rather than more modern WAL mode because,
135+ # apparently, in WAL mode it's impossible to enforce that read transactions
136+ # (started with BEGIN TRANSACTION) are blocked if a concurrent write transaction,
137+ # even EXCLUSIVE, is in progress, unless the read transactions actually read
138+ # any pages modified by the write transaction. But in the legacy journal mode,
139+ # it seems, it's possible to do this read-write locking without table data
140+ # modification at each exclusive lock.
141+ # See https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
142+ # "MEMORY" journal mode is fine because no actual writes to the are happening in write-lock
143+ # acquire, so crashes cannot adversely affect the DB. Even journal_mode=OFF would probably
144+ # be fine, too, but the SQLite documentation says that ROLLBACK becomes *undefined behaviour*
145+ # with journal_mode=OFF which sounds scarier.
146+ #
147+ # WHY SETTING THIS PRAGMA HERE RATHER THAN IN ReadWriteLock.__init__()?
148+ # Because setting this pragma may block on the database if it is locked at the moment,
149+ # so we must set this pragma *after* `PRAGMA busy_timeout` above.
150+ self .con .execute ('PRAGMA journal_mode=MEMORY;' )
151+ # Recompute the remaining timeout after the potentially blocking pragma
152+ # statement above.
153+ waited = time .perf_counter () - start_time
154+ timeout_ms_2 = timeout_for_sqlite (timeout , blocking , waited )
155+ if timeout_ms_2 != timeout_ms :
156+ self .con .execute ('PRAGMA busy_timeout=%d;' % timeout_ms_2 )
145157 self .con .execute ('BEGIN TRANSACTION;' )
146158 # Need to make SELECT to compel SQLite to actually acquire a SHARED db lock.
147159 # See https://www.sqlite.org/lockingv3.html#transaction_control
@@ -194,8 +206,17 @@ def acquire_write(self, timeout: float = -1, blocking: bool = True) -> AcquireRe
194206
195207 waited = time .perf_counter () - start_time
196208 timeout_ms = timeout_for_sqlite (timeout , blocking , waited )
197-
198- self .con .execute ('PRAGMA busy_timeout=?;' , (timeout_ms ,))
209+ self .con .execute ('PRAGMA busy_timeout=%d;' % timeout_ms )
210+ # For explanations for both why we use journal_mode=MEMORY and why we set
211+ # this pragma here rather than in ReadWriteLock.__init__(), see the comments
212+ # in acquire_read().
213+ self .con .execute ('PRAGMA journal_mode=MEMORY;' )
214+ # Recompute the remaining timeout after the potentially blocking pragma
215+ # statement above.
216+ waited = time .perf_counter () - start_time
217+ timeout_ms_2 = timeout_for_sqlite (timeout , blocking , waited )
218+ if timeout_ms_2 != timeout_ms :
219+ self .con .execute ('PRAGMA busy_timeout=%d;' % timeout_ms_2 )
199220 self .con .execute ('BEGIN EXCLUSIVE TRANSACTION;' )
200221
201222 with self ._internal_lock :
@@ -206,7 +227,7 @@ def acquire_write(self, timeout: float = -1, blocking: bool = True) -> AcquireRe
206227
207228 except sqlite3 .OperationalError as e :
208229 if 'database is locked' not in str (e ):
209- raise # Re-raise if it is an unexpected error .
230+ raise e # Re-raise unexpected errors .
210231 raise Timeout (self .lock_file )
211232 finally :
212233 self ._transaction_lock .release ()
@@ -226,7 +247,7 @@ def release(self, force: bool = False) -> None:
226247 self ._current_mode = None
227248 # Unless there are bugs in this code, sqlite3.ProgrammingError
228249 # must not be raise here, that is, the transaction should have been
229- # started in acquire ().
250+ # started in acquire_read() or acquire_write ().
230251 self .con .rollback ()
231252
232253 # ----- Context Manager Protocol -----
0 commit comments