barbican/barbican/queue/server.py

247 lines
8.5 KiB
Python

# Copyright (c) 2013-2014 Rackspace, 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.
"""
Server-side (i.e. worker side) classes and logic.
"""
import datetime
import functools
try:
import newrelic.agent
from newrelic.api import application
newrelic_loaded = True
except ImportError:
newrelic_loaded = False
from oslo_service import service
from barbican.common import utils
from barbican.model import models
from barbican.model import repositories
from barbican import queue
from barbican.tasks import common
from barbican.tasks import resources
if newrelic_loaded:
newrelic.agent.initialize('/etc/newrelic/newrelic.ini')
LOG = utils.getLogger(__name__)
# Maps the common/shared RetryTasks (returned from lower-level business logic
# and plugin processing) to top-level RPC tasks in the Tasks class below.
MAP_RETRY_TASKS = {}
def find_function_name(func, if_no_name=None):
"""Returns pretty-formatted function name."""
return getattr(func, '__name__', if_no_name)
def retryable_order(fn):
"""Provides retry/scheduling support to Order-related tasks."""
@functools.wraps(fn)
def wrapper(method_self, *args, **kwargs):
result = fn(method_self, *args, **kwargs)
retry_rpc_method = schedule_order_retry_tasks(
fn, result, *args, **kwargs)
if retry_rpc_method:
LOG.info("Scheduled RPC method for retry: '%s'", retry_rpc_method)
else:
LOG.info("Task '%s' did not have to be retried",
find_function_name(fn, if_no_name='???'))
return wrapper
def transactional(fn):
"""Provides request-scoped database transaction support to tasks."""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
fn_name = find_function_name(fn, if_no_name='???')
if not queue.is_server_side():
# Non-server mode directly invokes tasks.
fn(*args, **kwargs)
LOG.info("Completed worker task: '%s'", fn_name)
else:
# Manage session/transaction.
try:
fn(*args, **kwargs)
repositories.commit()
LOG.info("Completed worker task (post-commit): '%s'", fn_name)
except Exception:
"""NOTE: Wrapped functions must process with care!
Exceptions that reach here will revert the entire transaction,
including any updates made to entities such as setting error
codes and error messages.
"""
LOG.exception("Problem seen processing worker task: '%s'",
fn_name
)
repositories.rollback()
finally:
repositories.clear()
return wrapper
def monitored(fn): # pragma: no cover
"""Provides monitoring capabilities for task methods."""
# TODO(jvrbanac): Figure out how we should test third-party monitoring
# Support NewRelic Monitoring
if newrelic_loaded:
# Create a NewRelic app instance
app = application.application_instance()
def newrelic_wrapper(*args, **kwargs):
# Resolve real name since decorators are wrapper the method
if len(args) > 0 and hasattr(args[0], fn.__name__):
cls = type(args[0])
task_name = '{0}:{1}.{2}'.format(
cls.__module__,
cls.__name__,
fn.__name__
)
else:
task_name = newrelic.agent.callable_name(fn)
# Execute task under a monitored context
with newrelic.agent.BackgroundTask(app, task_name):
fn(*args, **kwargs)
return newrelic_wrapper
return fn
def schedule_order_retry_tasks(
invoked_task, retry_result, context, *args, **kwargs):
"""Schedules an Order-related task for retry.
:param invoked_task: The RPC method that was just invoked.
:param retry_result: A :class:`FollowOnProcessingStatusDTO` if follow-on
processing (such as retrying this or another task) is
required, otherwise None indicates no such follow-on
processing is required.
:param context: Queue context, not used.
:param order_id: ID of the Order entity the task to retry is for.
:param args: List of arguments passed in to the just-invoked task.
:param kwargs: Dict of arguments passed in to the just-invoked task.
:return: Returns the RPC task method scheduled for a retry, None if no RPC
task was scheduled.
"""
retry_rpc_method = None
order_id = kwargs.get('order_id')
if not retry_result or not order_id:
pass
elif common.RetryTasks.INVOKE_SAME_TASK == retry_result.retry_task:
if invoked_task:
retry_rpc_method = find_function_name(invoked_task)
else:
retry_rpc_method = MAP_RETRY_TASKS.get(retry_result.retry_task)
if retry_rpc_method:
LOG.debug(
'Scheduling RPC method for retry: {0}'.format(retry_rpc_method))
date_to_retry_at = datetime.datetime.utcnow() + datetime.timedelta(
milliseconds=retry_result.retry_msec)
retry_model = models.OrderRetryTask()
retry_model.order_id = order_id
retry_model.retry_task = retry_rpc_method
retry_model.retry_at = date_to_retry_at
retry_model.retry_args = args
retry_model.retry_kwargs = kwargs
retry_model.retry_count = 0
retry_repo = repositories.get_order_retry_tasks_repository()
retry_repo.create_from(retry_model)
return retry_rpc_method
class Tasks(object):
"""Tasks that can be invoked asynchronously in Barbican.
Only place task methods and implementations on this class, as they can be
called directly from the client side for non-asynchronous standalone
single-node operation.
If a new method is added that can be retried, please also add its method
name to MAP_RETRY_TASKS above.
The TaskServer class below extends this class to implement a worker-side
server utilizing Oslo messaging's RPC server. This RPC server can invoke
methods on itself, which include the methods in this class.
"""
@monitored
@transactional
@retryable_order
def process_type_order(self, context, order_id, project_id, request_id):
"""Process TypeOrder."""
message = "Processing type order: order ID is '%(order)s' and " \
"request ID is '%(request)s'"
LOG.info(message, {'order': order_id, 'request': request_id})
return resources.BeginTypeOrder().process_and_suppress_exceptions(
order_id, project_id)
class TaskServer(Tasks, service.Service):
"""Server to process asynchronous tasking from Barbican API nodes.
This server is an Oslo service that exposes task methods that can
be invoked from the Barbican API nodes. It delegates to an Oslo
RPC messaging server to invoke methods asynchronously on this class.
Since this class also extends the Tasks class above, its task-based
methods are hence available to the RPC messaging server.
"""
def __init__(self):
super(TaskServer, self).__init__()
# Setting up db engine to avoid lazy initialization
repositories.setup_database_engine_and_factory()
# This property must be defined for the 'endpoints' specified below,
# as the oslo_messaging RPC server will ask for it.
self.target = queue.get_target()
# Create an oslo RPC server, that calls back on to this class
# instance to invoke tasks, such as 'process_order()' on the
# extended Tasks class above.
self._server = queue.get_server(target=self.target,
endpoints=[self])
def start(self):
LOG.info("Starting the TaskServer")
self._server.start()
super(TaskServer, self).start()
def stop(self):
LOG.info("Halting the TaskServer")
super(TaskServer, self).stop()
self._server.stop()