Skip to content

Commit 0ca032b

Browse files
snopokeclaude
andcommitted
feat: pre-generate task IDs in the procrastinate integration
Generate the task ID with generate_task_id() before creating the pending task, so the id is known independently of the create response. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e62ec3a commit 0ca032b

3 files changed

Lines changed: 31 additions & 22 deletions

File tree

taskbadger/procrastinate.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .internal.models import StatusEnum
2222
from .mug import Badger
2323
from .safe_sdk import create_task_safe, update_task_safe
24-
from .sdk import DefaultMergeStrategy
24+
from .sdk import DefaultMergeStrategy, generate_task_id
2525

2626
log = logging.getLogger("taskbadger")
2727

@@ -164,14 +164,15 @@ async def defer_async(**kwargs):
164164
task.defer_async = defer_async
165165

166166

167-
def _create_pending_task(task, task_kwargs, queue=None):
167+
def _create_pending_task(task, task_kwargs, queue=None, task_id=None):
168168
"""Create a PENDING TaskBadger task for ``task`` if it should be tracked.
169169
170170
Returns the created TaskBadger task, or ``None`` if Badger isn't
171171
configured, the task isn't tracked (neither manual nor auto), or the
172172
create call failed. ``task_kwargs`` is used only for the
173173
``record_task_args`` data capture. ``queue`` overrides the queue name
174174
recorded on the TaskBadger task (defaults to the task's own queue).
175+
``task_id`` assigns the task's id (see ``_maybe_create_pending``).
175176
"""
176177
if not Badger.is_configured():
177178
return None
@@ -203,18 +204,22 @@ def _create_pending_task(task, task_kwargs, queue=None):
203204
if data:
204205
create_kwargs["data"] = data
205206

206-
return create_task_safe(name, **create_kwargs)
207+
return create_task_safe(name, task_id=task_id, **create_kwargs)
207208

208209

209210
def _maybe_create_pending(task, kwargs):
210211
"""Decide whether to track this defer, and if so create the TaskBadger
211-
task and inject its id into ``kwargs``. Always returns the kwargs dict."""
212-
tb_task = _create_pending_task(task, kwargs)
212+
task and inject its id into ``kwargs``. Always returns the kwargs dict.
213+
214+
The id is generated up front so it's known before the task exists, letting us
215+
inject it without depending on the create response."""
216+
tb_id = generate_task_id()
217+
tb_task = _create_pending_task(task, kwargs, task_id=tb_id)
213218
if tb_task is None:
214219
return kwargs
215220

216221
new_kwargs = dict(kwargs)
217-
new_kwargs[TB_TASK_ID_KWARG] = tb_task.id
222+
new_kwargs[TB_TASK_ID_KWARG] = tb_id
218223
return new_kwargs
219224

220225

@@ -314,11 +319,12 @@ async def patched(*, job, periodic_id, defer_timestamp):
314319
task = app.tasks.get(job.task_name)
315320
tb_id = None
316321
if task is not None:
317-
tb_task = _create_pending_task(task, job.task_kwargs, queue=job.queue)
322+
candidate_id = generate_task_id()
323+
tb_task = _create_pending_task(task, job.task_kwargs, queue=job.queue, task_id=candidate_id)
318324
if tb_task is not None:
319-
new_kwargs = {**job.task_kwargs, TB_TASK_ID_KWARG: tb_task.id}
325+
new_kwargs = {**job.task_kwargs, TB_TASK_ID_KWARG: candidate_id}
320326
job = job.evolve(task_kwargs=new_kwargs)
321-
tb_id = tb_task.id
327+
tb_id = candidate_id
322328
job_id = await jm._taskbadger_original_defer_periodic_job(
323329
job=job, periodic_id=periodic_id, defer_timestamp=defer_timestamp
324330
)

tests/test_procrastinate.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,13 @@ def add3(a, b):
123123

124124
create.assert_called_once()
125125
assert create.call_args.args == ("add3",)
126-
assert create.call_args.kwargs == {"status": StatusEnum.PENDING, "queue": "default"}
126+
task_id = create.call_args.kwargs["task_id"]
127+
assert create.call_args.kwargs == {"status": StatusEnum.PENDING, "queue": "default", "task_id": task_id}
127128

128-
# The injected id should appear in the Procrastinate job's task kwargs.
129+
# The generated id is passed to create and injected into the Procrastinate job's task kwargs.
129130
jobs = list(app.connector.jobs.values())
130131
assert len(jobs) == 1
131-
assert jobs[0]["args"][TB_TASK_ID_KWARG] == tb.id
132+
assert jobs[0]["args"][TB_TASK_ID_KWARG] == task_id
132133

133134

134135
@pytest.mark.usefixtures("_bind_settings")
@@ -175,13 +176,13 @@ async def add5(a, b):
175176

176177
tb = task_for_test()
177178
with (
178-
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb),
179+
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb) as create,
179180
mock.patch("taskbadger.procrastinate.update_task_safe"),
180181
):
181182
asyncio.run(add5.defer_async(a=1, b=2))
182183

183184
jobs = list(app.connector.jobs.values())
184-
assert jobs[0]["args"][TB_TASK_ID_KWARG] == tb.id
185+
assert jobs[0]["args"][TB_TASK_ID_KWARG] == create.call_args.kwargs["task_id"]
185186

186187

187188
@pytest.mark.usefixtures("_bind_settings")
@@ -194,12 +195,12 @@ def add_ext(a, b):
194195

195196
tb = task_for_test()
196197
with (
197-
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb),
198+
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb) as create,
198199
mock.patch("taskbadger.procrastinate.update_task_safe") as update,
199200
):
200201
job_id = add_ext.defer(a=1, b=2)
201202

202-
update.assert_called_once_with(tb.id, external_id=str(job_id))
203+
update.assert_called_once_with(create.call_args.kwargs["task_id"], external_id=str(job_id))
203204

204205

205206
@pytest.mark.usefixtures("_bind_settings")
@@ -212,12 +213,12 @@ async def add_ext_async(a, b):
212213

213214
tb = task_for_test()
214215
with (
215-
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb),
216+
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb) as create,
216217
mock.patch("taskbadger.procrastinate.update_task_safe") as update,
217218
):
218219
job_id = asyncio.run(add_ext_async.defer_async(a=1, b=2))
219220

220-
update.assert_called_once_with(tb.id, external_id=str(job_id))
221+
update.assert_called_once_with(create.call_args.kwargs["task_id"], external_id=str(job_id))
221222

222223

223224
def test_defer_no_external_id_when_untracked(app):
@@ -269,15 +270,15 @@ def bare(a):
269270

270271
tb = task_for_test()
271272
with (
272-
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb),
273+
mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb) as create,
273274
mock.patch("taskbadger.procrastinate.update_task_safe"),
274275
):
275276
bare.defer(a=1)
276277

277278
assert getattr(bare, "_taskbadger_manual") is True
278279
# Inspect the actual Procrastinate job - jobs is a dict keyed by int, kwargs under "args"
279280
jobs = list(app.connector.jobs.values())
280-
assert jobs[0]["args"][TB_TASK_ID_KWARG] == tb.id
281+
assert jobs[0]["args"][TB_TASK_ID_KWARG] == create.call_args.kwargs["task_id"]
281282

282283

283284
@pytest.mark.usefixtures("_bind_settings")
@@ -296,12 +297,14 @@ def raw(a):
296297

297298
create.assert_called_once()
298299
assert create.call_args.args == ("custom",)
300+
task_id = create.call_args.kwargs["task_id"]
299301
assert create.call_args.kwargs == {
300302
"status": StatusEnum.PENDING,
301303
"value_max": 10,
302304
"tags": {"env": "test"},
303305
"data": {"k": "v"},
304306
"queue": "default",
307+
"task_id": task_id,
305308
}
306309

307310

tests/test_procrastinate_system_integration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def auto_target(a):
6565
create.assert_called_once()
6666
# InMemoryConnector.jobs is a dict keyed by int; kwargs under "args"
6767
jobs = list(app.connector.jobs.values())
68-
assert jobs[0]["args"][TB_TASK_ID_KWARG] == tb.id
68+
assert jobs[0]["args"][TB_TASK_ID_KWARG] == create.call_args.kwargs["task_id"]
6969

7070

7171
@pytest.mark.usefixtures("_bind_settings")
@@ -121,7 +121,7 @@ def periodic_target(timestamp):
121121

122122
create.assert_called_once()
123123
jobs_stored = list(app.connector.jobs.values())
124-
assert jobs_stored[0]["args"][TB_TASK_ID_KWARG] == tb.id
124+
assert jobs_stored[0]["args"][TB_TASK_ID_KWARG] == create.call_args.kwargs["task_id"]
125125

126126

127127
@pytest.mark.usefixtures("_bind_settings")

0 commit comments

Comments
 (0)