Problem/Motivation

In some environments and workflows, the entity_usage_tracker queue processes items for entities that have already been deleted from storage.
Typical scenarios include:
Long‑running or delayed queues (e.g., cron/queue worker backlogs).
Database sanitation or custom cleanup jobs that remove entities directly.
Other cleanup queues (e.g., orphan reference purgers) deleting entities before entity_usage_tracker runs.
In these cases, the entity_usage_tracker queue worker currently:
Attempts to load() the source entity from the queue item.
If the entity cannot be loaded, logs an “Entity not found” error and returns early.
Does not clean up the corresponding rows in the entity_usage table.
This leaves stale usage records behind for sources that no longer exist. Downstream, any reports or Views that rely on entity_usage to determine whether a media/file/node is “used” will see these as still in use and will not treat them as orphans, even though all real references are gone.
This is particularly visible when trying to build “orphaned media/documents” reports that rely on entity_usage.count being 0 or NULL to detect unused items.

Steps to reproduce

Enable entity_usage and entity_usage_queue_tracking.
Ensure queue tracking is on so that usage updates go through the entity_usage_tracker queue.
Create a simple reference chain, for example:
A node N1 that embeds a paragraph P1.
Paragraph P1 references a media item M1 on some field (e.g., field_document).
Confirm that:
There is a row in the entity_usage table like:
target_type = 'media', target_id = M1
source_type = 'paragraph', source_id = P1
count >= 1.
Now delete the source entity out of band so that the queue will later see a missing entity. For example:
Use a custom cleanup or SQL delete to remove P1, or
Use another orphan reference purger that deletes P1 and does not immediately update entity_usage, or
Import a sanitized database where P1 has been removed but its entity_usage row remains.
Make sure there is still a queue item for that source entity in entity_usage_tracker. For example, confirm with drush queue:list that entity_usage_tracker has pending items.
Run the entity_usage_tracker queue (e.g., via Drush or cron).
Observe:
The queue worker logs an error similar to “Entity not found: Entity type paragraph, Entity ID P1.”
Despite the error, the row in entity_usage for source_type = 'paragraph' and source_id = P1 remains.
Any view/report looking for “unused media” by checking entity_usage (e.g., count IS NULL OR count < 1) will still see M1 as “used” and not orphaned.

Proposed resolution

When the queue worker processes a delete‑type operation and cannot load the source entity, it should treat this as a “best‑effort cleanup” situation and remove entity_usage rows by source instead of only logging and returning.
Concretely:
In the entity_usage_tracker queue worker (processItem()):
If $entity cannot be loaded and the operation is one of the delete operations (e.g., predelete, translation_delete, revision_delete), then:
Call the appropriate EntityUsageInterface method (for example, deleteBySourceEntity(source_id, source_type, ...)) to delete all entity_usage rows for that source.
Log that cleanup has occurred.
Return.
For non‑delete operations with a missing entity, keep the current behavior of logging an error and returning.
This change:
Does not alter the behavior of the normal, “happy path” where the entity still exists when the queue runs:
In those cases, trackUpdateOnDeletion($entity, ...) continues to be used to update entity_usage.
Adds a safe fallback for edge cases where:
The entity is already gone (but its queue item still exists).
The only correct behavior is to remove the now‑invalid entity_usage records for that missing source.
The net effect is that the entity_usage table remains consistent with the actual state of entities, and reports that rely on entity_usage to find “unused” or “orphaned” entities behave as expected even when deletes happen out of order or via separate cleanup processes.

Remaining tasks

[ ] Agree on the exact set of operations that should trigger the fallback (at minimum: predelete, translation_delete, revision_delete).
[ ] Implement the fallback cleanup logic in the queue worker using the existing entity_usage.usage service.
[ ] Add automated test coverage that simulates:
A queue item for a source entity that has already been deleted from storage.
The expected removal of entity_usage rows after the queue item is processed.
[ ] Update project documentation/changelog to mention the improved robustness around out‑of‑order deletes and queue processing.

User interface changes

None.

API changes

None.
The proposal uses the existing EntityUsageInterface APIs (e.g., deleteBySourceEntity) and does not change public signatures.

Data model changes

None.
The change only affects when and how existing entity_usage rows are cleaned up; no schema changes are required.

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

arthur.baghdasar created an issue.