Ticket #8350 (closed Bug: fixed)

Opened 4 years ago

Last modified 10 months ago

GenericSetup step loop: portlets -> content -> plone-final -> portlets

Reported by: rossp Owned by: kiorky
Priority: major Milestone: 4.x
Component: Infrastructure Keywords:
Cc:

Description

When registering an import step with a dependency on "content" (possibly the others), the step will fail to run after the "content" step due to a loop in the step dependencies. Specifically, in plone/app/portlets/exportimport/configure.zcml:23 the "portlets" step depends on "content". The "content" step depends on "plone-final" in CMFPlone/profiles/default/import_steps.xml:38. Finally, the "plone-final" step depends on "portlets" in CMFPlone/profiles/default/import_steps.xml:115.

I've seen this in Plone 3.1.4 and several older versions. I'm not sure where is the right place to break the cycle, sorry. I've worked around it by removing the dependency on "content" in "portlets" in my ZCML, but I don't know if this would break anything in Plone:

    <genericsetup:importStep
        name="portlets"
        title="Portlets"
        description="Import portlet managers, types, assignments and blacklistings"
        handler="plone.app.portlets.exportimport.portlets.importPortlets">
        <depends name="componentregistry"/>
    </genericsetup:importStep>

Maybe the condition in the GS dependency resolution in GenericSetup/utils.py:852 that excepts circular loops should log an error?

Attachments

graph.svg Download (39.5 KB) - added by do3cc 3 years ago.
Dependency graph of import steps for 3.3rc3
graph.png Download (60.6 KB) - added by do3cc 2 years ago.
Same graph as svg
new.gif Download (78.0 KB) - added by kiorky 10 months ago.
new proposed dependencies

Change History

comment:1 Changed 3 years ago by charleso

  • Keywords generic setup circular dependencies added

I just experienced the same issue. I have a generic setup import step with a dependency of plone-final. This actually worked for one site due to the random nature of how the nodes are thrown into the graph list. On another site using the same code I was affected by the following line of code:

if len(unresolved) == len(graph):

# Nothing was resolved in this loop. There must be circular or # missing dependencies. Just add them to the end. We can't # raise an error, because checkComplete relies on this method. for node, edges in unresolved:

result.append(node)

By default the plone-final depends on portlets, portlets depends on content, and content depends on plone-final. Therefore you cannot reliably make a step depend on one of these steps

These steps should not depend on each other or at least some kind of warning should be shown if you try to use one as a dependency

comment:2 Changed 3 years ago by charleso

here's my work around, i hope this helps someone else. i removed the import_steps.xml file completely and added this to my configure.zcml :

<genericsetup:importStep

name="do_it" title="DO IT Install Some Product Content" description="This import step installs all the necessary default content objects into a pre-determined location." handler="mypackage.policy.setuphandlers.do_it">

<depends name="plone-final"/>

</genericsetup:importStep>

comment:3 Changed 3 years ago by b_mathieu

I made a monkey patch for the sorting function. Before inserting unresolved nodes it will detect nodes involved in a cycle and insert them, giving a better chance for remaining nodes to be inserted with better dependencies order. My patch also skip dependencies if there's no import step with that name. This behaviour may be questionable, I don't know.

import logging
from Products.GenericSetup import tool, utils, registry

LOG = logging.getLogger(__name__)

def is_cyclic(step, steps_map, visited=None):

    if visited is None:
        visited = []

    if step in visited:
        # we want to say True only if the node is involved in the cycle,
        # not if the cycle is in one of its dependencies
        return visited.index(step) == 0

    visited.append(step)
    deps = steps_map.get(step, ())

    for d in deps:
        tmp_visited = list(visited)
        if is_cyclic(d, steps_map, tmp_visited):
            return True
    return False

def _patched_computeTopologicalSort( steps ):
    result = []
    graph = [ ( x[ 'id' ], x[ 'dependencies' ] ) for x in steps]

    unresolved = []
    cyclics_inserted = False
    existing_edges = frozenset((x['id'] for x in steps))

    while True:
        for node, edges in graph:

            after = -1
            resolved = 0

            for edge in edges:

                if edge not in existing_edges:
                    # missing edged, don't bother
                    resolved += 1

                elif edge in result:
                    resolved += 1
                    after = max( after, result.index( edge ) )

            if len(edges) > resolved:
                unresolved.append((node, edges))
            else:
                result.insert( after + 1, node )

        if not unresolved:
            break
        if len(unresolved) == len(graph):
            # Nothing was resolved in this loop. There must be circular or
            # missing dependencies. Just add them to the end. We can't
            # raise an error, because checkComplete relies on this method.

            # 1st pass: insert cyclics in result before others, and let others
            # have a chance to be inserted with dependencies better resolved
            if not cyclics_inserted:
                cyclics_inserted = True
                node_map = dict(graph)
                graph = []
                unresolved = []
                for step in node_map:
                    if is_cyclic(step, node_map):
                        result.append(step)
                    else:
                        graph.append((step, node_map[step]))
                continue

            # 2nd pass: cyclics have been inserted, add remaining nodes
            for node, edges in unresolved:
                print repr((node, edges))
                result.append(node)
            break
        graph = unresolved
        unresolved = []

    return result

utils._computeTopologicalSort = _patched_computeTopologicalSort
tool._computeTopologicalSort = _patched_computeTopologicalSort
registry._computeTopologicalSort = _patched_computeTopologicalSort
LOG.info('PATCHED utils._computeTopologicalSort to have a fix for dependencies'
         ' properly sorted (https://dev.plone.org/plone/ticket/8350)')

comment:4 Changed 3 years ago by b_mathieu

More useful, here are the doctests for is_cyclic:

    >>> steps = dict([('base', ()),
    ...               (u'my-final', (u'plone-final', u'portlets')),
    ...               (u'plone-final', (u'portlets',)),
    ...               (u'portlets', (u'content',)),
    ...               (u'content', (u'plone-final',)),
    ...              ])
    >>> is_cyclic(u'base', steps)
    False
    >>> is_cyclic(u'plone-final', steps)
    True
    >>> is_cyclic(u'portlets', steps)
    True
    >>> is_cyclic(u'content', steps)
    True
    >>> is_cyclic(u'my-final', steps)
    False

and _computeTopologicalSort. It should fail on current implementation and succeed with mine (missing edges case is not tested, though).

    >>> steps = [{'id': u'base', 'dependencies': ()},
    ...          {'id': u'final', 'dependencies': (u'intermediate',)},
    ...          {'id': u'intermediate', 'dependencies': (u'base',)}
    ...          ]
    >>> _computeTopologicalSort(steps)
    [u'base', u'intermediate', u'final']

    now 'my-final' will depend on 'plone-final' and 'portlets', which are both
    involved in a cycle. We want 'my-final' to be placed after both.

    >>> steps = [{'id': u'base', 'dependencies': ()},
    ...          {'id': u'my-final', 'dependencies': (u'plone-final', u'portlets')},
    ...          {'id': u'plone-final', 'dependencies': (u'portlets',)},
    ...          {'id': u'portlets', 'dependencies': (u'content',)},
    ...          {'id': u'content', 'dependencies': (u'plone-final',)},
    ...          ]
    >>> _computeTopologicalSort(steps)
    [u'base', u'content', u'plone-final', u'portlets', u'my-final']

comment:5 Changed 3 years ago by wichert

  • Status changed from new to closed
  • Resolution set to invalid

GenericSetup is not maintained as part of Plone, but as part of CMF. Please submit a bugreport for the CMF project at launchpad.

comment:6 Changed 3 years ago by b_mathieu

  • Status changed from closed to reopened
  • Resolution invalid deleted

Ok, Reported GenericSetup issue:  https://bugs.launchpad.net/zope-cmf/+bug/314010

But you have closed the ticket too fast: it's summary says that CMFPlone has circular dependency in its profile, this is not of GS concern. AFAIK the import step loop is still defined in the plone profile: I reopen the ticket. Please correct me if the profile code has been fixed. BTW I don't think that it should be listed in "portlets" component.

comment:7 Changed 3 years ago by claytron

  • Keywords TuneUpJan30 added
  • Status changed from reopened to new
  • Owner optilude deleted

b_mathieu is correct, the circular loops just needs to be fixed in Plone. I am removing optilude as the owner as I know he is to busy to take care of this. We'll see if we can take care of this during the next tune-up.

There are two issues with the current import steps.

  1. as noted. the 'content' step depends on 'plone-final', which ends up depending on 'content' by way of the 'portlets' step
  2. the 'plonepas' step in Products.PlonePAS:default profile depends on a non-existent step named 'plonepas-contents'. this renders that step 'unresolved' as well.

comment:8 Changed 3 years ago by lucie

  • Status changed from new to assigned
  • Owner set to lucie

comment:9 Changed 3 years ago by lucie

I am thinking about no more having "plone-final" depending on "portlets" (I commented out the dependency into Products/CMFPlone/profiles/default/import_steps.xml). I succeeded to create a plone site with no problems so far, but I want to run all the plone steps to make sure nothing is broken. It will happen during the next tune up.

If this works, then we can also have "portlets" no more depending on "componentregistry", because "portlets" is already depending on "content", which is indirectly depending on "componentregistry"

comment:10 Changed 3 years ago by gabriellehp

  • Keywords TuneUpFeb20 added; TuneUpJan30 removed

comment:11 Changed 3 years ago by gabriellehp

  • Keywords TuneUp12 added; TuneUpFeb20 removed

comment:12 Changed 3 years ago by gabriellehp

  • Keywords TuneUp13 added; TuneUp12 removed

comment:13 Changed 3 years ago by lucie

  • Status changed from assigned to new
  • Owner lucie deleted

comment:14 Changed 3 years ago by hannosch

  • Component changed from Portlets to Infrastructure

comment:15 Changed 3 years ago by jluvsu2

  • Keywords TuneUp14 added; generic setup circular dependencies TuneUp13 removed

comment:16 Changed 3 years ago by jluvsu2

  • Keywords TuneUp15 added; TuneUp14 removed

comment:17 Changed 3 years ago by jluvsu2

  • Keywords TuneUp16 added; TuneUp15 removed

Changed 3 years ago by do3cc

Dependency graph of import steps for 3.3rc3

comment:18 Changed 3 years ago by do3cc

I got struck by this while finding out why my import step cannot depend on content. I do not understand the code enough to be able to untangle that dependency mess, but maybe the dependency graphs helps anybody who can.

comment:19 Changed 3 years ago by jluvsu2

  • Keywords TuneUp17 added; TuneUp16 removed

comment:20 Changed 3 years ago by jluvsu2

  • Keywords TuneUp18 added; TuneUp17 removed

comment:21 Changed 2 years ago by jluvsu2

  • Keywords TuneUp20 added; TuneUp18 removed

comment:22 Changed 2 years ago by jluvsu2

  • Keywords TuneUp21 added; TuneUp20 removed

comment:23 Changed 2 years ago by alecm

  • Priority changed from minor to major

I'm not sure which of these dependencies if any is needed and why they were added. In particular the dependence of the content step on plone-final seems odd based on nomenclature alone. In any case, the circular dependency has consequences for developers. Effectively, it means that the 'plone-final', 'portlets', and 'content' steps as well as any other steps that depend on those steps directly or indirectly will be run in a random order with dependencies ignored. IMO, this is a significant issue.

comment:24 Changed 2 years ago by jluvsu2

  • Keywords TuneUp22 added; TuneUp21 removed

comment:25 Changed 2 years ago by aclark

  • Owner set to aclark
  • Status changed from new to assigned

comment:26 Changed 2 years ago by aclark

  • Status changed from assigned to new
  • Owner aclark deleted

I can't reproduce this. I tried creating 'my.package' with import_steps containing 'mandatory-step' and 'circular-step' as described in  https://bugs.launchpad.net/zope-cmf/+bug/314010

Then added 'my-step' via zcml and ran the installer. No errors, am I missing something? My code:

From my.package/my/package/configure.zcml:

  <gs:importStep
      name="my-step"
      handler="my.package.setuphandlers.importVarious"
      title="test"
      description="test">
  </gs:importStep>

From my.package/my/package/profiles/default/import_steps.xml:

<import-steps>
    <import-step id='mandatory-step' version='1' handler='my.package.setuphandlers.importVarious'>
        <dependency step='my-step' />
    </import-step>
    <import-step id='circular-step' version='1' handler='my.package.setuphandlers.importVarious'>
        <dependency step='mandatory-step' />
    </import-step>
</import-steps>

comment:27 Changed 2 years ago by do3cc

Yes, there is no exception if the dependency is broken, but they are just added in random order. There should be an error message and there should not be a circular dependency in plone, see my dependency graph earlier.

The problem is in Products.Genericsetup.utils._computeTopologicalSort

Changed 2 years ago by do3cc

Same graph as svg

comment:28 Changed 2 years ago by jluvsu2

  • Keywords TuneUp23 added; TuneUp22 removed

comment:29 Changed 2 years ago by jluvsu2

  • Keywords TuneUp24 added; TuneUp23 removed

comment:30 Changed 2 years ago by jluvsu2

  • Keywords TuneUp25 added; TuneUp24 removed

comment:31 Changed 2 years ago by jluvsu2

  • Keywords TuneUp26 added; TuneUp25 removed

comment:32 Changed 2 years ago by jluvsu2

  • Keywords TuneUp27 added; TuneUp26 removed

comment:33 Changed 2 years ago by jessilfp

  • Keywords TuneUp28 added; TuneUp27 removed

comment:34 Changed 23 months ago by jessilfp

  • Keywords TuneUp29 added; TuneUp28 removed

comment:35 Changed 22 months ago by jessilfp

  • Keywords TuneUp30 added; TuneUp29 removed

comment:36 Changed 21 months ago by jessilfp

  • Keywords TuneUp30 removed

comment:37 Changed 18 months ago by do3cc

This bug is still an issue in Plone 4. I added a logging message to GenericSetup trunk that emits a warning if there is a problem. The Warning also contains data that can be fed into the dot tool from the graphviz package. It visualizes the dependencies and highlights the problematic parts.

When I added the log, GenericSetup 1.6.2 was current. I assume in 1.6.3 it will be included.

comment:38 Changed 10 months ago by kiorky

Also ran into this bug, plone 4.0.5

comment:39 Changed 10 months ago by kiorky

I fixed the plonepas step (now registered in zcml and drop the unexistant plonepas-content step). Its commited in r237801 (collective).

Looking if i can do something for the other issue.

comment:40 Changed 10 months ago by kiorky

I partially fixed the bug in collective -r13592. In Products.MimeTypesRegistry, i also converted the import step to be registered in ZCML. Without that, if the products isn't installed, we will throw into this cyclic dependencies bug.

Playing around with Plone egg code to fix the last problem with portlet -> content -> plone-final

comment:41 Changed 10 months ago by kiorky

Ooops, for MimeTypesRegistry, it's on archetypes repo, of course !

comment:42 Changed 10 months ago by kiorky

  • Milestone changed from 3.3.x to 4.x

comment:43 Changed 10 months ago by kiorky

Also commited a related patch to collective.flowplayer.

comment:44 Changed 10 months ago by kiorky

Commited another change in -r48735 on plone.app.upgrade (plone)

comment:45 Changed 10 months ago by kiorky

oups, my bad -r48950

comment:46 Changed 10 months ago by kiorky

Related commit in -r48959

comment:47 Changed 10 months ago by kiorky

Port of -r48950 in -r48960 for plone41.

This bug is now closed.

But, BUT, but.

Be aware of products using legacy import/exports steps.xml files to declare their steps.

If:

  • OTHERS products rely on those declared steps
  • This product is not specifically installed into your plonesite

that will trigger dependencies problems in the generic setup dependency machinery triggering circular dependencies errors.

You have then two possible fixes:

  • Install the product which has the step onto your plonesite to have the step. BAD.
  • Fix the product to register the step with a ZCML slug as:

for an import step

  <genericsetup:importStep
      name="plone-difftool"
      handler="Products.CMFDiffTool.exportimport.difftool.importDiffTool"
      title="Diff Tool"
      description="Import CMFDiffTool settings">
  </genericsetup:importStep>

Or for an export step

  <genericsetup:exportStep
      name="content"
      title="Content"
      description="Export the site's structure and content."
      handler="Products.CMFCore.exportimport.content.exportSiteStructure"
      />

And delete the .xml files ;)

Finally, you may also have to purge your portal_setup registries from thoses step with such a migration code as example:

from Products.GenericSetup.registry import _import_step_registry
from zope.app.component.hooks import setSite

logger = logging.getLogger('update Portal Setup Registries')
logger.warn('running')
setSite(plone)
setup_tool = plone['portal_setup']
irs = setup_tool.getImportStepRegistry(test=True)
rewrite = ["plonepas"]
for registry in irs, _import_step_registry:
    registered = registry._registered
    for key in registered.keys():
        step = registered.get(key)
        if 'plonepas' in key:
            step["dependencies"] = (u'componentregistry', u'memberdata-properties', u'rolemap')
        if not key in rewrite:continue
        logger.warn("Rewriting GenericSetup import step : %s" % key)
        if not isinstance(registry, dict):
            registry.unregisterStep(key)
            registry.registerStep(step["id"], step["version"], step["handler"],
                                  step["dependencies"] , step["title"], step["description"])

comment:48 Changed 10 months ago by kiorky

  • Status changed from new to closed
  • Resolution set to fixed

comment:49 Changed 10 months ago by kiorky

  • Status changed from closed to reopened
  • Resolution fixed deleted

comment:50 Changed 10 months ago by kiorky

  • Status changed from reopened to new
  • Owner set to kiorky

Changed 10 months ago by kiorky

new proposed dependencies

comment:51 Changed 10 months ago by kiorky

  • Status changed from new to closed
  • Resolution set to fixed

(In [48968]) another will to fix #8350

comment:52 Changed 10 months ago by kiorky

(In [48969]) another will to fix #8350

comment:53 Changed 10 months ago by kiorky

(In [48970]) another will to fix #8350

comment:54 Changed 10 months ago by kiorky

A better fix in -r48968 -r48969 48970.

comment:55 Changed 10 months ago by maurits

(In [48977]) Let the portlets import step depend on the content import step again. Refs #8350.

comment:56 Changed 10 months ago by maurits

(In [48978]) Let the content import step (importing the site structure) not depend on plone-final but on typeinfo. Solves circular dependency. Refs #8350.

comment:57 Changed 10 months ago by maurits

(In [48980]) Merged r48977 from trunk: Let the portlets import step depend on the content import step again. Refs #8350.

comment:58 Changed 10 months ago by maurits

(In [48981]) Merged r48978 from Products.CMFPlone/trunk: Let the content import step (importing the site structure) not depend on plone-final but on typeinfo. Solves circular dependency. Refs #8350.

Note: See TracTickets for help on using tickets.