Rework of how services are sorted based on dependencies using a topological sort.
Includes error handling to detect circular/self imports (should raise a DependecyError).
Added in logging to the CLI to log out any DependencyErrors.
Removed the compact module as it is no longer used.
This commit is contained in:
Cameron Maske 2014-01-23 22:27:54 +00:00
parent ee880ca7be
commit ae7573b9b8
5 changed files with 139 additions and 39 deletions

View File

@ -8,7 +8,7 @@ import signal
from inspect import getdoc from inspect import getdoc
from .. import __version__ from .. import __version__
from ..project import NoSuchService from ..project import NoSuchService, DependencyError
from ..service import CannotBeScaledError from ..service import CannotBeScaledError
from .command import Command from .command import Command
from .formatter import Formatter from .formatter import Formatter
@ -40,10 +40,7 @@ def main():
except KeyboardInterrupt: except KeyboardInterrupt:
log.error("\nAborting.") log.error("\nAborting.")
exit(1) exit(1)
except UserError as e: except (UserError, NoSuchService, DependencyError) as e:
log.error(e.msg)
exit(1)
except NoSuchService as e:
log.error(e.msg) log.error(e.msg)
exit(1) exit(1)
except NoSuchCommand as e: except NoSuchCommand as e:

View File

View File

@ -1,23 +0,0 @@
# Taken from python2.7/3.3 functools
def cmp_to_key(mycmp):
"""Convert a cmp= function into a key= function"""
class K(object):
__slots__ = ['obj']
def __init__(self, obj):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
__hash__ = None
return K

View File

@ -2,21 +2,36 @@ from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
import logging import logging
from .service import Service from .service import Service
from .compat.functools import cmp_to_key
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def sort_service_dicts(services): def sort_service_dicts(services):
# Sort in dependency order # Get all services that are dependant on another.
def cmp(x, y): dependent_services = [s for s in services if s.get('links')]
x_deps_y = y['name'] in x.get('links', []) flatten_links = sum([s['links'] for s in dependent_services], [])
y_deps_x = x['name'] in y.get('links', []) # Get all services that are not linked to and don't link to others.
if x_deps_y and not y_deps_x: non_dependent_sevices = [s for s in services if s['name'] not in flatten_links and not s.get('links')]
return 1 sorted_services = []
elif y_deps_x and not x_deps_y: # Topological sort.
return -1 while dependent_services:
return 0 n = dependent_services.pop()
return sorted(services, key=cmp_to_key(cmp)) # Check if a service is dependent on itself, if so raise an error.
if n['name'] in n.get('links', []):
raise DependencyError('A service can not link to itself: %s' % n['name'])
sorted_services.append(n)
for l in n['links']:
# Get the linked service.
linked_service = next(s for s in services if l == s['name'])
# Check that there isn't a circular import between services.
if n['name'] in linked_service.get('links', []):
raise DependencyError('Circular import between %s and %s' % (n['name'], linked_service['name']))
# Check the linked service has no links and is not already in the
# sorted service list.
if not linked_service.get('links') and linked_service not in sorted_services:
sorted_services.insert(0, linked_service)
return non_dependent_sevices + sorted_services
class Project(object): class Project(object):
""" """
@ -134,3 +149,11 @@ class NoSuchService(Exception):
def __str__(self): def __str__(self):
return self.msg return self.msg
class DependencyError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg

103
tests/sort_service_test.py Normal file
View File

@ -0,0 +1,103 @@
from fig.project import sort_service_dicts, DependencyError
from . import unittest
class SortServiceTest(unittest.TestCase):
def test_sort_service_dicts_1(self):
services = [
{
'links': ['redis'],
'name': 'web'
},
{
'name': 'grunt'
},
{
'name': 'redis'
}
]
sorted_services = sort_service_dicts(services)
self.assertEqual(len(sorted_services), 3)
self.assertEqual(sorted_services[0]['name'], 'grunt')
self.assertEqual(sorted_services[1]['name'], 'redis')
self.assertEqual(sorted_services[2]['name'], 'web')
def test_sort_service_dicts_2(self):
services = [
{
'links': ['redis', 'postgres'],
'name': 'web'
},
{
'name': 'postgres',
'links': ['redis']
},
{
'name': 'redis'
}
]
sorted_services = sort_service_dicts(services)
self.assertEqual(len(sorted_services), 3)
self.assertEqual(sorted_services[0]['name'], 'redis')
self.assertEqual(sorted_services[1]['name'], 'postgres')
self.assertEqual(sorted_services[2]['name'], 'web')
def test_sort_service_dicts_circular_imports(self):
services = [
{
'links': ['redis'],
'name': 'web'
},
{
'name': 'redis',
'links': ['web']
},
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('redis', e.msg)
self.assertIn('web', e.msg)
else:
self.fail('Should have thrown an DependencyError')
def test_sort_service_dicts_circular_imports_2(self):
services = [
{
'links': ['postgres', 'redis'],
'name': 'web'
},
{
'name': 'redis',
'links': ['web']
},
{
'name': 'postgres'
}
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('redis', e.msg)
self.assertIn('web', e.msg)
else:
self.fail('Should have thrown an DependencyError')
def test_sort_service_dicts_self_imports(self):
services = [
{
'links': ['web'],
'name': 'web'
},
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('web', e.msg)
else:
self.fail('Should have thrown an DependencyError')