Files
deb-python-taskflow/taskflow/storage.py
Joshua Harlow 45d350e80d Move toward using a backend+connection model
Instead of having a pretty restrictive module
based api for saving logbook objects it is much
more friendly and extensible to move toward a more
ceilometer-influenced engine and connection based
storage backend using stevedore to do the backend
loading instead of a custom registration/fetching
mechanism. This allows us to provide a base object
oriented backend api that can be easily inherited
from to allow for customized & pluggable backend
storage modules.

Implements blueprint stevedore-based-backends
Implements blueprint ceilometer-influenced-backends

Change-Id: Ib5868d3d9018b7aa1a3354858dcb90ca1a04055d
2013-09-12 20:01:06 +04:00

209 lines
7.7 KiB
Python

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (C) 2013 Yahoo! Inc. All Rights Reserved.
#
# 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 contextlib
from taskflow import exceptions
from taskflow.openstack.common import uuidutils
from taskflow.persistence import logbook
from taskflow import states
from taskflow.utils import threading_utils
STATES_WITH_RESULTS = (states.SUCCESS, states.REVERTING, states.FAILURE)
class Storage(object):
"""Interface between engines and logbook
This class provides simple interface to save task details and
results to persistence layer for use by engines.
"""
injector_name = '_TaskFlow_INJECTOR'
def __init__(self, flow_detail, backend=None):
self._result_mappings = {}
self._reverse_mapping = {}
self._backend = backend
self._flowdetail = flow_detail
def _with_connection(self, functor, *args, **kwargs):
if self._backend is None:
return
with contextlib.closing(self._backend.get_connection()) as conn:
functor(conn, *args, **kwargs)
def add_task(self, uuid, task_name):
"""Add the task to storage
Task becomes known to storage by that name and uuid.
Task state is set to PENDING.
"""
# TODO(imelnikov): check that task with same uuid or
# task name does not exist
td = logbook.TaskDetail(name=task_name, uuid=uuid)
td.state = states.PENDING
self._flowdetail.add(td)
self._with_connection(self._save_flow_detail)
self._with_connection(self._save_task_detail, task_detail=td)
def _save_flow_detail(self, conn):
# NOTE(harlowja): we need to update our contained flow detail if
# the result of the update actually added more (aka another process
# added item to the flow detail).
self._flowdetail.update(conn.update_flow_details(self._flowdetail))
def get_uuid_by_name(self, task_name):
"""Get uuid of task with given name"""
td = self._flowdetail.find_by_name(task_name)
if td is not None:
return td.uuid
else:
raise exceptions.NotFound("Unknown task name: %r" % task_name)
def _taskdetail_by_uuid(self, uuid):
td = self._flowdetail.find(uuid)
if td is None:
raise exceptions.NotFound("Unknown task: %r" % uuid)
return td
def _save_task_detail(self, conn, task_detail):
# NOTE(harlowja): we need to update our contained task detail if
# the result of the update actually added more (aka another process
# is also modifying the task detail).
task_detail.update(conn.update_task_details(task_detail))
def set_task_state(self, uuid, state):
"""Set task state"""
td = self._taskdetail_by_uuid(uuid)
td.state = state
self._with_connection(self._save_task_detail, task_detail=td)
def get_task_state(self, uuid):
"""Get state of task with given uuid"""
return self._taskdetail_by_uuid(uuid).state
def save(self, uuid, data, state=states.SUCCESS):
"""Put result for task with id 'uuid' to storage"""
td = self._taskdetail_by_uuid(uuid)
td.state = state
td.results = data
self._with_connection(self._save_task_detail, task_detail=td)
def get(self, uuid):
"""Get result for task with id 'uuid' to storage"""
td = self._taskdetail_by_uuid(uuid)
if td.state not in STATES_WITH_RESULTS:
raise exceptions.NotFound("Result for task %r is not known" % uuid)
return td.results
def reset(self, uuid, state=states.PENDING):
"""Remove result for task with id 'uuid' from storage"""
td = self._taskdetail_by_uuid(uuid)
td.results = None
td.state = state
self._with_connection(self._save_task_detail, task_detail=td)
def inject(self, pairs):
"""Add values into storage
This method should be used by job in order to put flow parameters
into storage and put it to action.
"""
pairs = dict(pairs)
injector_uuid = uuidutils.generate_uuid()
self.add_task(injector_uuid, self.injector_name)
self.save(injector_uuid, pairs)
for name in pairs.iterkeys():
entries = self._reverse_mapping.setdefault(name, [])
entries.append((injector_uuid, name))
def set_result_mapping(self, uuid, mapping):
"""Set mapping for naming task results
The result saved with given uuid would be accessible by names
defined in mapping. Mapping is a dict name => index. If index
is None, the whole result will have this name; else, only
part of it, result[index].
"""
if not mapping:
return
self._result_mappings[uuid] = mapping
for name, index in mapping.iteritems():
entries = self._reverse_mapping.setdefault(name, [])
entries.append((uuid, index))
def fetch(self, name):
"""Fetch named task result"""
try:
indexes = self._reverse_mapping[name]
except KeyError:
raise exceptions.NotFound("Name %r is not mapped" % name)
# Return the first one that is found.
for uuid, index in indexes:
try:
result = self.get(uuid)
if index is None:
return result
else:
return result[index]
except exceptions.NotFound:
# NOTE(harlowja): No result was found for the given uuid.
pass
except (KeyError, IndexError, TypeError):
# NOTE(harlowja): The result that the uuid returned can not be
# accessed in the manner that the index is requesting. Perhaps
# the result is a dictionary-like object and that key does
# not exist (key error), or the result is a tuple/list and a
# non-numeric key is being requested (index error), or there
# was no result and an attempt to index into None is being
# requested (type error).
pass
raise exceptions.NotFound("Unable to find result %r" % name)
def fetch_all(self):
"""Fetch all named task results known so far
Should be used for debugging and testing purposes mostly.
"""
result = {}
for name in self._reverse_mapping:
try:
result[name] = self.fetch(name)
except exceptions.NotFound:
pass
return result
def fetch_mapped_args(self, args_mapping):
"""Fetch arguments for the task using arguments mapping"""
return dict((key, self.fetch(name))
for key, name in args_mapping.iteritems())
def set_flow_state(self, state):
"""Set flowdetails state and save it"""
self._flowdetail.state = state
self._with_connection(self._save_flow_detail)
def get_flow_state(self):
"""Set state from flowdetails"""
return self._flowdetail.state
class ThreadSafeStorage(Storage):
__metaclass__ = threading_utils.ThreadSafeMeta