Refs #24366 -- Fixed recursion depth error in migration graph

Made MigrationGraph forwards_plan() and backwards_plan() fall back to an
iterative approach in case the recursive approach exceeds the recursion
depth limit.
This commit is contained in:
Marten Kenbeek 2015-03-28 17:46:10 +01:00 committed by Markus Holtermann
parent bc83add04c
commit 75430be86f
2 changed files with 68 additions and 9 deletions

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import warnings
from collections import deque from collections import deque
from django.db.migrations.state import ProjectState from django.db.migrations.state import ProjectState
@ -7,6 +8,13 @@ from django.utils.datastructures import OrderedSet
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import total_ordering from django.utils.functional import total_ordering
RECURSION_DEPTH_WARNING = (
"Maximum recursion depth exceeded while generating migration graph, "
"falling back to iterative approach. If you're experiencing performance issues, "
"consider squashing migrations as described at "
"https://docs.djangoproject.com/en/dev/topics/migrations/#squashing-migrations."
)
@python_2_unicode_compatible @python_2_unicode_compatible
@total_ordering @total_ordering
@ -139,7 +147,12 @@ class MigrationGraph(object):
self.ensure_not_cyclic(target, lambda x: (parent.key for parent in self.node_map[x].parents)) self.ensure_not_cyclic(target, lambda x: (parent.key for parent in self.node_map[x].parents))
self.cached = True self.cached = True
node = self.node_map[target] node = self.node_map[target]
return node.ancestors() try:
return node.ancestors()
except RuntimeError:
# fallback to iterative dfs
warnings.warn(RECURSION_DEPTH_WARNING, RuntimeWarning)
return self.iterative_dfs(node)
def backwards_plan(self, target): def backwards_plan(self, target):
""" """
@ -154,7 +167,35 @@ class MigrationGraph(object):
self.ensure_not_cyclic(target, lambda x: (child.key for child in self.node_map[x].children)) self.ensure_not_cyclic(target, lambda x: (child.key for child in self.node_map[x].children))
self.cached = True self.cached = True
node = self.node_map[target] node = self.node_map[target]
return node.descendants() try:
return node.descendants()
except RuntimeError:
# fallback to iterative dfs
warnings.warn(RECURSION_DEPTH_WARNING, RuntimeWarning)
return self.iterative_dfs(node, forwards=False)
def iterative_dfs(self, start, forwards=True):
"""
Iterative depth first search, for finding dependencies.
"""
visited = deque()
visited.append(start)
if forwards:
stack = deque(sorted(start.parents))
else:
stack = deque(sorted(start.children))
while stack:
node = stack.popleft()
visited.appendleft(node)
if forwards:
children = sorted(node.parents, reverse=True)
else:
children = sorted(node.children, reverse=True)
# reverse sorting is needed because prepending using deque.extendleft
# also effectively reverses values
stack.extendleft(children)
return list(OrderedSet(visited))
def root_nodes(self, app=None): def root_nodes(self, app=None):
""" """

View File

@ -1,7 +1,8 @@
from unittest import expectedFailure import warnings
from django.db.migrations.graph import ( from django.db.migrations.graph import (
CircularDependencyError, MigrationGraph, NodeNotFoundError, RECURSION_DEPTH_WARNING, CircularDependencyError, MigrationGraph,
NodeNotFoundError,
) )
from django.test import TestCase from django.test import TestCase
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -164,11 +165,14 @@ class GraphTests(TestCase):
graph.add_node(child, None) graph.add_node(child, None)
graph.add_dependency(str(i), child, parent) graph.add_dependency(str(i), child, parent)
expected.append(child) expected.append(child)
leaf = expected[-1]
actual = graph.node_map[root].descendants() forwards_plan = graph.forwards_plan(leaf)
self.assertEqual(expected[::-1], actual) self.assertEqual(expected, forwards_plan)
backwards_plan = graph.backwards_plan(root)
self.assertEqual(expected[::-1], backwards_plan)
@expectedFailure
def test_graph_iterative(self): def test_graph_iterative(self):
graph = MigrationGraph() graph = MigrationGraph()
root = ("app_a", "1") root = ("app_a", "1")
@ -180,9 +184,23 @@ class GraphTests(TestCase):
graph.add_node(child, None) graph.add_node(child, None)
graph.add_dependency(str(i), child, parent) graph.add_dependency(str(i), child, parent)
expected.append(child) expected.append(child)
leaf = expected[-1]
actual = graph.node_map[root].descendants() with warnings.catch_warnings(record=True) as w:
self.assertEqual(expected[::-1], actual) forwards_plan = graph.forwards_plan(leaf)
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, RuntimeWarning))
self.assertEqual(str(w[-1].message), RECURSION_DEPTH_WARNING)
self.assertEqual(expected, forwards_plan)
with warnings.catch_warnings(record=True) as w:
backwards_plan = graph.backwards_plan(root)
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, RuntimeWarning))
self.assertEqual(str(w[-1].message), RECURSION_DEPTH_WARNING)
self.assertEqual(expected[::-1], backwards_plan)
def test_plan_invalid_node(self): def test_plan_invalid_node(self):
""" """