Problem/Motivation

\Drupal\Core\DefaultContent\Finder::__construct() builds the dependency graph keyed by UUID, sorts it with \Drupal\Component\Graph\Graph::searchAndSort(), and reduces it to an ordered $this->datawith no check for dependency cycles. When _meta.depends forms a cycle (e.g. A→B→C→A), no valid topological order exists, but searchAndSort() still returns an arbitrary order, so at least one entity is imported before a dependency it references.

In \Drupal\Core\DefaultContent\Importer, setFieldValues() then calls loadEntityDependency(), which returns NULL because the unresolved target isn't yet imported; that NULL is passed straight to $property->setValue(). (Method line numbers vary by branch; verified on the current dev tip.)

  • For an entity_reference property this silently stores an empty reference — data loss whose occurrence depends on the filesystem iteration order of the Symfony Finder scan, so a recipe can pass CI on one machine and silently lose different data on another.
  • For entity_reference_revisions it is fatal, aborting drush site:install.

There is no NULL-guard in loadEntityDependency() and no cycle detection upstream. Silent, machine-dependent data loss is the worst outcome here; failing loudly at import time surfaces the authoring mistake immediately. (Filing as Normal, but flagging the data-loss angle for a priority call.)

Steps to reproduce

Author default-content YAML whose _meta.depends declarations form a cycle (e.g. a node → paragraph (ERR field_sections) → node → first node), then drush site:install <recipe>. Observed on a service node → paragraph (ERR) → node → service cycle.

Proposed resolution

Detect cycles in Finder immediately after Graph::searchAndSort() and throw ImportException naming the offending source files, instead of returning a broken order. A cycle is detected by self-reachability (isset($vertex['paths'][$uuid])); the full member set of each cycle is the strongly-connected component paths ∩ reverse_paths (so the back-edge vertex is named too, not just the entry vertices).

Tests included:

  • Unit (FinderTest): three-node cycle, self-loop, and two disjoint cycles — asserts every file in every cycle is named.
  • Kernel (ImporterTest): a cyclic directory makes importContent() throw before writing anything (fail-before-write). Verified RED→GREEN at both levels; no regressions across DefaultContent unit + kernel tests; PHPCS (Drupal/DrupalPractice) clean.

Related

  • #3442022 (Trigger entity validation in the core Importer — validate before save): adjacent. Cycle detection is upstream of, and complementary to, entity validation; worth deciding whether they should land together.
  • A NULL-guard in Importer::loadEntityDependency() would be complementary defense-in-depth, but it can only see one unresolved UUID mid-import and can't name the cycle. Proposing the Finder fix as fix-of-record, with the guard as optional follow-up hardening.

An MR will follow from an issue fork (patch + unit/kernel tests are ready).

Created with the help of Claude Fable

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3595546

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

alex ua created an issue. See original summary.

alex ua’s picture

Issue summary: View changes
quietone’s picture

Version: 11.x-dev » main
Issue summary: View changes

@alex ua, Thanks for the disclosure of AI usage.

Issues for Drupal core should be targeted to the 'main' branch, our primary development branch. Changes are made on the main branch first, and are then back ported as needed according to the Core change policies. The version the problem was discovered on should be stated in the issue summary Problem/Motivation section. Thanks.

Also restoring issue template so we can track progress and task.

phenaproxima’s picture

I am of two minds on this.

On the one hand: it's a legitimate bug and has thorough test coverage (which I have not reviewed in depth).

On the other hand, this is not really a problem with the default content system per se. Consider the doc comment of \Drupal\Component\Graph\Graph, which says:

This class represents a directed acyclic graph (DAG) and provides methods for processing and sorting it.

To me, this says that the Graph class is assuming there are no cycles in the graph, but not verifying it, which leads to the problems surfaced in this issue. So the flaw is lower-level than the default content system.

IMHO, it should not be possible to feed a cyclic graph to the Graph class. It should fail with an exception. But that would be a BC break.

So I propose the following:

  • Don't change the default content system; change the Graph class.
  • There should be a new public method, maybe called something like verifyGraphIsAcyclic(). It should throw an exception if the graph has any cycles, with a clear outline of the cyclic path to aid in debugging bad input data.
  • If searchAndSort() is called without having first called verifyGraphIsAcyclic(), raise a deprecation notice. This means all calling code needs to confirm it's providing an acyclic graph in the first place, and handle that case as needed.
  • In Drupal 13, searchAndSort() should always call verifyGraphIsAcyclic(), and we remove the deprecation.

Thoughts?

phenaproxima’s picture

It occurs to me that we might not be able to tell if the graph has cycles before we search-and-sort it. But even so, we should raise an error: when searchAndSort() is called, as a last step before returning it should check for cycles. If it finds one, it raises a deprecation notice. In Drupal 13, we convert that to an exception.

alex ua’s picture

Thanks, that direction makes sense to me.

I reworked the patch around Graph rather than keeping the cycle detection only in DefaultContent:

- Added Drupal\Component\Graph\Graph::verifyGraphIsAcyclic().
- Added Drupal\Component\Graph\CyclicGraphException with a concrete cycle path in the message.
- Added a deprecation from Graph::searchAndSort() when it is called on a cyclic graph without prior acyclic verification, with the Drupal 13 path left as the hard exception.
- Updated DefaultContent Finder to call the verifier and wrap the graph exception as an ImportException that names the default-content source files involved in the cycle.
- Kept the DefaultContent fail-before-write kernel coverage, so recipe authors get the actionable import failure rather than a later NULL reference or fatal.

That preserves the reported DefaultContent behavior while moving the invariant to the lower-level graph API. I am not claiming every existing Graph caller can safely throw in 11.x; the deprecation path is there for that BC boundary.

Prepared with AI assistance.