Problem/motivation
When processing items in a queue, sometimes it is useful to delay further processing of an item due to uncontrollable external circumstances. Unfortunately, this isn't possible with the current queue worker processing via cron since RequeueException immediately re-queues an item (which causes an infinite loop depending on queue implementation) and all other exceptions trigger a log message to be stored via watchdog.
Proposed resolution
I propose introducing a new exception, DelayedRequeueException to behave similarly to RequeueException except that the item being processed is not released via QueueInterface::releaseItem(). This would allow for an in-built back-off delay in the further processing of queue items without requiring a custom queue implementation.
Backward compatibility
No backward-incompatible changes should be required as the only change to Drupal will be to add a new exception. If a queue runner is compatible with Cron::processQueue, then this will cause no problems as the only change between throwing \Exception and the new exception is the lack of logging. For the same reason, no new functionality needs to be added to existing queue backends.
| Comment | File | Size | Author |
|---|---|---|---|
| #78 | 3116478-78--9.0.x.patch | 23.47 KB | br0ken |
| #69 | 3116478-69.patch | 23.45 KB | clayfreeman |
| #69 | 3116478-69.interdiff.txt | 299 bytes | clayfreeman |
Comments
Comment #2
clayfreemanAdding a patch to implement this feature request.
Comment #3
clayfreemanComment #4
cliddell commentedNice. Can't find any need for improvement/nitpick and appears to work as intended. Sending to RTBC.
Comment #5
clayfreemanSetting to "Needs Work" until I have a chance to add a test as per the suggestion of @chx.
Comment #6
ghost of drupal pastComment #7
clayfreemanAdded a test case for queue item processing to assert expected logging and queue characteristics.
Comment #8
ghost of drupal pastThis is fantastic, thanks!
Comment #9
jungleNot sure if this kind of indentation is encouraged
Comment #10
jungleFound the corresponding coding standard is
Drupal.Formatting.MultipleStatementAlignment.NotSame, #2937853: Fix 'Drupal.Formatting.MultipleStatementAlignment' coding standardI'd set back to NW, besides of this, RTBC +1
Comment #11
jungleShould be Drupal.WhiteSpace.OperatorSpacing.NoSpaceBefore probably. But phpcs says that it does not exist
Comment #12
jungleComment #13
clayfreemanThanks for the feedback @Charlie ChX Negyesi and @jungle!
I've attached an updated patch to remove extraneous whitespace. Let me know if I should change anything else.
Comment #14
jungleThanks @clayfreeman
Just FYI, personally, I prefer using prophecy. See https://www.drupal.org/docs/8/phpunit/using-prophecy
Comment #15
longwaveThis is a feature request and so I think it can only go into 9.1.x now.
Do we also need a kernel test, ie. extend or duplicate Drupal\Tests\system\Kernel\System\CronQueueTest?
Is it neater to use assertFalse/assertTrue over assertEquals here?
Comment #16
clayfreemanI'm not too familiar with the semantics for feature requests, but since this is a fairly small non-BC breaking change shouldn't it be acceptable for release under the guidelines (especially since there was no 8.9 alpha)?
The primary reason that my organization is looking forward to this patch hitting 8.9 is because we're going to have a substantially harder time upgrading to 9.x due to the size of our code base. While not preferred, if this issue must target 9.x I can appreciate that.
Attached is an updated patch to add some testing to
\Drupal\Tests\system\Kernel\System\CronQueueTestand useUnitTestCase::assertTrue()/UnitTestCase::assertFalse().Comment #17
clayfreemanComment #18
longwaveI guess it is a minor addition with no disruption so this is perhaps OK for 8.9.x after all - let's leave that to the core committers to decide.
The changes look great, thanks for adding to CronQueueTest to prove that the delayed item works end to end, so marking this RTBC.
Comment #19
neclimdulSo I started thinking through if this gave other queue systems the tools to support this behavior and just catching an exception and continuing starts to seem kinda weird and hacky. You have no idea what the delay is going to be and its just what ever the claiming code sent as the lease for running the queue and now we're going to delay out the rest of the time. :( Shouldn't we be sending a delay? Which leads to should there be some sort of
delayItem($item, $delay)method that delay's the item allowing say beanstalk to send an explicit delay call?Maybe I'm wrong, the code is very simple, but I'm not sure this is the right approach.
Comment #20
clayfreemanHere's a supplemental conversation on Slack for the record.
This patch adds support for a custom delay interval set in
DelayedRequeueExceptionthat is intended to be used for updating the item's lock expiry timestamp to the current time plus the specified interval (in seconds). Custom delays are only supported for queues that implement the newDelayableQueueInterface.I'm not sure that this feature is immediately applicable to
\Drupal\Core\Cron::processQueues(), so I'm not going to spend time writing tests for it yet; in the linked discussion, it was decided that this would be useful primarily for persistent queue processors but cron is definitely not that.@neclimdul let me know if this patch addresses your concern. I personally would opt to implement this functionality at a later date since it can be done without breaking BC and presently adds way more complexity than this issue originally called for without any gain due to the current apparent lack of support for persistent queue processors.
Comment #21
jungleThank you, @clayfreeman!
Prefer replacing
else-ifwithifto both two usages, The logic do not change if to do so.It's tricky here,
Cron::processQueues()is a protected method. Repalace with$this->cron->run();the test should pass as well.$this->cron->run()does callCron::processQueues()Do not understand why to allow float. delay 0.5 sec? Make a little sense, if so, why not just use microseconds.
The
::setup()is about 70 lines long. not sure if using prophecy can make it better.\Drupal::state()are called many times, Set it a property in::setup()?Better to use
$this->assertEquals()Comment #22
clayfreemanThanks for the additional review @jungle. Here are my responses:
OK.Done.::setUp()since I would have to fully mock services that\Drupal\Core\Cron::run()uses but\Drupal\Core\Cron::processQueues()never uses. There are several examples of reflection being used in other tests; is there a reason why we should do the extra work to switch which method is being called here?microtime(TRUE))I can try to refactor to use Prophecy.Done.OK.Done.OK.Done.Do you think it is necessary to add the extra functionality from #19 to
\Drupal\Core\Cron::processQueues()? Or should we remove it since it won't necessarily be accurate?Also, do you think that it is worth exploring support for a custom delay at this time or should that be postponed until later? I'm still unclear about what the immediate value-add would be if we implement this now.
Comment #23
jungleThanks for your quick response. @clayfreeman
Default to 0 -- no delay. So it won't break existed queue workers.
Comment #24
clayfreemanUploading a patch containing my progress from #22. Haven't reviewed your patch yet.
Comment #25
clayfreemanUploading new patch to adopt changes in #23. Thanks @jungle!
For some reason during initial development cron wouldn't run the queues for me, but it's working now and your suggestion is better.
Comment #26
neclimdulYeah, I actually agree with your BC note in context of this method signature because this change could be a problem. I didn't notice that's what you put in slack.
The BC concern is why I suggested something like
public function delayItem($item, $delay)in my earlier review because it is an entirely new method.. Its also a bit clearer, and I'm realizing now possibly allow you to _add_ an item with a delay which is handy.Totally a nit not relevant to the issue but these magic numbers are kinda hard to follow. Maybe we could make them constants on the test to tie everything together and self document what the states are?
Also, this doesn't test the delay time. :(
oh this is a lot easier to read, nice work! I'm also a big fan of prophecy but I didn't want to say anything because a test is a test and I'm not writing it. :-D
This made me uncomfortable the first time I read it and I wasn't sure why but now thinking about it I realize the problem. It is sort of a ticking time bomb because if another test defines the method tests might pass or fail depending on the order the tests run in. I don't have a suggestion of the top of my head but yeah.
I guess a real fix might be blocked on #2932518: Deprecate watchdog_exception which is no reason to hold this up. Can you just add @runInSeparateProcess to the test, that might avoid the problem for now. :(
Comment #27
clayfreeman::releaseItem($item, $delay = 0)to::delayItem($item, $delay)so that we don't have BC concerns.CronQueueTestExceptionto use class constants for its state values. These are used byCronQueueTestnow as well.CronQueueTestshould now test for a value set byDatabaseQueue::delayItem()that is larger than the original lease.CronTestto add@runInSeparateProcess.Comment #28
neslee canil pinto@clayfreeman, #27 Looks good to me 👍🏻, Moving it to RTBC, Thanks.
Comment #29
jungleHi @Neslee Canil Pinto. From what I got from the conversations on slack between @neclimdul and @clayfreeman, @neclimdul will check back soon, who is the maintainer of Queue/QueueAPI. His opinions are important. So let's set it back to Needs Review.
Comment #30
neclimdulThanks everyone, I've got a few observations that should probably be noted for committers and i'm sorry, a nit that will make this easier to use.
Observation, boy howdy you have to work hard to get a negative value to trigger this :) Probably could have just tested the delay since using a negative delay to set a time in the past doesn't really do anything I don't think but its all good.
nit, could we just take the delay as a constructor argument to make this more straight forward?
good catch. not related but we'll keep it a secret between you me and all the people that read this comment ;)
Comment #31
clayfreemanComment #32
neclimdulhm... thought i'd kicked this up. I think this is looking very good, lets see what committers think.
Comment #33
jungleI think a CR is necessary.
Comment #34
clayfreemanCR created here.
Comment #35
clayfreemanUpdating patch to address a nitpick of the new test case's
::setUp()method return type. Leaving in RTBC since this is a rather small change that shouldn't need additional review.Also changing target to
9.1.xsince we're in a beta feature freeze. Still, I understand that core committers have some leeway in what makes it in during a beta freeze (especially if the change is rather minor); if this issue makes it into8.9.x, my team would be much obliged!Comment #36
clayfreemanI should note that the change in #35 is only backward compatible with Drupal versions where the minimum PHP version is at least 7.1; the patch in #31 should be used where this is not the case.
Comment #37
alexpottHere's some thoughts about the runtime code. I've not reviewed the tests yet.
What happens if $queue is not a DelayableQueueInterface and is that tested?
Why the call to intval() - now we have PHP 7.3 we can typehint to integer.
I ponder if you mean here that $delay should always be positive. Imo we should throw an exception if delay is negative.
Let's typehint $delay to integer.
Let's not have a setter that gives this exception state that's unnecessary. Do the setting in the constructor.
The new functionality definitely looks useful.
Comment #38
clayfreemanHere's a link to a discussion on Slack about whether or not we should abandon sub-second precision in the delays by type-hinting to
intin::delayItem()(and related code). We decided that we don't really see a value in sub-second precision, so support for that is being removed in this patch to address the review criteria in #37.Here's my feedback on the review:
CronTest.phpon line 147. I was hesitant to include a test for the actual expiry value since the chance of unexpected temporal drift in the test runners is non-zero and I can't think of a way to easily control for that; I'd much rather have a reliable test than one that fails occasionally for seemingly no reason.::delayItem()is using a typehint ofintand throw an exception if a negative value is encountered.Thanks for your review @alexpott! This has been a fun one to write and I look forward to having my first contribution to Drupal under my belt :)
Comment #39
clayfreemanJust realized that I missed a couple types in the documentation.
Comment #40
clayfreemanThird time's the charm... posting an interdiff all the way back from 35 to make review easier; kept noticing other things that needed updating.
Comment #41
jungleThank you @clayfreeman! All points in #37 are addressed.
But the comment to address #37.1, I would suggest changing it to the following:
1) Use
-to organize them in bullets, 2)requeued, should bere-queuedprobably, see #3138768: [Meta] Fix flagged spelling errors due to missing hyphens for prefixesOtherwise, this is RTBC.
Comment #42
mrinalini9 commentedComment #43
mrinalini9 commentedUpdated patch #40 along with the changes suggested in #41, please review.
Comment #44
jungleThanks, @mrinalini9!
Comment #45
ghost of drupal pastWhen did we start adding scalar typehints? Is this a new policy for D9.1?
Comment #46
krzysztof domański@Charlie ChX Negyesi For now only a few test methods require type hints. See [Meta] Implement strict typing in existing code and Overridden test methods require void return type hints. Since Drupal 9 requires PHP 7.3 we can make it better.
Comment #47
catchShould there be some kind of message logged when the queue doesn't support ::delayItem()?
This should use the time service.
Comment #48
clayfreemanNo idea if this patch will pass testing, but here goes. I'm attempting to address #37.1 with additional testing.
In reply to #47:
time(); this enabled me to mock the time service for reliable testing (I was cautious to avoid implementing unreliable testing in #38, but now we can do so safely).I opted to skip the setup routine for testbot locally because that takes time that I don't have, so I'm relying on Drupal CI for initial complaints here, then I'll set up a local test bot if debugging becomes necessary.
Comment #49
jungleAbout #47.2, maybe we could add a time-service related Trait which is similar to the
\Drupal\Core\Messenger\MessengerTraittrait to bypass the BC concerns, For example, call$this->getTime()to get the "datetime.time" service instance.Comment #50
clayfreemanAttaching a new patch that introduces the trait proposed by @jungle in #49.
This patch successfully accommodates the testing requested by @alexpott in #37.1 and the criteria provided by @catch in #47.2. #47.1 is addressed by improved documentation on
Drupal\Core\Queue\DelayedRequeueException.Comment #52
clayfreemanComment #53
jungle#47.2 was addressed via introduced the
TimeTraitwhich wrapped the\Drupal::time();service and suggested by me in #49. I am not sure if it is a good idea. But if it's not, I would suggest moving forward here, furthermore, rescoping #3123216: Replace non-test usages of \Drupal::time() with IoC injection and refactoring it with IoC in #3123216, removing theTimeTraitthere if necessary.So setting back to RTBC, to have opinions from committer(s) again.
Thanks!
Comment #54
clayfreemanFollow-up for committers on #53.2:
The BC concern that we're trying to avoid stems from a complication in core/includes/form.inc:1022.
For
DatabaseQueue, this is fine, but forMemoryQueue(or any other queue), this could pose issues. This is why we ended up addingTimeTrait.See this conversation on Slack for more information. There are several alternatives that require committer direction:
TimeTraitin favor of adding it in a separate ticket that blocks this one. This could slow things up quite dramatically here and my team is quite eager to get this particular issue resolved. Aside from proper issue scoping, is there any value-add in doing this?TimeTrait. While technically out of scope, we are testing it implicitly so if it works for the queue subsystem, it should work anywhere else. Since the work is already done, this may serve as a decent compromise. We also get the added benefit of better testing in this issue.\Drupal::time()usage alone (ignoring #47.2) and add a follow-up issue (or maybe extend the scope of #3123216: Replace non-test usages of \Drupal::time() with IoC injection?) to replace it with dependency injection at a later date. This will require us to remove testing that is valuable to this issue.There may be more options that I'm missing; just wanted to lay out the obvious options and their pros/cons.
Comment #55
alexpottI discussed this issue with @catch. Never of us are keen on adding a TimeTrait in this issue. I think the best way forward here is use \Drupal::time() here and then open a follow-up to change _batch_queue to use the class resolver service so queue classes can get their dependencies injected.
Whilst looking at the above I also realised that one impact of this change is that now core's batches via \Drupal\Core\Queue\Batch will automatically support this functionality. I'm not sure whether this is an intended side-effect - or even desired - I've not thought it through yet. But, if Batch does support this then I think we need to make sure that that is documented properly.
Comment #56
clayfreeman@alexpott thanks for taking the time to review and leave feedback.
This patch removes the
TimeTraitand instead uses\Drupal::time()as a fallback. Luckily, we're still able to override this service to provide a reliable time base for these tests (contrary to what I suspected).I'm not familiar with the batch subsystem, so I'll need further guidance on what to do to make sure the documentation updates are squared away.
I believe #3123216: Replace non-test usages of \Drupal::time() with IoC injection is well-scoped enough to accommodate your request for a follow-up issue to replace
\Drupal::time(). If you disagree, I can open a separate issue or the scope of that issue can be adjusted when this is merged.Comment #57
andypostthis changes looks out of scope but could use separate issue to inject time service otoh all this places are valid to use time() as exact system time is needed. Better to add new child into #2729597: [meta] Replace \Drupal with injected services where appropriate in core
The only reason to do injection of new service is better ability to unit-test the code, but not sure it makes sense
instead of "mixed" better use 'object'
I think set value makes sense only when $delay > 0 (as default is 0 in class property)
needs type-hint to int
Comment #58
jonathanshawBatch operations are documented in core/includes/form.inc. Also core/lib/Drupal/Core/Form/form.api.php callback_batch_operation() may be a relevant place to document. Neither place currently mention anything about how exeptions (including the existing RequeueException) are handled.
But the first task is to establish what effect throwing DelayedRequeueException in a batch operation currently has with this patch. Does the item get processed again at the end of the batch? Does it get left in the queue for the next time the batch is run? Does it get orphaned in the queue and will never run? Does it get silently discarded? It's possible this needs a test.
Comment #59
clayfreemanRe #57:
time()for\Drupal::time()->getCurrentTime(). Please refer to the changes made to\Drupal\Tests\system\Kernel\System\CronQueueTestby this patch; a stable time base is extremely important to be able to reliably test the functionality of the proposed change(s).Re #58:
\Drupal\Core\Cron, I don't see how this specific change has any impact on batch. The two subsystems appear to be unrelated at first glance.DelayableQueueInterface::delayItem()was used, but\Drupal\Core\Queue\Batch::claimItem()disregards item expiry entirely in its query.\Drupal\Core\Queue\BatchMemoryisn't susceptible to any repercussions since it doesn't implementDelayableQueueInterface.Comment #60
jonathanshawMakes sense.
Seems to me the feature is RTBC, but the wonderfully extensive test coverage could use a few tweaks.
Updating the state from within the test queue plugin seems odd. I understand it was how it was done before, but now that this test is getting more complicated maybe it would be best to move these into testExceptions() to make the flow of the test logic more explicit.
Probably better to do these sanity tests within setup rather than a a seperate test.
watchdog_exception is scheduled for deprecation in #2932518: Deprecate watchdog_exception. What we usually do is mock/prophesize the logger. See e.g. core\tests\Drupal\Tests\Core\Block\BlockManagerTest
Feels like this could use a dataProvider to be more DRY
Comment #61
clayfreeman::testDelayException().::setUp().watchdog_exception()definition has been removed from the test, but there will be additional follow-up for the referenced issue sincewatchdog_exception()would otherwise be undefined. Let me know if I took the right approach and whether I should proceed to update the scope of #2932518: Deprecate watchdog_exception.Comment #62
jonathanshawI'm surprised if this is needed. Drupal's test isolation is usually rather good as is.
The obvious question here is why we're not using $logger->expects() and are doing this complicated state dance instead. At the very least this needs a comment explaining. I suspect the reason is that we need to know the arguments from the dataProvider to set the logger expectation and we don't know these in setUp(). But why not move the logger setup into the test method itself? Seems cleaner to me.
It would seem conceptually simpler to have this item return NULL on the second call, the same as the 'Complete' item? Because RequeueException is what we care about here, not DelayedRequeueException.
Comment #63
clayfreemanI noticed some additional things that needed testing, namely the requeue count for
Drupal\Core\Queue\RequeueException, and improved the compatibility of this test with #2932518: Deprecate watchdog_exception when it's merged by adding a mock for each method ofPsr\Log\LoggerInterface.In response to your review:
Comment #64
jonathanshawComment #65
mxr576Awesome! I had to implement this feature in Drupal 7 from scratch for processing a huge amount data via a 3rd party system that could have failed anytime, it was years ago but this feature finally gets supported in Drupal
89! RTBC++Comment #66
mikechr commentedThis also works for me
Comment #67
catchThe test coverage is a bit tricky here, but I don't have ideas to simplify it, and the coverage it adds is good.
We need to add 'delayable' to the cspell dictionary, but otherwise RTBC for me.
Also we should open an issue against drush to support this once it's added here - since that's the main queue runner in contrib.
Comment #68
jungleAdded "delayable" into core/misc/cspell/dictionary.txt
Tagging "Needs followup" for this.
Setting back to RTBC.
Thanks!
Comment #69
clayfreemanRe-upload to fix error in @jungle's reply. No changes, aside from creating a non-empty patch.
Comment #70
jungleOh, my bad, @clayfreeman, thanks!
Comment #72
catchCommitted 0f10d21 and pushed to 9.1.x. Thanks!
Comment #73
catchOpened the follow-up for drush: https://github.com/drush-ops/drush/issues/4543
Comment #74
clayfreemanThanks everyone for your input. This has been quite a journey to my first Drupal core contribution, but I'm very happy with the end result.
Just to note, the patch in #63 should also apply cleanly to 8.9.x for anyone needing this in D8; the only difference is the lack of an update to
core/misc/cspell/dictionary.txt, which didn't exist until D9.Also included some small fixes to the issue summary.
Comment #75
jungle@clayfreeman, congrats on your first Drupal core contribution!
Comment #77
dpiCreated #3177922: DelayedRequeueException should call parent, and optionally allow providing default args to cover improving
\Drupal\Core\Queue\DelayedRequeueException.Comment #78
br0kenHere is a re-roll for 9.0.x with #3177922: DelayedRequeueException should call parent, and optionally allow providing default args included.
Comment #79
br0ken#3183220: Add support for pre-delayed queue items
The follow-up on this issue: allow mutating the queue item's data between processing attempts.
Comment #80
larowlanFolks may be interested in #3198868: Add delay to queue suspend too
Comment #81
quietone commentedI don't think this is related to implementing the coding standards in core.