1+ import atexit
12import json as json_mod
23import os
34import shutil
3031 TRACKIO_DIR ,
3132 deserialize_values ,
3233 get_color_palette ,
34+ on_spaces ,
3335 serialize_values ,
3436)
3537
@@ -44,24 +46,92 @@ def _configure_sqlite_pragmas(conn: sqlite3.Connection) -> None:
4446 override = os .environ .get ("TRACKIO_SQLITE_JOURNAL_MODE" , "" ).strip ().lower ()
4547 if override in _JOURNAL_MODE_WHITELIST :
4648 journal = override .upper ()
47- elif os . environ . get ( "SYSTEM" ) == "spaces" :
49+ elif on_spaces () :
4850 journal = "DELETE"
4951 else :
5052 journal = "WAL"
5153 conn .execute (f"PRAGMA journal_mode = { journal } " )
5254 conn .execute ("PRAGMA synchronous = NORMAL" )
5355 conn .execute ("PRAGMA temp_store = MEMORY" )
5456 conn .execute ("PRAGMA cache_size = -20000" )
57+ if on_spaces ():
58+ conn .execute ("PRAGMA locking_mode = EXCLUSIVE" )
59+
60+
61+ _persistent_connections : dict [str , sqlite3 .Connection ] = {}
62+ _persistent_lock = Lock ()
63+ _db_access_locks : dict [str , Lock ] = {}
64+
65+
66+ def _get_db_access_lock (db_path : Path ) -> Lock :
67+ key = str (db_path )
68+ with _persistent_lock :
69+ if key not in _db_access_locks :
70+ _db_access_locks [key ] = Lock ()
71+ return _db_access_locks [key ]
72+
73+
74+ def _get_or_create_persistent_conn (
75+ db_path : Path , timeout : float = 30.0
76+ ) -> sqlite3 .Connection :
77+ key = str (db_path )
78+ with _persistent_lock :
79+ conn = _persistent_connections .get (key )
80+ if conn is not None :
81+ try :
82+ conn .execute ("SELECT 1" )
83+ return conn
84+ except sqlite3 .Error :
85+ try :
86+ conn .close ()
87+ except sqlite3 .Error :
88+ pass
89+ _persistent_connections .pop (key , None )
90+ conn = sqlite3 .connect (str (db_path ), timeout = timeout , check_same_thread = False )
91+ _configure_sqlite_pragmas (conn )
92+ conn .execute ("SELECT 1" )
93+ _persistent_connections [key ] = conn
94+ return conn
95+
96+
97+ def _close_all_persistent_connections () -> None :
98+ with _persistent_lock :
99+ for conn in _persistent_connections .values ():
100+ try :
101+ conn .close ()
102+ except sqlite3 .Error :
103+ pass
104+ _persistent_connections .clear ()
105+
106+
107+ atexit .register (_close_all_persistent_connections )
55108
56109
57110class ProcessLock :
58- """A file-based lock that works across processes using fcntl (Unix) or msvcrt (Windows)."""
111+ """Lock used to coordinate database access.
112+
113+ Normally uses file-based locking for cross-process coordination. When running
114+ on a bucket-mounted filesystem where file locks are unreliable,
115+ falls back to an in-memory threading Lock (single-process only)."""
116+
117+ _thread_locks : dict [str , Lock ] = {}
118+ _meta_lock = Lock ()
59119
60120 def __init__ (self , lockfile_path : Path ):
61121 self .lockfile_path = lockfile_path
62122 self .lockfile = None
123+ self ._use_thread_lock = on_spaces ()
124+ if self ._use_thread_lock :
125+ key = str (lockfile_path )
126+ with ProcessLock ._meta_lock :
127+ if key not in ProcessLock ._thread_locks :
128+ ProcessLock ._thread_locks [key ] = Lock ()
129+ self ._thread_lock = ProcessLock ._thread_locks [key ]
63130
64131 def __enter__ (self ):
132+ if self ._use_thread_lock :
133+ self ._thread_lock .acquire ()
134+ return self
65135 if fcntl is None and _msvcrt is None :
66136 return self
67137 self .lockfile_path .parent .mkdir (parents = True , exist_ok = True )
@@ -82,6 +152,9 @@ def __enter__(self):
82152 raise IOError ("Could not acquire database lock after 10 seconds" )
83153
84154 def __exit__ (self , exc_type , exc_val , exc_tb ):
155+ if self ._use_thread_lock :
156+ self ._thread_lock .release ()
157+ return
85158 if self .lockfile :
86159 try :
87160 if fcntl is not None :
@@ -107,16 +180,31 @@ def _get_connection(
107180 configure_pragmas : bool = True ,
108181 row_factory = sqlite3 .Row ,
109182 ) -> Iterator [sqlite3 .Connection ]:
110- conn = sqlite3 .connect (str (db_path ), timeout = timeout )
111- try :
112- if configure_pragmas :
113- _configure_sqlite_pragmas (conn )
114- if row_factory is not None :
183+ if on_spaces ():
184+ # On Spaces, all callers share a single persistent connection
185+ # that is pragma-configured at creation time. The `configure_pragmas`
186+ # flag is intentionally ignored here — the pragmas (journal mode,
187+ # synchronous, locking mode) don't affect query semantics.
188+ access_lock = _get_db_access_lock (db_path )
189+ access_lock .acquire ()
190+ try :
191+ conn = _get_or_create_persistent_conn (db_path , timeout = timeout )
115192 conn .row_factory = row_factory
116- with conn :
117- yield conn
118- finally :
119- conn .close ()
193+ with conn :
194+ yield conn
195+ finally :
196+ access_lock .release ()
197+ else :
198+ conn = sqlite3 .connect (str (db_path ), timeout = timeout )
199+ try :
200+ if configure_pragmas :
201+ _configure_sqlite_pragmas (conn )
202+ if row_factory is not None :
203+ conn .row_factory = row_factory
204+ with conn :
205+ yield conn
206+ finally :
207+ conn .close ()
120208
121209 @staticmethod
122210 def _get_process_lock (project : str ) -> ProcessLock :
0 commit comments