TLDR

BatchQueue::claimItem() has a query which can cause memory issues in the database engine, if the queue table contains huge chunks of data for a single batch.

This can lead to the query getting stuck.

The visible effect is that the batch operation gets stuck / does not start.

Background / use case

On a project I am working on, I encountered a case where a query in BatchQueue::claimItem() got stuck, because another module (search_api_et) added unreasonably large chunks of data to the 'queue' table. See #3124298: Huge data size in batch queue items.

The first lesson of course is to fix that contrib module.

But I also found a possible (memory) optimization to the query in core, so will scale much better to huge batches. See #985184-6: Poor performance of batch API with large queues ff.

Technical details

The query looks like this:

public function claimItem($lease_time = 30) {
    while (TRUE) {
      try {
        $item = $this->connection->queryRange('SELECT [data], [created], [item_id] FROM {' . static::TABLE_NAME . '} q WHERE [expire] = 0 AND [name] = :name ORDER BY [created], [item_id] ASC', 0, 1, [':name' => $this->name])->fetchObject();
      }
      catch (\Exception $e) {
        $this->catchException($e);
      }
     [..]

In our case, the 'queue' table contained around 500 entries for a single batch name, and the 'data' column was quite big for each one.

Internally, it seems MySQL does the following:

  • Build the full list of 500 results with data + item_id. This can get very big if the 'data' column has long values.
  • Sort the full list of items with data + item_id.
  • Limit the list to the first item.
  • Return the first item.

It is possible to run this query directly in sqlc or in devel/php, without running a batch, given that:

  • the data is still present in the table.
  • you specify the correct 'name' parameter which actually has this huge result set.
  • the query contains an ORDER BY. Without that, it will not get stuck.

Symptoms:

  • If the data is sufficiently large, the query gets stuck.
  • If the data is not too large, the query is quite fast.
  • I have not found a case where the query is simply just slow but eventually finishes. This suggests that this is a memory issue inside the database engine, NOT a performance issue.

Solution

The solution is quite simple:

  • First fetch only the item_id, so that the temporary list inside MySQL will be smaller.
  • Use a second query to fetch the data for the given item_id.

Issue fork drupal-3124304

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

donquixote created an issue. See original summary.

donquixote’s picture

A similar situation can be found in SystemQueue::claimItem().
Here we find an additional condition for 'expire'.

donquixote’s picture

Status: Active » Needs review
StatusFileSize
new1.39 KB
new2.94 KB

Here is my shot at optimizing this.
I am providing separate patches for BatchQueue and SystemQueue.
We can later decide if this should both be done in this issue or a separate one.

I am not yet sure how this can be tested reproducibly.

douggreen’s picture

I came to the similar solution on my own, but I think you want to update the expire time before you grab the data.

douggreen’s picture

Version: 7.x-dev » 10.5.x-dev
Status: Needs review » Needs work

And modern Drupal has the same problem, see DatabaseQueue.

My summary of what is happening. Let's say that you have 10,000 items in your queue, and the serialized data for each is 5MB (our actual use-case), two processes are trying to claim the queue item at the same time, each processes query takes multiple seconds (up to 30 seconds), to get the same item and data, but only one of them will then succeed to set the expire and lock the item, thus the second process just wasted 30 seconds trying to get a lock.

We should instead, just get the item, set the expire, and then get the data.

douggreen’s picture

Title: BatchQueue::claimItem() can get stuck with huge data in batch queue » DatabaseQueue::claimItem() is inefficient with huge data
quietone’s picture

Version: 10.5.x-dev » 11.x-dev
Status: Needs work » Active
Issue tags: +Needs issue summary update

Changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies.

The technical details in the summary contains Drupal 7 code and the patch is for Drupal 7. Tagging for an issue summary update

pwolanin made their first commit to this issue’s fork.

pwolanin’s picture

Status: Active » Needs review

I just opened an issue fork against 11.x and made what I think are the simple changes needed for the DatabaseQueue

pwolanin’s picture

pwolanin’s picture

bramdriesen’s picture

bramdriesen’s picture

bramdriesen’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

To add something meaningful to this issue (beside my tests above :-) ), I updated the technical details code snippet.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Looking at the change think this makes since, if the data isn't needed why retrieve :)

catch’s picture

Status: Reviewed & tested by the community » Needs work

I think this could use a comment as to why we do two queries here so that no-one tries to 'optimize' it back to one query.

pwolanin’s picture

See #3517969: Track the number of times a queue item is claimed and pass queue item metadata as an optional param to queue workers which includes this change and additional improvements, though maybe not the suggested comment.

I'd like to be able to close this as duplicate if that gets committed.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

prudloff’s picture

#3517969: Track the number of times a queue item is claimed and pass queue item metadata as an optional param to queue workers contains a huge MR.
Changes are more likely to be committed if they are kept small and with a clear scope.

prudloff’s picture

Status: Needs work » Needs review

I think this could use a comment as to why we do two queries here so that no-one tries to 'optimize' it back to one query.

I added a comment.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update

Should #3593606: Trigger warning when #access has an invalid value be folded here, each only contain 1 file.

This one could use a summary cleanup. Not sure if test coverage can be added.

prudloff’s picture

smustgrave’s picture

lol you are 100% right

smustgrave’s picture

Postponed #23 thinking it could be folded into here. Since we are talking 2 files to review

Still would need an IS update.