mirror of https://github.com/celery/kombu.git
kombu.pidbox: Process mailbox. Generic implementation of Celery's remote control commands
This commit is contained in:
parent
e3369ab793
commit
4411e3355d
|
@ -0,0 +1,255 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
Server
|
||||||
|
======
|
||||||
|
|
||||||
|
mailbox = pidbox.Mailbox("celerybeat", )
|
||||||
|
|
||||||
|
@mailbox.handler
|
||||||
|
def reload_schedule(state, **kwargs):
|
||||||
|
state.beat.reload_schedule()
|
||||||
|
state["last_reload"] = time()
|
||||||
|
|
||||||
|
@mailbox.handler
|
||||||
|
def connection_info(state, **kwargs):
|
||||||
|
return {"connection": state.connection.info()}
|
||||||
|
|
||||||
|
|
||||||
|
connection = kombu.BrokerConnection()
|
||||||
|
mailbox(connection).Node(hostname).consume()
|
||||||
|
|
||||||
|
Client
|
||||||
|
======
|
||||||
|
|
||||||
|
celerybeat = pidbox.lookup("celerybeat")
|
||||||
|
celerybeat.cast("reload_schedule")
|
||||||
|
info = celerybeat.call("connection_info", timeout=1)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
from copy import copy
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
from kombu.entity import Exchange, Queue
|
||||||
|
from kombu.messaging import Consumer, Producer
|
||||||
|
from kombu.utils import gen_unique_id, kwdict
|
||||||
|
|
||||||
|
|
||||||
|
class Node(object):
|
||||||
|
|
||||||
|
def __init__(self, hostname, state=None, channel=None, handlers=None,
|
||||||
|
mailbox=None):
|
||||||
|
self.channel = channel
|
||||||
|
self.mailbox = mailbox
|
||||||
|
self.hostname = hostname
|
||||||
|
self.state = state
|
||||||
|
if handlers is None:
|
||||||
|
handlers = {}
|
||||||
|
self.handlers = handlers
|
||||||
|
|
||||||
|
def Consumer(self, channel=None, **options):
|
||||||
|
options.setdefault("no_ack", True)
|
||||||
|
return Consumer(channel or self.channel,
|
||||||
|
[self.mailbox.get_queue(self.hostname)],
|
||||||
|
**options)
|
||||||
|
|
||||||
|
def listen(self, channel=None, callback=None):
|
||||||
|
callback = callback or self.handle_message
|
||||||
|
consumer = self.Consumer(channel=channel,
|
||||||
|
callbacks=[callback or self.handle_message])
|
||||||
|
consumer.consume()
|
||||||
|
return consumer
|
||||||
|
|
||||||
|
def reply(self, data, exchange, routing_key, **kwargs):
|
||||||
|
self.mailbox._publish_reply(data, exchange, routing_key,
|
||||||
|
channel=self.channel)
|
||||||
|
|
||||||
|
def dispatch_from_message(self, message):
|
||||||
|
message = dict(message)
|
||||||
|
method = message["method"]
|
||||||
|
destination = message.get("destination")
|
||||||
|
reply_to = message.get("reply_to")
|
||||||
|
arguments = message.get("arguments")
|
||||||
|
if not destination or self.hostname in destination:
|
||||||
|
return self.dispatch(method, arguments, reply_to)
|
||||||
|
|
||||||
|
def dispatch(self, method, arguments=None, reply_to=None):
|
||||||
|
arguments = arguments or {}
|
||||||
|
handle = reply_to and self.handle_call or self.handle_cast
|
||||||
|
try:
|
||||||
|
reply = handle(method, kwdict(arguments))
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception, exc:
|
||||||
|
reply = {"error": repr(exc)}
|
||||||
|
|
||||||
|
if reply_to:
|
||||||
|
self.reply({self.hostname: reply}, **reply_to)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
def handle_call(self, method, arguments):
|
||||||
|
return self.handle(method, arguments)
|
||||||
|
|
||||||
|
def handle_cast(self, method, arguments):
|
||||||
|
return self.handle(method, arguments)
|
||||||
|
|
||||||
|
def handler(self, fun):
|
||||||
|
self.handlers[fun.__name__] = fun
|
||||||
|
return fun
|
||||||
|
|
||||||
|
def handle(self, method, arguments={}):
|
||||||
|
return self.handlers[method](self.state, **arguments)
|
||||||
|
|
||||||
|
def handle_message(self, message_data, message):
|
||||||
|
self.dispatch_from_message(message_data)
|
||||||
|
|
||||||
|
|
||||||
|
class Mailbox(object):
|
||||||
|
node_cls = Node
|
||||||
|
exchange_fmt = "%s.pidbox"
|
||||||
|
reply_exchange_fmt = "reply.%s.pidbox"
|
||||||
|
|
||||||
|
def __init__(self, namespace, type="direct", connection=None):
|
||||||
|
self.namespace = namespace
|
||||||
|
self.connection = connection
|
||||||
|
self.type = type
|
||||||
|
self.exchange = self._get_exchange(self.namespace, self.type)
|
||||||
|
self.reply_exchange = self._get_reply_exchange(self.namespace)
|
||||||
|
|
||||||
|
def __call__(self, connection):
|
||||||
|
bound = copy(self)
|
||||||
|
bound.connection = connection
|
||||||
|
return bound
|
||||||
|
|
||||||
|
def Node(self, hostname=None, state=None, channel=None, handlers=None):
|
||||||
|
hostname = hostname or socket.gethostname()
|
||||||
|
return self.node_cls(hostname, state, channel, handlers, mailbox=self)
|
||||||
|
|
||||||
|
def call(self, destination, command, kwargs={}, timeout=None,
|
||||||
|
callback=None, channel=None):
|
||||||
|
return self._broadcast(command, kwargs, destination,
|
||||||
|
reply=True, timeout=timeout,
|
||||||
|
callback=callback,
|
||||||
|
channel=channel)
|
||||||
|
|
||||||
|
def cast(self, destination, command, kwargs={}):
|
||||||
|
return self._broadcast(command, kwargs, destination, reply=False)
|
||||||
|
|
||||||
|
def abcast(self, command, kwargs={}):
|
||||||
|
return self._broadcast(command, kwargs, reply=False)
|
||||||
|
|
||||||
|
def multi_call(self, command, kwargs={}, timeout=1,
|
||||||
|
limit=None, callback=None, channel=None):
|
||||||
|
return self._broadcast(command, kwargs, reply=True,
|
||||||
|
timeout=timeout, limit=limit,
|
||||||
|
callback=callback,
|
||||||
|
channel=channel)
|
||||||
|
|
||||||
|
def get_reply_queue(self, ticket):
|
||||||
|
return Queue("%s.%s" % (ticket, self.reply_exchange.name),
|
||||||
|
exchange=self.reply_exchange,
|
||||||
|
routing_key=ticket,
|
||||||
|
durable=False,
|
||||||
|
auto_delete=True)
|
||||||
|
|
||||||
|
def get_queue(self, hostname):
|
||||||
|
return Queue("%s.%s.pidbox" % (hostname, self.namespace),
|
||||||
|
exchange=self.exchange)
|
||||||
|
|
||||||
|
|
||||||
|
def _publish_reply(self, reply, exchange, routing_key, channel=None):
|
||||||
|
chan = channel or self.connection.channel()
|
||||||
|
try:
|
||||||
|
exchange = Exchange(exchange, exchange_type="direct",
|
||||||
|
delivery_mode="transient",
|
||||||
|
durable=False,
|
||||||
|
auto_delete=True)
|
||||||
|
producer = Producer(chan, exchange=exchange)
|
||||||
|
producer.publish(reply, routing_key=routing_key)
|
||||||
|
finally:
|
||||||
|
channel or chan.close()
|
||||||
|
|
||||||
|
def _publish(self, type, arguments, destination=None, reply_ticket=None,
|
||||||
|
channel=None):
|
||||||
|
message = {"method": type,
|
||||||
|
"arguments": arguments,
|
||||||
|
"destination": destination}
|
||||||
|
if reply_ticket:
|
||||||
|
message["reply_to"] = {"exchange": self.reply_exchange.name,
|
||||||
|
"routing_key": reply_ticket}
|
||||||
|
chan = channel or self.connection.channel()
|
||||||
|
producer = Producer(chan, exchange=self.exchange)
|
||||||
|
try:
|
||||||
|
producer.publish(message)
|
||||||
|
finally:
|
||||||
|
channel or chan.close()
|
||||||
|
|
||||||
|
def _broadcast(self, command, arguments=None, destination=None,
|
||||||
|
reply=False, timeout=1, limit=None, callback=None, channel=None):
|
||||||
|
arguments = arguments or {}
|
||||||
|
reply_ticket = reply and gen_unique_id() or None
|
||||||
|
|
||||||
|
if destination is not None and \
|
||||||
|
not isinstance(destination, (list, tuple)):
|
||||||
|
raise ValueError("destination must be a list/tuple not %s" % (
|
||||||
|
type(destination)))
|
||||||
|
|
||||||
|
# Set reply limit to number of destinations (if specificed)
|
||||||
|
if limit is None and destination:
|
||||||
|
limit = destination and len(destination) or None
|
||||||
|
|
||||||
|
chan = channel or self.connection.channel()
|
||||||
|
try:
|
||||||
|
if reply_ticket:
|
||||||
|
self.get_reply_queue(reply_ticket)(chan).declare()
|
||||||
|
|
||||||
|
self._publish(command, arguments, destination=destination,
|
||||||
|
reply_ticket=reply_ticket,
|
||||||
|
channel=chan)
|
||||||
|
|
||||||
|
if reply_ticket:
|
||||||
|
return self._collect(reply_ticket, limit=limit,
|
||||||
|
timeout=timeout,
|
||||||
|
callback=callback,
|
||||||
|
channel=chan)
|
||||||
|
finally:
|
||||||
|
channel or chan.close()
|
||||||
|
|
||||||
|
def _collect(self, ticket, limit=None, timeout=1,
|
||||||
|
callback=None, channel=None):
|
||||||
|
chan = channel or self.connection.channel()
|
||||||
|
queue = self.get_reply_queue(ticket)
|
||||||
|
consumer = Consumer(channel, [queue], no_ack=True)
|
||||||
|
responses = []
|
||||||
|
|
||||||
|
def on_message(message_data, message):
|
||||||
|
if callback:
|
||||||
|
callback(message_data)
|
||||||
|
responses.append(message_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
consumer.register_callback(on_message)
|
||||||
|
consumer.consume()
|
||||||
|
for i in limit and range(limit) or count():
|
||||||
|
try:
|
||||||
|
self.connection.drain_events(timeout=timeout)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
return responses
|
||||||
|
finally:
|
||||||
|
channel or chan.close()
|
||||||
|
|
||||||
|
def _get_exchange(self, namespace, type):
|
||||||
|
return Exchange(self.exchange_fmt % namespace,
|
||||||
|
type=type,
|
||||||
|
durable=False,
|
||||||
|
auto_delete=True,
|
||||||
|
delivery_mode="transient")
|
||||||
|
|
||||||
|
def _get_reply_exchange(self, namespace):
|
||||||
|
return Exchange(self.reply_exchange_fmt % namespace,
|
||||||
|
type="direct",
|
||||||
|
durable=False,
|
||||||
|
auto_delete=True,
|
||||||
|
delivery_mode="transient")
|
|
@ -1,4 +1,35 @@
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from uuid import UUID, uuid4, _uuid_generate_random
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
except ImportError:
|
||||||
|
ctypes = None
|
||||||
|
|
||||||
|
|
||||||
|
def gen_unique_id():
|
||||||
|
"""Generate a unique id, having - hopefully - a very small chance of
|
||||||
|
collission.
|
||||||
|
|
||||||
|
For now this is provided by :func:`uuid.uuid4`.
|
||||||
|
"""
|
||||||
|
# Workaround for http://bugs.python.org/issue4607
|
||||||
|
if ctypes and _uuid_generate_random:
|
||||||
|
buffer = ctypes.create_string_buffer(16)
|
||||||
|
_uuid_generate_random(buffer)
|
||||||
|
return str(UUID(bytes=buffer.raw))
|
||||||
|
return str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def kwdict(kwargs):
|
||||||
|
"""Make sure keyword arguments are not in unicode.
|
||||||
|
|
||||||
|
This should be fixed in newer Python versions,
|
||||||
|
see: http://bugs.python.org/issue4978.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return dict((key.encode("utf-8"), value)
|
||||||
|
for key, value in kwargs.items())
|
||||||
|
|
||||||
|
|
||||||
def maybe_list(v):
|
def maybe_list(v):
|
||||||
|
|
Loading…
Reference in New Issue