Skip to content

Commit f832486

Browse files
docs: Singleton as lazily-loaded instance, fresh config per instance
- dj.config, dj.conn(), dj.Schema() delegate to singleton instance - Singleton lazily loaded on first access - thread_safe checked at module import, blocks singleton access - inst.config created fresh (not copied from dj.config) - dj.instance() always works, creates isolated instance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f92af1c commit f832486

File tree

1 file changed

+95
-71
lines changed

1 file changed

+95
-71
lines changed

docs/design/thread-safe-mode.md

Lines changed: 95 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ DataJoint uses global state (`dj.config`, `dj.conn()`) that is not thread-safe.
66

77
## Solution
88

9-
Introduce **context** objects that encapsulate config and connection. The `dj` module provides the singleton (legacy) context. New isolated contexts are created with `dj.new()`.
9+
Introduce **instance** objects that encapsulate config and connection. The `dj` module provides access to a lazily-loaded singleton instance. New isolated instances are created with `dj.instance()`.
1010

1111
## API
1212

13-
### Legacy API (singleton context)
13+
### Legacy API (singleton instance)
1414

1515
```python
1616
import datajoint as dj
@@ -24,36 +24,38 @@ class Mouse(dj.Manual):
2424
definition = "..."
2525
```
2626

27-
### New API (isolated context)
27+
Internally, `dj.config`, `dj.conn()`, and `dj.Schema()` delegate to a lazily-loaded singleton instance.
28+
29+
### New API (isolated instance)
2830

2931
```python
3032
import datajoint as dj
3133

32-
ctx = dj.new(
34+
inst = dj.instance(
3335
host="localhost",
3436
user="user",
3537
password="password",
3638
)
37-
ctx.config.safemode = False
38-
schema = ctx.Schema("my_schema")
39+
inst.config.safemode = False
40+
schema = inst.Schema("my_schema")
3941

4042
@schema
4143
class Mouse(dj.Manual):
4244
definition = "..."
4345
```
4446

45-
### Context structure
47+
### Instance structure
4648

47-
Each context exposes only:
48-
- `ctx.config` - Config instance (copy of `dj.config` at creation)
49-
- `ctx.connection` - Connection (created at context construction)
50-
- `ctx.Schema()` - Schema factory using context's connection
49+
Each instance has:
50+
- `inst.config` - Config (created fresh at instance creation)
51+
- `inst.connection` - Connection (created at instance creation)
52+
- `inst.Schema()` - Schema factory using instance's connection
5153

5254
```python
53-
ctx = dj.new(host="localhost", user="u", password="p")
54-
ctx.config # Config instance
55-
ctx.connection # Connection instance
56-
ctx.Schema("name") # Creates schema using ctx.connection
55+
inst = dj.instance(host="localhost", user="u", password="p")
56+
inst.config # Config instance
57+
inst.connection # Connection instance
58+
inst.Schema("name") # Creates schema using inst.connection
5759
```
5860

5961
### Thread-safe mode
@@ -62,122 +64,144 @@ ctx.Schema("name") # Creates schema using ctx.connection
6264
export DJ_THREAD_SAFE=true
6365
```
6466

65-
When `thread_safe=True`:
67+
`thread_safe` is read from environment/config file at module import time.
68+
69+
When `thread_safe=True`, accessing the singleton raises `ThreadSafetyError`:
70+
- `dj.config` raises `ThreadSafetyError`
6671
- `dj.conn()` raises `ThreadSafetyError`
6772
- `dj.Schema()` raises `ThreadSafetyError`
68-
- `dj.config` only allows access to `thread_safe` (all other access raises `ThreadSafetyError`)
69-
- `dj.new()` works - isolated contexts are always allowed
73+
- `dj.instance()` works - isolated instances are always allowed
7074

7175
```python
7276
# thread_safe=True
7377

74-
dj.config.thread_safe # OK - allowed
75-
dj.config.safemode # ThreadSafetyError
76-
dj.config.safemode = False # ThreadSafetyError
77-
dj.conn() # ThreadSafetyError
78-
dj.Schema("name") # ThreadSafetyError
78+
dj.config # ThreadSafetyError
79+
dj.conn() # ThreadSafetyError
80+
dj.Schema("name") # ThreadSafetyError
7981

80-
ctx = dj.new(host="h", user="u", password="p") # OK
81-
ctx.config.safemode = False # OK
82-
ctx.Schema("name") # OK
82+
inst = dj.instance(host="h", user="u", password="p") # OK
83+
inst.config.safemode = False # OK
84+
inst.Schema("name") # OK
8385
```
8486

8587
## Behavior Summary
8688

8789
| Operation | `thread_safe=False` | `thread_safe=True` |
8890
|-----------|--------------------|--------------------|
89-
| `dj.config.thread_safe` | Works | Works |
90-
| `dj.config.*` (other) | Works | `ThreadSafetyError` |
91-
| `dj.conn()` | Works | `ThreadSafetyError` |
92-
| `dj.Schema()` | Works | `ThreadSafetyError` |
93-
| `dj.new()` | Works | Works |
94-
| `ctx.config.*` | Works | Works |
95-
| `ctx.connection` | Works | Works |
96-
| `ctx.Schema()` | Works | Works |
91+
| `dj.config` | Singleton config | `ThreadSafetyError` |
92+
| `dj.conn()` | Singleton connection | `ThreadSafetyError` |
93+
| `dj.Schema()` | Uses singleton | `ThreadSafetyError` |
94+
| `dj.instance()` | Works | Works |
95+
| `inst.config` | Works | Works |
96+
| `inst.connection` | Works | Works |
97+
| `inst.Schema()` | Works | Works |
98+
99+
## Singleton Lazy Loading
100+
101+
The singleton instance is created lazily on first access to `dj.config`, `dj.conn()`, or `dj.Schema()`:
102+
103+
```python
104+
# First access triggers singleton creation
105+
dj.config.safemode # Creates singleton, returns singleton.config.safemode
106+
dj.conn() # Returns singleton.connection (connects if needed)
107+
dj.Schema("name") # Returns singleton.Schema("name")
108+
```
97109

98110
## Usage Example
99111

100112
```python
101113
import datajoint as dj
102114

103-
# Create isolated context
104-
ctx = dj.new(
115+
# Create isolated instance
116+
inst = dj.instance(
105117
host="localhost",
106118
user="user",
107119
password="password",
108120
)
109121

110122
# Configure
111-
ctx.config.safemode = False
112-
ctx.config.stores = {"raw": {"protocol": "file", "location": "/data"}}
123+
inst.config.safemode = False
124+
inst.config.stores = {"raw": {"protocol": "file", "location": "/data"}}
113125

114126
# Create schema
115-
schema = ctx.Schema("my_schema")
127+
schema = inst.Schema("my_schema")
116128

117129
@schema
118130
class Mouse(dj.Manual):
119131
definition = """
120132
mouse_id: int
121133
"""
122134

123-
@schema
124-
class Session(dj.Manual):
125-
definition = """
126-
-> Mouse
127-
session_date: date
128-
"""
129-
130135
# Use tables
131136
Mouse().insert1({"mouse_id": 1})
132-
Mouse().delete() # Uses ctx.config.safemode
137+
Mouse().delete() # Uses inst.config.safemode
133138
```
134139

135140
## Implementation
136141

137-
### 1. Create Context class
142+
### 1. Create Instance class
138143

139144
```python
140-
class Context:
141-
def __init__(self, host, user, password, port=3306, ...):
142-
self.config = copy(dj.config) # Independent config copy
145+
class Instance:
146+
def __init__(self, host, user, password, port=3306, **kwargs):
147+
self.config = Config() # Fresh config with defaults
148+
# Apply any config overrides from kwargs
143149
self.connection = Connection(host, user, password, port, ...)
144-
self.connection._config = self.config # Link config to connection
150+
self.connection._config = self.config
145151

146152
def Schema(self, name, **kwargs):
147153
return Schema(name, connection=self.connection, **kwargs)
148154
```
149155

150-
### 2. Add dj.new()
156+
### 2. Add dj.instance()
151157

152158
```python
153-
def new(host, user, password, **kwargs) -> Context:
154-
"""Create a new isolated context with its own config and connection."""
155-
return Context(host, user, password, **kwargs)
159+
def instance(host, user, password, **kwargs) -> Instance:
160+
"""Create a new isolated instance with its own config and connection."""
161+
return Instance(host, user, password, **kwargs)
156162
```
157163

158-
### 3. Add thread_safe guards
159-
160-
In `dj.config`:
161-
- Allow read/write of `thread_safe` always
162-
- When `thread_safe=True`, block all other attribute access
164+
### 3. Singleton with lazy loading
163165

164166
```python
165-
def __getattr__(self, name):
166-
if name == "thread_safe":
167-
return self._thread_safe
168-
if self._thread_safe:
169-
raise ThreadSafetyError("Global config is inaccessible in thread-safe mode.")
170-
# ... normal access
167+
# Module level
168+
_thread_safe = _load_thread_safe_from_env_or_config()
169+
_singleton = None
170+
171+
def _get_singleton():
172+
if _thread_safe:
173+
raise ThreadSafetyError(
174+
"Global DataJoint state is disabled in thread-safe mode. "
175+
"Use dj.instance() to create an isolated instance."
176+
)
177+
global _singleton
178+
if _singleton is None:
179+
_singleton = Instance(
180+
host=_load_from_config("database.host"),
181+
user=_load_from_config("database.user"),
182+
password=_load_from_config("database.password"),
183+
...
184+
)
185+
return _singleton
186+
187+
# Public API
188+
@property
189+
def config():
190+
return _get_singleton().config
191+
192+
def conn():
193+
return _get_singleton().connection
194+
195+
def Schema(name, **kwargs):
196+
return _get_singleton().Schema(name, **kwargs)
171197
```
172198

173199
### 4. Refactor internal code
174200

175201
All internal code uses `self.connection._config` instead of global `config`:
176202
- Tables access config via `self.connection._config`
177-
- This works uniformly for both singleton and isolated contexts
203+
- This works uniformly for both singleton and isolated instances
178204

179205
## Error Messages
180206

181-
- `dj.config.*`: `"Global config is inaccessible in thread-safe mode. Use ctx = dj.new(...) for isolated config."`
182-
- `dj.conn()`: `"dj.conn() is disabled in thread-safe mode. Use ctx = dj.new(...) to create an isolated context."`
183-
- `dj.Schema()`: `"dj.Schema() is disabled in thread-safe mode. Use ctx = dj.new(...) to create an isolated context."`
207+
- Singleton access: `"Global DataJoint state is disabled in thread-safe mode. Use dj.instance() to create an isolated instance."`

0 commit comments

Comments
 (0)