Skip to content

Commit 821253d

Browse files
Merge pull request #1614 from datajoint/fix/jobs-completed-time-precision-2.3
fix(jobs): record completed_time at millisecond precision (2.3)
2 parents 0c83448 + 15c61b2 commit 821253d

2 files changed

Lines changed: 83 additions & 4 deletions

File tree

‎src/datajoint/jobs.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,8 @@ def complete(self, key: dict, duration: float | None = None) -> None:
510510
- If False: deletes the job entry
511511
"""
512512
if self.connection._config.jobs.keep_completed:
513-
# Use server time for completed_time
514-
server_now = self.connection.query("SELECT CURRENT_TIMESTAMP").fetchone()[0]
513+
# Server time at the column's datetime(3) precision, as reserve() writes reserved_time
514+
server_now = self.connection.query("SELECT CURRENT_TIMESTAMP(3)").fetchone()[0]
515515
pk = self._get_pk(key)
516516
update_row = {
517517
**pk,
@@ -540,8 +540,8 @@ def error(self, key: dict, error_message: str, error_stack: str | None = None) -
540540
if len(error_message) > ERROR_MESSAGE_LENGTH:
541541
error_message = error_message[: ERROR_MESSAGE_LENGTH - len(TRUNCATION_APPENDIX)] + TRUNCATION_APPENDIX
542542

543-
# Use server time for completed_time
544-
server_now = self.connection.query("SELECT CURRENT_TIMESTAMP").fetchone()[0]
543+
# Server time at the column's datetime(3) precision, as reserve() writes reserved_time
544+
server_now = self.connection.query("SELECT CURRENT_TIMESTAMP(3)").fetchone()[0]
545545

546546
pk = self._get_pk(key)
547547
update_row = {

‎tests/integration/test_jobs.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Tests for per-table Job management (AutoPopulate 2.0)."""
22

3+
import datetime
34
import random
45
import string
6+
import time
7+
8+
import pytest
59

610
import datajoint as dj
711
from datajoint.jobs import ERROR_MESSAGE_LENGTH, TRUNCATION_APPENDIX
@@ -230,3 +234,78 @@ def test_jobs_table_uuid_fk_derived_pk(schema_uuid):
230234
pk = BasicComputed.jobs.heading.primary_key
231235
assert "item" in pk
232236
assert BasicComputed.jobs.heading["item"].original_type == "uuid"
237+
238+
239+
# Each make() below sleeps this long between reserve() and complete()/error().
240+
MAKE_SECONDS = 0.15
241+
242+
243+
@pytest.fixture(scope="function")
244+
def schema_by_backend(connection_by_backend, db_creds_by_backend):
245+
"""Create a fresh schema per test, parameterized across backends."""
246+
backend = db_creds_by_backend["backend"]
247+
test_id = str(int(time.time() * 1000))[-8:]
248+
schema = dj.Schema(f"djtest_jobs_{backend}_{test_id}"[:64], connection=connection_by_backend)
249+
yield schema
250+
schema.drop(prompt=False)
251+
252+
253+
def test_completed_time_precision(schema_by_backend):
254+
"""completed_time keeps datetime(3) precision on success, so it never precedes reserved_time (#1600)."""
255+
256+
@schema_by_backend
257+
class Item(dj.Manual):
258+
definition = """
259+
item : int32
260+
"""
261+
262+
@schema_by_backend
263+
class Result(dj.Computed):
264+
definition = """
265+
-> Item
266+
---
267+
value : int32
268+
"""
269+
270+
def make(self, key):
271+
time.sleep(MAKE_SECONDS)
272+
self.insert1(dict(key, value=key["item"]))
273+
274+
Item.insert({"item": i} for i in range(3))
275+
with dj.config.override(jobs__keep_completed=True):
276+
Result.populate(reserve_jobs=True)
277+
jobs = Result.jobs.completed.to_dicts()
278+
assert len(jobs) == 3
279+
for job in jobs:
280+
# Truncated to the second, completed_time falls before reserved_time or short of make()'s sleep
281+
assert job["completed_time"] - job["reserved_time"] >= datetime.timedelta(seconds=0.1), job
282+
283+
284+
def test_error_time_precision(schema_by_backend):
285+
"""completed_time keeps datetime(3) precision on error, so it never precedes reserved_time (#1600)."""
286+
287+
@schema_by_backend
288+
class Item(dj.Manual):
289+
definition = """
290+
item : int32
291+
"""
292+
293+
@schema_by_backend
294+
class Result(dj.Computed):
295+
definition = """
296+
-> Item
297+
---
298+
value : int32
299+
"""
300+
301+
def make(self, key):
302+
time.sleep(MAKE_SECONDS)
303+
raise RuntimeError("not ready")
304+
305+
Item.insert({"item": i} for i in range(3))
306+
Result.populate(reserve_jobs=True, suppress_errors=True)
307+
jobs = Result.jobs.errors.to_dicts()
308+
assert len(jobs) == 3
309+
for job in jobs:
310+
# Error records carry no duration, so this difference is the only record of how long make() ran
311+
assert job["completed_time"] - job["reserved_time"] >= datetime.timedelta(seconds=0.1), job

0 commit comments

Comments
 (0)