3
3
import threading
4
4
import logging
5
5
import time
6
- from _error import Timeout
6
+ from . _error import Timeout
7
7
from filelock ._api import AcquireReturnProxy , BaseFileLock
8
8
from typing import Literal , Any
9
9
from contextlib import contextmanager
@@ -62,8 +62,12 @@ def get_lock(cls, lock_file: str | os.PathLike[str],
62
62
normalized = os .path .abspath (lock_file )
63
63
with cls ._instances_lock :
64
64
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
+
67
71
if instance .timeout != timeout or instance .blocking != blocking :
68
72
raise ValueError ("Singleton lock created with timeout=%s, blocking=%s, cannot be changed to timeout=%s, blocking=%s" , instance .timeout , instance .blocking , timeout , blocking )
69
73
return instance
@@ -89,19 +93,6 @@ def __init__(
89
93
# _lock_level is the reentrance counter.
90
94
self ._lock_level = 0
91
95
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;' )
105
96
106
97
def acquire_read (self , timeout : float = - 1 , blocking : bool = True ) -> AcquireReturnProxy :
107
98
"""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
119
110
self ._lock_level += 1
120
111
return AcquireReturnProxy (lock = self )
121
112
122
- timeout_ms = timeout_for_sqlite (timeout , blocking )
123
-
124
113
start_time = time .perf_counter ()
125
114
# Acquire the transaction lock so that the (possibly blocking) SQLite work
126
115
# happens without conflicting with other threads' transaction work.
@@ -140,8 +129,31 @@ def acquire_read(self, timeout: float = -1, blocking: bool = True) -> AcquireRet
140
129
141
130
waited = time .perf_counter () - start_time
142
131
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 )
145
157
self .con .execute ('BEGIN TRANSACTION;' )
146
158
# Need to make SELECT to compel SQLite to actually acquire a SHARED db lock.
147
159
# See https://www.sqlite.org/lockingv3.html#transaction_control
@@ -194,8 +206,17 @@ def acquire_write(self, timeout: float = -1, blocking: bool = True) -> AcquireRe
194
206
195
207
waited = time .perf_counter () - start_time
196
208
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 )
199
220
self .con .execute ('BEGIN EXCLUSIVE TRANSACTION;' )
200
221
201
222
with self ._internal_lock :
@@ -206,7 +227,7 @@ def acquire_write(self, timeout: float = -1, blocking: bool = True) -> AcquireRe
206
227
207
228
except sqlite3 .OperationalError as e :
208
229
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 .
210
231
raise Timeout (self .lock_file )
211
232
finally :
212
233
self ._transaction_lock .release ()
@@ -226,7 +247,7 @@ def release(self, force: bool = False) -> None:
226
247
self ._current_mode = None
227
248
# Unless there are bugs in this code, sqlite3.ProgrammingError
228
249
# must not be raise here, that is, the transaction should have been
229
- # started in acquire ().
250
+ # started in acquire_read() or acquire_write ().
230
251
self .con .rollback ()
231
252
232
253
# ----- Context Manager Protocol -----
0 commit comments