PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/diesel/convoy/__init__.py

http://github.com/jamwt/diesel
Python | 361 lines | 320 code | 40 blank | 1 comment | 67 complexity | 261b438d30d090832405fb61a75baa1a MD5 | raw file
  1. from collections import defaultdict
  2. from uuid import uuid4
  3. from time import time
  4. from random import choice
  5. import operator as op
  6. from palm.palm import ProtoBase
  7. from functools import partial
  8. from diesel import quickstart, Thunk, sleep, log, fork
  9. from diesel.util.queue import Queue, first
  10. from diesel.logmod import LOGLVL_DEBUG
  11. from .convoy_env_palm import MessageResponse, MessageEnvelope
  12. from .consensus.server import run_server as run_consensus_server
  13. from .consensus.client import (ConvoyNameService, ConsensusSet,
  14. ConvoySetFailed, ConvoySetTimeout,
  15. ConvoyWaitDone)
  16. from .messagenet import (me, ConvoyService,
  17. MESSAGE_RES, MESSAGE_OUT,
  18. host_loop)
  19. class ConvoyRemoteException(object):
  20. def __init__(self, s):
  21. self.exc_desc = s
  22. class ConvoyRemoteNull(object):
  23. pass
  24. class ConvoyRemoteResult(object):
  25. def __init__(self, i):
  26. self.i = i
  27. @property
  28. def single(self):
  29. return self.i[0]
  30. def __iter__(self):
  31. return self.i
  32. def __len__(self):
  33. return len(self.i)
  34. class ConvoyRemoteError(Exception): pass
  35. class ConvoyTimeoutError(Exception): pass
  36. class Convoy(object):
  37. def __init__(self):
  38. self.routes = defaultdict(set) # message name to host
  39. self.local_handlers = {}
  40. self.enabled_handlers = {}
  41. self.classes = {}
  42. self.host_queues = {}
  43. self.run_nameserver = None
  44. self.role_messages = defaultdict(list)
  45. self.roles = set()
  46. self.roles_wanted = set()
  47. self.roles_owned = set()
  48. self.role_clocks = {}
  49. self.role_by_name = {}
  50. self.incoming = Queue()
  51. self.pending = {}
  52. self.rpc_waits = {}
  53. self.table_changes = Queue()
  54. def run_with_nameserver(self, myns, nameservers, *objs):
  55. self.run_nameserver = myns
  56. self.run(nameservers, *objs)
  57. def run(self, nameservers, *objs):
  58. nameservers = [(h, int(p))
  59. for h, p in (i.split(':')
  60. for i in nameservers)]
  61. runem = []
  62. if self.run_nameserver:
  63. runem.append(
  64. Thunk(lambda: run_consensus_server(self.run_nameserver, nameservers)))
  65. runem.append(self)
  66. handler_functions = dict((v, k) for k, v in self.local_handlers.iteritems())
  67. final_o = []
  68. for o in objs:
  69. if type(o.__class__) is ConvoyRegistrar:
  70. r = o.__class__
  71. self.roles_wanted.add(r)
  72. for m in self.role_messages[r]:
  73. assert m not in self.local_handlers, \
  74. "cannot add two instances for same role/message"
  75. self.local_handlers[m] = \
  76. getattr(o, 'handle_' + m)
  77. else:
  78. final_o.append(o)
  79. self.ns = ConvoyNameService(nameservers)
  80. runem.append(self.ns)
  81. runem.append(self.deliver)
  82. runem.extend(final_o)
  83. runem.append(ConvoyService())
  84. quickstart(*runem)
  85. def __call__(self):
  86. assert me.id
  87. should_process = self.roles
  88. rlog = log.sublog("convoy-resolver", LOGLVL_DEBUG)
  89. while True:
  90. for r in should_process:
  91. if r in self.roles_wanted:
  92. resp = self.ns.add(r.name(), me.id, r.limit)
  93. ans = None
  94. if type(resp) == ConsensusSet:
  95. self.roles_owned.add(r)
  96. ans = resp
  97. else:
  98. if r in self.roles_owned:
  99. self.roles_owned.remove(r)
  100. if resp.set:
  101. ans = resp.set
  102. else:
  103. ans = self.ns.lookup(r.name())
  104. if ans:
  105. self.role_clocks[r.name()] = ans.clock
  106. for m in self.role_messages[r]:
  107. self.routes[m] = ans.members
  108. if should_process:
  109. self.log_resolution_table(rlog, should_process)
  110. self.table_changes.put(None)
  111. wait_result = self.ns.wait(5, self.role_clocks)
  112. if type(wait_result) == ConvoyWaitDone:
  113. should_process = set([self.role_by_name[wait_result.key]])
  114. else:
  115. should_process = set()
  116. self.ns.alive()
  117. def log_resolution_table(self, rlog, processed):
  118. rlog.debug("======== diesel/convoy routing table updates ========")
  119. rlog.debug(" ")
  120. for p in processed:
  121. rlog.debug(" %s [%s]" %
  122. (p.name(),
  123. ', '.join(self.role_messages[p])))
  124. if self.role_messages:
  125. hosts = self.routes[self.role_messages[p][0]]
  126. for h in hosts:
  127. rlog.debug(" %s %s" % (
  128. '*' if h == me.id else '-',
  129. h))
  130. def register(self, mod):
  131. for name in dir(mod):
  132. v = getattr(mod, name)
  133. if type(v) is type and issubclass(v, ProtoBase):
  134. self.classes[v.__name__] = v
  135. def add_target_role(self, o):
  136. self.roles.add(o)
  137. self.role_by_name[o.name()] = o
  138. for k, v in o.__dict__.iteritems():
  139. if k.startswith("handle_") and callable(v):
  140. handler_for = k.split("_", 1)[-1]
  141. assert handler_for in self.classes, "protobuf class not recognized; register() the module"
  142. self.role_messages[o].append(handler_for)
  143. def host_specific_send(self, host, msg, typ, transport_cb):
  144. if host not in self.host_queues:
  145. q = Queue()
  146. fork(host_loop, host, q)
  147. self.host_queues[host] = q
  148. self.host_queues[host].put((msg, typ, transport_cb))
  149. def local_dispatch(self, env):
  150. if env.type not in self.classes:
  151. self.host_specific_send(env.node_id,
  152. MessageResponse(in_response_to=env.req_id,
  153. result=MessageResponse.REFUSED,
  154. error_message="cannot handle type"),
  155. MESSAGE_RES, None)
  156. elif me.id not in self.routes[env.type]:
  157. # use routes, balance, etc
  158. self.host_specific_send(env.node_id,
  159. MessageResponse(in_response_to=env.req_id,
  160. delivered=MessageResponse.REFUSED,
  161. error_message="do not own route"),
  162. MESSAGE_RES, None)
  163. else:
  164. inst = self.classes[env.type](env.body)
  165. r = self.local_handlers[env.type]
  166. sender = ConvoySender(env)
  167. back = MessageResponse(in_response_to=env.req_id,
  168. delivered=MessageResponse.ACCEPTED)
  169. self.host_specific_send(env.node_id, back,
  170. MESSAGE_RES, None)
  171. try:
  172. r(sender, inst)
  173. except Exception, e:
  174. s = str(e)
  175. back.result = MessageResponse.EXCEPTION
  176. back.error_message = s
  177. raise
  178. else:
  179. if sender.responses:
  180. back.result = MessageResponse.RESULT
  181. back.responses.extend(sender.responses)
  182. else:
  183. back.result = MessageResponse.NULL
  184. if env.wants_result:
  185. back.delivered = MessageResponse.FINISHED
  186. self.host_specific_send(env.node_id, back,
  187. MESSAGE_RES, None)
  188. def local_response(self, result):
  189. id = result.in_response_to
  190. if result.delivered == MessageResponse.REFUSED:
  191. self.retry(id)
  192. elif result.delivered == MessageResponse.ACCEPTED:
  193. if id in self.pending:
  194. del self.pending[id]
  195. elif result.delivered == MessageResponse.FINISHED:
  196. if id in self.rpc_waits:
  197. q = self.rpc_waits.pop(id)
  198. if result.result == MessageResponse.EXCEPTION:
  199. resp = ConvoyRemoteException(result.error_message)
  200. elif result.result == MessageResponse.NULL:
  201. resp = ConvoyRemoteNull()
  202. elif result.result == MessageResponse.RESULT:
  203. res = [self.classes[m.type](m.body)
  204. for m in result.responses]
  205. resp = ConvoyRemoteResult(res)
  206. else:
  207. assert 0
  208. q.put(resp)
  209. def send(self, m, timeout=10):
  210. self.incoming.put(Delivery(m, timeout))
  211. def broadcast(self, m):
  212. pass
  213. def rpc(self, m, timeout=10):
  214. q = Queue()
  215. self.incoming.put(Delivery(m, timeout, rqueue=q))
  216. ev, res = first(sleep=timeout, waits=[q])
  217. if ev == q:
  218. if res == ConvoyRemoteException:
  219. raise ConvoyRemoteError(res.exc_desc)
  220. if res == ConvoyRemoteNull:
  221. return None
  222. return res
  223. else:
  224. raise ConvoyTimeoutError("No response from a " +
  225. ("consensus remote within %ss timeout period" % timeout))
  226. def retry(self, id):
  227. if id in self.pending:
  228. next = self.pending.pop(id)
  229. self.incoming.put(next)
  230. def deliver(self):
  231. deferred = []
  232. srg = self.routes.get
  233. empty = set()
  234. sorter = op.attrgetter("reschedule_at")
  235. while True:
  236. wait = (1.0 if not deferred else
  237. deferred[-1].remaining)
  238. r, next = first(waits=[self.incoming,
  239. self.table_changes], sleep=wait)
  240. if r == self.incoming:
  241. if next.rqueue:
  242. self.rpc_waits[next.id] = next.rqueue
  243. hosts = srg(next.target, empty)
  244. potentials = hosts - next.hosts_tried
  245. if not potentials:
  246. next.reschedule()
  247. deferred.append(next)
  248. else:
  249. host = choice(list(potentials))
  250. next.hosts_tried.add(host)
  251. self.pending[next.id] = next
  252. self.host_specific_send(host, next.env,
  253. MESSAGE_OUT,
  254. partial(self.retry, next.id))
  255. deferred.sort(key=sorter, reverse=True)
  256. t = time()
  257. while deferred and deferred[-1].due(t):
  258. i = deferred.pop()
  259. if not i.expired(t):
  260. self.incoming.put(i)
  261. class Delivery(object):
  262. def __init__(self, m, timeout, rqueue=None, broadcast=False):
  263. self.id = str(uuid4())
  264. self.target = m.__class__.__name__
  265. self.timeout = time() + timeout
  266. self.rqueue = rqueue
  267. self.hosts_tried = set()
  268. self.reschedule_at = None
  269. self.reschedule_interval = 0.2
  270. self.m = m
  271. self.broadcast = broadcast
  272. self.env = MessageEnvelope(
  273. body=m.dumps(),
  274. type=self.target,
  275. req_id=self.id,
  276. node_id=me.id,
  277. wants_result=bool(rqueue))
  278. def due(self, t):
  279. return t >= self.reschedule_at
  280. def reschedule(self):
  281. self.reschedule_at = min(
  282. time() + self.reschedule_interval,
  283. self.timeout)
  284. self.reschedule_interval *= 2
  285. self.hosts_tried = set()
  286. @property
  287. def remaining(self):
  288. return max(0, self.reschedule_at
  289. - time())
  290. def expired(self, t):
  291. return t >= self.timeout
  292. class ConvoySender(object):
  293. def __init__(self, env):
  294. self.from_host = env.node_id
  295. self.type = env.type
  296. self.req_id = env.req_id
  297. self.responses = []
  298. def respond(self, m):
  299. env = MessageEnvelope(body=m.dumps(),
  300. type=m.__class__.__name__,
  301. req_id='',
  302. node_id='',
  303. wants_result=False)
  304. self.responses.append(env)
  305. convoy = Convoy()
  306. class ConvoyRegistrar(type):
  307. def __new__(*args):
  308. t = type.__new__(*args)
  309. if t.__name__ != 'ConvoyRole':
  310. convoy.add_target_role(t)
  311. return t
  312. class ConvoyRole(object):
  313. __metaclass__ = ConvoyRegistrar
  314. limit = 0
  315. @classmethod
  316. def name(cls):
  317. return cls.__name__