Closes #22872: Validate custom script Meta values before enqueueing - #23068
Conversation
A custom script could declare an invalid job_timeout or notifications_default in its Meta class. Both values were passed through to the job unvalidated: an invalid notifications_default reached Job.full_clean() and an invalid job_timeout reached RQ, each raising an unhandled exception that surfaced as an HTTP 500. Validation now happens once at the ScriptJob.enqueue choke point, via a new BaseScript.validate_meta() classmethod. Because every way of running a script funnels through ScriptJob.enqueue (interactive runs, the REST API, the runscript command, event-rule actions, and recurring reschedules), a misconfigured script is caught before any job is created and each caller surfaces the error in its own idiom: the UI re-renders the form with a message, the REST API returns a 400, the runscript command raises a CommandError, an event-rule action is logged and skipped so it cannot abort the triggering object change, and a recurring reschedule is logged against the completed job without altering its outcome. Unset values keep their existing valid defaults and are not rejected, so scripts that run today are unaffected.
|
@claude /review Only report on issues of potential concern; we don't need commentary on anything that doesn't require further consideration. |
|
Claude finished @jnovinger's task in 6m 57s —— View job Review: #23068 — Validate custom script
|
Validate the execution parameters actually being enqueued rather than only the script's Meta defaults, so an explicit job_timeout or notifications supplied by a caller (for example via the REST API) is checked and a valid explicit value is no longer rejected because of an unused Meta default. Make the instance argument to ScriptJob.enqueue keyword-only so the override preserves JobRunner.enqueue's signature and cannot collide with a positional argument in enqueue_once(). Return the REST API validation error under the non-field detail key instead of a bare list, and chain the original exception. Add a runscript management command test covering the invalid-Meta path. Drop the release-note entry, which does not belong in a bug-fix PR.
Fixed in 4330376.
Fixed in 4330376:
Keeping log-and-continue here to match the existing pipeline:
Added a test in 4330376.
Removed in 4330376. Whoops, let Claude get that one by me.
Fixed in 4330376: raised under the
|
The new event-rule test triggered on Site, whose object type is already covered by the class-level event rules in setUpTestData. Under the parallel test runner the extra Site update enqueued a job into the shared RQ queue that another test counted, making test_single_update_process_eventrule intermittently see two queued jobs instead of one. Trigger the test on Manufacturer instead, which no class-level rule targets, and assert the queue stays empty so the test cannot leak a job to a sibling.
pheus
left a comment
There was a problem hiding this comment.
Thanks!
I found one remaining compatibility issue that I think needs to be addressed before merge: positional instance calls are still broken. I’ve left an inline comment with the details and a suggested regression test.
The keyword-only instance parameter broke positional calls: JobRunner.enqueue forwards the first positional argument to Job.enqueue as instance, so ScriptJob.enqueue(script) sent instance both positionally and as instance=None, raising a TypeError. Keep the inherited (*args, **kwargs) signature and resolve the instance for validation without consuming it, forwarding the original arguments to super() unchanged. Add regression tests covering both the positional and keyword forms.
|
This looks like another shared-RQ-queue race rather than a rollback failure. The two successful Could we keep those callbacks unexecuted and assert that they were registered instead? That should retain the forwarding coverage without allowing these tests to affect another worker's queue. |
Test classes using RQQueueTestMixin share one Redis instance. The parallel test runner isolates the database per worker but not Redis, so classes that enqueue jobs and assert exact queue counts race each other across workers: one worker's enqueues and flushall() perturb another worker's count. This surfaced as intermittent AssertionError on queue.count in EventRuleTestCase, with the failing test varying by environment. Mix SerializeMixin into RQQueueTestMixin so classes sharing the queue hold an exclusive lock and never run concurrently. This is Django's documented mechanism for test classes that share a single external resource under the parallel runner.
ScriptJobEnqueueValidationTestCase is a plain TestCase with no RQQueueTestMixin, so it never clears the RQ queue. Two of its tests executed the on_commit callback that pushes a real ScriptJob into the shared Redis queue, and under the parallel runner that leaked job was read by a concurrent EventRuleTestCase.test_send_webhook (send_webhook(**job.kwargs) raised TypeError on the ScriptJob's job= kwarg). These tests assert Job-row creation, which Job.enqueue() does via save() before registering the RQ push, so they don't need the push to fire. Drop execute=True from captureOnCommitCallbacks so the callback is captured but never run: the Job row is still created and asserted, and nothing enters the shared queue. End-to-end enqueue-to-RQ coverage remains in EventRuleTestCase, which clears the queue.
This (plus the test serialization of tests in that class) was the answer. Thanks! |
pheus
left a comment
There was a problem hiding this comment.
Thanks for the update! This looks good to me!
Closes: #22872
A custom script could declare an invalid
job_timeoutornotifications_defaultin itsMetaclass. Both values reached the job unvalidated: a badnotifications_defaultfailedJob.full_clean()and a badjob_timeoutfailed in RQ, each raising an unhandled exception that surfaced as an HTTP 500.Validation now happens once, at the
ScriptJob.enqueuechoke point, through a newBaseScript.validate_meta()classmethod. Every way of running a script funnels throughScriptJob.enqueue(interactive runs, the REST API, therunscriptcommand, event-rule actions, and recurring reschedules), so a misconfigured script is caught before any job is created and each caller reports the error in its own idiom:runscriptcommand: aCommandErrorinstead of a tracebackUnset values keep their existing valid defaults, so scripts that run today are unaffected. Non-positive
job_timeoutvalues are also rejected: they parse cleanly but are nonsensical as a timeout.