Skip to content

Commit 2429a8a

Browse files
docs: document connection-scoped config architecture in thread-safe mode spec
Adds Architecture section covering the object graph, config flow for both singleton and Instance paths, and a table of all connection-scoped config reads across 9 modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 092d79f commit 2429a8a

File tree

1 file changed

+94
-13
lines changed

1 file changed

+94
-13
lines changed

docs/design/thread-safe-mode.md

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,89 @@ Mouse().insert1({"mouse_id": 1})
163163
Mouse().fetch()
164164
```
165165

166+
## Architecture
167+
168+
### Object graph
169+
170+
There is exactly **one** global `Config` object created at import time in `settings.py`. Both the legacy API and the `Instance` API hang off `Connection` objects, each of which carries a `_config` reference.
171+
172+
```
173+
settings.py
174+
config = _create_config() ← THE single global Config
175+
176+
instance.py
177+
_global_config = settings.config ← same object (not a copy)
178+
_singleton_connection = None ← lazily created Connection
179+
180+
__init__.py
181+
dj.config = _ConfigProxy() ← proxy → _global_config (with thread-safety check)
182+
dj.conn() ← returns _singleton_connection
183+
dj.Schema() ← uses _singleton_connection
184+
dj.FreeTable() ← uses _singleton_connection
185+
186+
Connection (singleton)
187+
_config → _global_config ← same Config that dj.config writes to
188+
189+
Connection (Instance)
190+
_config → fresh Config ← isolated per-instance
191+
```
192+
193+
### Config flow: singleton path
194+
195+
```
196+
dj.config["safemode"] = False
197+
↓ _ConfigProxy.__setitem__
198+
_global_config["safemode"] = False (same object as settings.config)
199+
200+
Connection._config["safemode"] (points to _global_config)
201+
202+
schema.drop() reads self.connection._config["safemode"] → False ✓
203+
```
204+
205+
### Config flow: Instance path
206+
207+
```
208+
inst = dj.Instance(host=..., user=..., password=...)
209+
210+
inst.config = _create_config() (fresh Config, independent)
211+
inst.connection._config = inst.config
212+
213+
inst.config["safemode"] = False
214+
215+
schema.drop() reads self.connection._config["safemode"] → False ✓
216+
```
217+
218+
### Key invariant
219+
220+
**All runtime config reads go through `self.connection._config`**, never through the global `config` directly. This ensures both the singleton and Instance paths read the correct config.
221+
222+
### Connection-scoped config reads
223+
224+
Every module that previously imported `from .settings import config` now reads config from the connection:
225+
226+
| Module | What was read | How it's read now |
227+
|--------|--------------|-------------------|
228+
| `schemas.py` | `config["safemode"]`, `config.database.create_tables` | `self.connection._config[...]` |
229+
| `table.py` | `config["safemode"]` in `delete()`, `drop()` | `self.connection._config["safemode"]` |
230+
| `expression.py` | `config["loglevel"]` in `__repr__()` | `self.connection._config["loglevel"]` |
231+
| `preview.py` | `config["display.*"]` (8 reads) | `query_expression.connection._config[...]` |
232+
| `autopopulate.py` | `config.jobs.allow_new_pk_fields`, `auto_refresh` | `self.connection._config.jobs.*` |
233+
| `jobs.py` | `config.jobs.default_priority`, `stale_timeout`, `keep_completed` | `self.connection._config.jobs.*` |
234+
| `declare.py` | `config.jobs.add_job_metadata` | `config` param (threaded from `table.py`) |
235+
| `diagram.py` | `config.display.diagram_direction` | `self._connection._config.display.*` |
236+
| `staged_insert.py` | `config.get_store_spec()` | `self._table.connection._config.get_store_spec()` |
237+
238+
### Functions that receive config as a parameter
239+
240+
Some module-level functions cannot access `self.connection`. Config is threaded through:
241+
242+
| Function | Caller | How config arrives |
243+
|----------|--------|--------------------|
244+
| `declare()` in `declare.py` | `Table.declare()` in `table.py` | `config=self.connection._config` kwarg |
245+
| `_get_job_version()` in `jobs.py` | `AutoPopulate._make_tuples()`, `Job.reserve()` | `config=self.connection._config` positional arg |
246+
247+
Both functions accept `config=None` and fall back to the global `settings.config` for backward compatibility.
248+
166249
## Implementation
167250

168251
### 1. Create Instance class
@@ -185,8 +268,11 @@ class Instance:
185268
### 2. Global config and singleton connection
186269

187270
```python
188-
# Module level
189-
_global_config = _create_config() # Created at import time
271+
# settings.py - THE single global config
272+
config = _create_config() # Created at import time
273+
274+
# instance.py - reuses the same config object
275+
_global_config = settings.config # Same reference, not a copy
190276
_singleton_connection = None # Created lazily
191277

192278
def _check_thread_safe():
@@ -224,8 +310,12 @@ class _ConfigProxy:
224310

225311
config = _ConfigProxy()
226312

227-
# dj.conn() -> singleton connection
228-
def conn():
313+
# dj.conn() -> singleton connection (persistent across calls)
314+
def conn(host=None, user=None, password=None, *, reset=False):
315+
_check_thread_safe()
316+
if reset or (_singleton_connection is None and credentials_provided):
317+
_singleton_connection = Connection(...)
318+
_singleton_connection._config = _global_config
229319
return _get_singleton_connection()
230320

231321
# dj.Schema() -> uses singleton connection
@@ -238,21 +328,12 @@ def Schema(name, connection=None, **kwargs):
238328
# dj.FreeTable() -> uses singleton connection
239329
def FreeTable(conn_or_name, full_table_name=None):
240330
if full_table_name is None:
241-
# Called as FreeTable("db.table")
242331
_check_thread_safe()
243332
return _FreeTable(_get_singleton_connection(), conn_or_name)
244333
else:
245-
# Called as FreeTable(conn, "db.table")
246334
return _FreeTable(conn_or_name, full_table_name)
247335
```
248336

249-
### 4. Refactor internal code
250-
251-
All internal code uses `self.connection._config` instead of global `config`:
252-
- Connection stores reference to its config as `self._config`
253-
- Tables access config via `self.connection._config`
254-
- This works uniformly for both singleton and isolated instances
255-
256337
## Global State Audit
257338

258339
All module-level mutable state was reviewed for thread-safety implications.

0 commit comments

Comments
 (0)