To make it easy to add new type errors and to make the existing ones have a common string pattern adjust the current type errors to contain at least the following string format: '%s' (%s) where these two places will be filled in with the object of the wrong type and the type of that object. This information is useful when analyzing the exception (by the user) to know exactly what they passed in and what type it was. This convention is not maintained where it would interpolate large text blobs (such as in binary encoding/decoding and json decoding). Change-Id: Id84b0e7ce684a543cc407b15016e77804e6f03ed
237 lines
8.4 KiB
Python
237 lines
8.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
|
|
# Copyright (C) 2013 Rackspace Hosting 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 functools
|
|
|
|
import six
|
|
|
|
from taskflow import exceptions as exc
|
|
from taskflow import logging
|
|
from taskflow.persistence import base
|
|
from taskflow.persistence import logbook
|
|
from taskflow.utils import lock_utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class _Memory(object):
|
|
"""Where the data is really stored."""
|
|
|
|
def __init__(self):
|
|
self.log_books = {}
|
|
self.flow_details = {}
|
|
self.atom_details = {}
|
|
|
|
def clear_all(self):
|
|
self.log_books.clear()
|
|
self.flow_details.clear()
|
|
self.atom_details.clear()
|
|
|
|
|
|
class _MemoryHelper(object):
|
|
"""Helper functionality for the memory backends & connections."""
|
|
|
|
def __init__(self, memory):
|
|
self._memory = memory
|
|
|
|
@staticmethod
|
|
def _fetch_clone_args(incoming):
|
|
if isinstance(incoming, (logbook.LogBook, logbook.FlowDetail)):
|
|
# We keep our own copy of the added contents of the following
|
|
# types so we don't need the clone to retain them directly...
|
|
return {
|
|
'retain_contents': False,
|
|
}
|
|
return {}
|
|
|
|
def construct(self, uuid, container):
|
|
"""Reconstructs a object from the given uuid and storage container."""
|
|
source = container[uuid]
|
|
clone_kwargs = self._fetch_clone_args(source)
|
|
clone = source['object'].copy(**clone_kwargs)
|
|
rebuilder = source.get('rebuilder')
|
|
if rebuilder:
|
|
for component in map(rebuilder, source['components']):
|
|
clone.add(component)
|
|
return clone
|
|
|
|
def merge(self, incoming, saved_info=None):
|
|
"""Merges the incoming object into the local memories copy."""
|
|
if saved_info is None:
|
|
if isinstance(incoming, logbook.LogBook):
|
|
saved_info = self._memory.log_books.setdefault(
|
|
incoming.uuid, {})
|
|
elif isinstance(incoming, logbook.FlowDetail):
|
|
saved_info = self._memory.flow_details.setdefault(
|
|
incoming.uuid, {})
|
|
elif isinstance(incoming, logbook.AtomDetail):
|
|
saved_info = self._memory.atom_details.setdefault(
|
|
incoming.uuid, {})
|
|
else:
|
|
raise TypeError("Unknown how to merge '%s' (%s)"
|
|
% (incoming, type(incoming)))
|
|
try:
|
|
saved_info['object'].merge(incoming)
|
|
except KeyError:
|
|
clone_kwargs = self._fetch_clone_args(incoming)
|
|
saved_info['object'] = incoming.copy(**clone_kwargs)
|
|
if isinstance(incoming, logbook.LogBook):
|
|
flow_details = saved_info.setdefault('components', set())
|
|
if 'rebuilder' not in saved_info:
|
|
saved_info['rebuilder'] = functools.partial(
|
|
self.construct, container=self._memory.flow_details)
|
|
for flow_detail in incoming:
|
|
flow_details.add(self.merge(flow_detail))
|
|
elif isinstance(incoming, logbook.FlowDetail):
|
|
atom_details = saved_info.setdefault('components', set())
|
|
if 'rebuilder' not in saved_info:
|
|
saved_info['rebuilder'] = functools.partial(
|
|
self.construct, container=self._memory.atom_details)
|
|
for atom_detail in incoming:
|
|
atom_details.add(self.merge(atom_detail))
|
|
return incoming.uuid
|
|
|
|
|
|
class MemoryBackend(base.Backend):
|
|
"""A in-memory (non-persistent) backend.
|
|
|
|
This backend writes logbooks, flow details, and atom details to in-memory
|
|
dictionaries and retrieves from those dictionaries as needed.
|
|
"""
|
|
def __init__(self, conf=None):
|
|
super(MemoryBackend, self).__init__(conf)
|
|
self._memory = _Memory()
|
|
self._helper = _MemoryHelper(self._memory)
|
|
self._lock = lock_utils.ReaderWriterLock()
|
|
|
|
def _construct_from(self, container):
|
|
return dict((uuid, self._helper.construct(uuid, container))
|
|
for uuid in six.iterkeys(container))
|
|
|
|
@property
|
|
def log_books(self):
|
|
with self._lock.read_lock():
|
|
return self._construct_from(self._memory.log_books)
|
|
|
|
@property
|
|
def flow_details(self):
|
|
with self._lock.read_lock():
|
|
return self._construct_from(self._memory.flow_details)
|
|
|
|
@property
|
|
def atom_details(self):
|
|
with self._lock.read_lock():
|
|
return self._construct_from(self._memory.atom_details)
|
|
|
|
def get_connection(self):
|
|
return Connection(self)
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
class Connection(base.Connection):
|
|
"""A connection to an in-memory backend."""
|
|
|
|
def __init__(self, backend):
|
|
self._backend = backend
|
|
self._helper = backend._helper
|
|
self._memory = backend._memory
|
|
self._lock = backend._lock
|
|
|
|
def upgrade(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
pass
|
|
|
|
@property
|
|
def backend(self):
|
|
return self._backend
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def clear_all(self):
|
|
with self._lock.write_lock():
|
|
self._memory.clear_all()
|
|
|
|
def destroy_logbook(self, book_uuid):
|
|
with self._lock.write_lock():
|
|
try:
|
|
# Do the same cascading delete that the sql layer does.
|
|
book_info = self._memory.log_books.pop(book_uuid)
|
|
except KeyError:
|
|
raise exc.NotFound("No logbook found with uuid '%s'"
|
|
% book_uuid)
|
|
else:
|
|
while book_info['components']:
|
|
flow_uuid = book_info['components'].pop()
|
|
flow_info = self._memory.flow_details.pop(flow_uuid)
|
|
while flow_info['components']:
|
|
atom_uuid = flow_info['components'].pop()
|
|
self._memory.atom_details.pop(atom_uuid)
|
|
|
|
def update_atom_details(self, atom_detail):
|
|
with self._lock.write_lock():
|
|
try:
|
|
atom_info = self._memory.atom_details[atom_detail.uuid]
|
|
return self._helper.construct(
|
|
self._helper.merge(atom_detail, saved_info=atom_info),
|
|
self._memory.atom_details)
|
|
except KeyError:
|
|
raise exc.NotFound("No atom details found with uuid '%s'"
|
|
% atom_detail.uuid)
|
|
|
|
def update_flow_details(self, flow_detail):
|
|
with self._lock.write_lock():
|
|
try:
|
|
flow_info = self._memory.flow_details[flow_detail.uuid]
|
|
return self._helper.construct(
|
|
self._helper.merge(flow_detail, saved_info=flow_info),
|
|
self._memory.flow_details)
|
|
except KeyError:
|
|
raise exc.NotFound("No flow details found with uuid '%s'"
|
|
% flow_detail.uuid)
|
|
|
|
def save_logbook(self, book):
|
|
with self._lock.write_lock():
|
|
return self._helper.construct(self._helper.merge(book),
|
|
self._memory.log_books)
|
|
|
|
def get_logbook(self, book_uuid):
|
|
with self._lock.read_lock():
|
|
try:
|
|
return self._helper.construct(book_uuid,
|
|
self._memory.log_books)
|
|
except KeyError:
|
|
raise exc.NotFound("No logbook found with uuid '%s'"
|
|
% book_uuid)
|
|
|
|
def get_logbooks(self):
|
|
# Don't hold locks while iterating...
|
|
with self._lock.read_lock():
|
|
book_uuids = set(six.iterkeys(self._memory.log_books))
|
|
for book_uuid in book_uuids:
|
|
try:
|
|
with self._lock.read_lock():
|
|
book = self._helper.construct(book_uuid,
|
|
self._memory.log_books)
|
|
yield book
|
|
except KeyError:
|
|
pass
|