Populate and Long-Running Computations¶
Why¶
DataJoint wraps each populate call in a database transaction. This protects
data integrity, but it also holds a table lock for the entire duration of the
computation. For analyses that take seconds, this is fine. For analyses that
take minutes or hours (spike sorting, LFP filtering, decoding), the open
transaction blocks other users from modifying related tables — even for
unrelated sessions.
The old workaround was to set _use_transaction = False on the table class.
This bypassed the transaction wrapper entirely, which removed the lock but also
removed the data-integrity guarantees. It is now deprecated.
What: tri-part make¶
The replacement is the tri-part make pattern: split the monolithic make
method into three methods with explicit responsibilities.
| Method | Responsibility | DB access |
|---|---|---|
make_fetch(key) |
Read inputs from upstream tables | Read only |
make_compute(key, ...) |
Run the computation | None |
make_insert(key, ...) |
Write results to the database | Write only |
Spyglass's populate calls these three methods in sequence. make_fetch and
make_compute run outside the transaction; make_insert runs inside one. The
long computation no longer holds a lock, but the database write is still atomic.
How¶
import datajoint as dj
from spyglass.utils import SpyglassMixin
schema = dj.schema("my_schema")
@schema
class MyHeavyTable(SpyglassMixin, dj.Computed):
definition = """
-> UpstreamTable
---
result: float
"""
_parallel_make = True # enables tri-part populate
def make_fetch(self, key):
"""Read inputs. No database writes allowed here."""
data = (UpstreamTable & key).fetch1("raw_data")
params = (ParameterTable & key).fetch1("params")
return [data, params]
def make_compute(self, key, data, params):
"""Run the computation. No database access allowed here."""
result = heavy_analysis(data, params) # can take minutes
return [{"result": result}]
def make_insert(self, key, insert_dict):
"""Write results. Runs inside a transaction."""
self.insert1(dict(key, **insert_dict))
Rules:
make_fetchmust only read — no inserts, updates, or deletes.make_fetchmust be deterministic: the same key always returns the same data.make_computemust not access the database at all.make_insertis the only method that writes to the database.- Each method returns a list; the next method receives those values as
positional arguments after
key.
For a detailed walkthrough with a real Spyglass table, see Custom Pipelines — Make Method.
Migration from _use_transaction = False¶
If your table currently sets _use_transaction = False, Spyglass emits a
deprecation warning once per table per Python process and falls back to the old
no-transaction behavior. Migrate by removing the attribute and splitting your
make into three methods.
Before¶
@schema
class MyHeavyTable(SpyglassMixin, dj.Computed):
definition = """
-> UpstreamTable
---
result: float
"""
_use_transaction = False # deprecated — remove this
def make(self, key):
# step 1: fetch
data = (UpstreamTable & key).fetch1("raw_data")
params = (ParameterTable & key).fetch1("params")
# step 2: compute (long-running, holds no lock under old pattern)
result = heavy_analysis(data, params)
# step 3: insert
self.insert1(dict(key, result=result))
After¶
@schema
class MyHeavyTable(SpyglassMixin, dj.Computed):
definition = """
-> UpstreamTable
---
result: float
"""
_parallel_make = True # replaces _use_transaction = False
def make_fetch(self, key):
data = (UpstreamTable & key).fetch1("raw_data")
params = (ParameterTable & key).fetch1("params")
return [data, params]
def make_compute(self, key, data, params):
result = heavy_analysis(data, params)
return [{"result": result}]
def make_insert(self, key, insert_dict):
self.insert1(dict(key, **insert_dict))
NOTE: The deprecation warning is triggered by the _use_transaction = False
class attribute — not by calling populate with use_transaction=False as a
keyword argument. The keyword argument is still a supported way to override
transaction behavior at call time. The warning is logged once per table per
Python process; the logged identifier in ActivityLog takes the form
no_transact:<full_table_name> (truncated to 64 characters).