solar/solar/orchestration/graph.py

231 lines
6.5 KiB
Python

# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
import uuid
from collections import Counter
import networkx as nx
from solar.dblayer.model import ModelMeta
from solar.dblayer.solar_models import Task
from solar import errors
from solar.orchestration.traversal import states
from solar import utils
def save_graph(graph):
for n in nx.topological_sort(graph):
values = {'name': n, 'execution': graph.graph['uid']}
values.update(graph.node[n])
t = Task.new(values)
graph.node[n]['task'] = t
for pred in graph.predecessors(n):
pred_task = graph.node[pred]['task']
t.parents.add(pred_task)
pred_task.save()
t.save_lazy()
def set_states(uid, tasks):
plan = get_graph(uid)
for t in tasks:
if t not in plan.node:
raise Exception("No task %s in plan %s", t, uid)
plan.node[t].status = states.NOOP.name
plan.node[t].save_lazy()
def get_task_by_name(dg, task_name):
return next(t for t in dg.nodes() if t.name == task_name)
def get_graph(uid):
mdg = nx.MultiDiGraph()
mdg.graph['uid'] = uid
mdg.graph['name'] = uid.split(':')[0]
tasks_by_uid = {t.key: t for t
in Task.multi_get(Task.execution.filter(uid))}
mdg.add_nodes_from(tasks_by_uid.values())
mdg.add_edges_from([(tasks_by_uid[parent], task) for task in mdg.nodes()
for parent in task.parents.all()])
return mdg
def longest_path_time(graph):
"""We are not interested in the path itself, just get the start
of execution and the end of it.
"""
start = float('inf')
end = float('-inf')
for n in graph:
node_start = n.start_time
node_end = n.end_time
if int(node_start) == 0 or int(node_end) == 0:
continue
if node_start < start:
start = node_start
if node_end > end:
end = node_end
return max(end - start, 0.0)
def total_delta(graph):
delta = 0.0
for n in graph:
node_start = n.start_time
node_end = n.end_time
if int(node_start) == 0 or int(node_end) == 0:
continue
delta += node_end - node_start
return delta
get_plan = get_graph
def assign_weights_nested(dg):
"""Based on number of childs assign weights that will be
used later for scheduling.
"""
#: NOTE reverse(copy=False) swaps successors and predecessors
# on same copy of graph, thus before returning it - reverse it back
reversed_graph = dg.reverse(copy=False)
for task in nx.topological_sort(reversed_graph):
task.weight = sum([t.weight + 1 for t
in reversed_graph.predecessors(task)])
task.save_lazy()
return reversed_graph.reverse(copy=False)
def parse_plan(plan_path):
"""parses yaml definition and returns graph"""
plan = utils.yaml_load(plan_path)
dg = nx.MultiDiGraph()
dg.graph['name'] = plan['name']
for task in plan['tasks']:
defaults = {
'status': 'PENDING',
'errmsg': '',
}
defaults.update(task['parameters'])
dg.add_node(
task['uid'], **defaults)
for v in task.get('before', ()):
dg.add_edge(task['uid'], v)
for u in task.get('after', ()):
dg.add_edge(u, task['uid'])
return dg
def create_plan_from_graph(dg):
dg.graph['uid'] = "{0}:{1}".format(dg.graph['name'], str(uuid.uuid4()))
# FIXME change save_graph api to return new graph with Task objects
# included
save_graph(dg)
ModelMeta.save_all_lazy()
return get_graph(dg.graph['uid'])
def show(uid):
dg = get_graph(uid)
result = {}
tasks = []
result['uid'] = dg.graph['uid']
result['name'] = dg.graph['name']
for task in nx.topological_sort(dg):
tasks.append(
{'uid': task.name,
'parameters': task.to_dict(),
'before': dg.successors(task),
'after': dg.predecessors(task)
})
result['tasks'] = tasks
return utils.yaml_dump(result)
def create_plan(plan_path):
return create_plan_from_graph(parse_plan(plan_path))
def reset_by_uid(uid, state_list=None):
dg = get_graph(uid)
return reset(dg, state_list=state_list)
def reset(graph, state_list=None):
for n in graph:
if state_list is None or n.status in state_list:
n.status = states.PENDING.name
n.start_time = 0.0
n.end_time = 0.0
n.save_lazy()
def reset_filtered(uid):
reset_by_uid(uid, state_list=[states.SKIPPED.name, states.NOOP.name])
def report_progress(uid):
return report_progress_graph(get_graph(uid))
def report_progress_graph(dg):
tasks = []
report = {
'total_time': longest_path_time(dg),
'total_delta': total_delta(dg),
'tasks': tasks}
# FIXME just return topologically sorted list of tasks
for task in nx.topological_sort(dg):
tasks.append([
task.name,
task.status,
task.errmsg,
task.start_time,
task.end_time])
return report
def wait_finish(uid, timeout):
"""Check if graph is finished
Will return when no PENDING or INPROGRESS otherwise yields summary
"""
start_time = time.time()
while start_time + timeout >= time.time():
dg = get_graph(uid)
summary = Counter()
summary.update({s.name: 0 for s in states})
summary.update([task.status for task in dg.nodes()])
yield summary
if summary[states.PENDING.name] + summary[states.INPROGRESS.name] == 0:
return
else:
# on db backends with snapshot isolation level and higher
# updates wont be visible after start of transaction,
# in order to report state correctly we will "refresh" transaction
ModelMeta.session_end()
ModelMeta.session_start()
else:
raise errors.ExecutionTimeout(
'Run %s wasnt able to finish' % uid)