Problem/Motivation

This issue has been introduced in #3622264: Avoid expensive operations while client waits for response and discovered by @scotwith1t in #3581562-14: Add option to disable Linkchecker (esp. entity hooks):

Drush calls kernel terminate once, when the command ends. Every fieldable entity saved during the run is queued in LinkExtractorService::$entities and LinkCleanUp::$entityStack, and the extraction runs after the last save. Resetting the entity memory cache in the script does not free them; the two queues hold their own references.

Measured on a site with 332k rows in linkchecker_link: 200 published nodes with 5 links each, saved in one drush php:script run, entity caches reset every 50 saves, the site's other per-save side effects paused.

MR !156 applied, switch off (the HEAD code path) MR !156 applied, switch on
The 200 saves 2.4 s 2.3 s
Entities held until terminate 400 (each node and its content_moderation_state entity) 0
Memory after the saves and cache resets 74 MB 34 MB
Work run at kernel terminate 272.8 s 0

So the command looks finished after 2.4 s and the process then runs for another four and a half minutes (each per-link existence query is a full-table scan on this site, 80 to 100 ms, see #3607810). Memory grows by roughly 200 KB per saved node with nothing able to reclaim it. A 30k-node import would hold roughly 6 GB at that rate; with any normal memory_limit the process dies first, and everything still queued is lost. Entities that already had an index row are then not revisited by cron, because LinkExtractorBatch::processEntities() only selects entities with no row in linkchecker_index.

Proposed resolution

Maybe we need to revert parts of #3622264: Avoid expensive operations while client waits for response when running in batch mode.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

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

claudiu.cristea created an issue. See original summary.

claudiu.cristea’s picture

Issue tags: +Performance

claudiu.cristea’s picture

Status: Active » Needs review

This is ready for review

claudiu.cristea’s picture

It would be nice if @scotwith1t can run the benchmark tests from #3581562-14: Add option to disable Linkchecker (esp. entity hooks) against this MR

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

joelpittet’s picture

Thanks @claudiu.cristea, and thanks for all the time you've put into this queue lately. I know your time is limited, so I've tried to keep this focused.

I opened MR 181 from MR 180 so your commits are preserved if I've taken this in the wrong direction.

1. Deleting an entity throws at terminate

doCleanupForDeletedEntities() reuses the linkchecker_index condition group in the linkcheckerlink entity query, causing:

QueryException: '("entity_type" = …) AND ("entity_id" IN (…))' not found

That leaves the deleted entity's links behind and skips the rest of destruct(). Building a separate condition group using parent_entity_type_id/parent_entity_id fixes it.

I added a kernel test that deletes a node and terminates the kernel. It also creates an entity_test with the same ID to verify only the node's records are removed.

2. Queue IDs instead of entities

I think this is the main memory fix. The queues currently retain full entity objects until terminate, so long Drush imports keep every saved entity in memory.

MR 181 instead queues IDs, then loads and processes each entity in destruct() and resets its storage cache afterward. This keeps #3626994: Queued entities of different types with the same ID overwrite each other's per-type separation.

Saving 2000 nodes with 5 links each:

MR Queue by ID
Memory after saves 111.4 MB 12.9 MB
Peak memory 116.7 MB 18.9 MB

With IDs, memory stays flat as saves increase.

One small behaviour change: the old array_shift() loop could process entities added during destruct(); the new foreach does not. Since linkcheckerlink saves are no longer queued, I don't think anything realistic depends on that.

Possible follow-ups, but not needed here:

@scotwith1t, if you have a chance to rerun your #3581562-14: Add option to disable Linkchecker (esp. entity hooks) benchmark against this approach, that would be helpful.

claudiu.cristea’s picture

Assigned: Unassigned » claudiu.cristea

Assigning to fix the remarks

claudiu.cristea’s picture

Status: Needs review » Needs work
claudiu.cristea’s picture

Assigned: claudiu.cristea » Unassigned
Status: Needs work » Needs review

Let's get a review

  • joelpittet committed 1f31bdb5 on 2.1.x
    fix: #3627255 Memory issues when scanning for links
    
    By: claudiu.cristea...
joelpittet’s picture

Status: Needs review » Fixed

We made some headway here!

Queueing by ID is the big win.
Both MR versions hold about 16 MB where 2.1.x holds 115–134 MB. Terminate peaks at 22–40 MB instead of about 500 MB, and runs 2–5× faster.

Thanks for collaborating on this with me @claudiu.cristea! We can iterate on this I am sure.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.