request_handler.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import sys
  2. import modelverse_kernel.primitives as primitive_functions
  3. import modelverse_kernel.jit as jit
  4. from collections import defaultdict
  5. class KnownRequestHandled(Exception):
  6. """An exception that signifies that a known request was handled."""
  7. pass
  8. class UnhandledRequestHandlerException(Exception):
  9. """The type of exception that is thrown when the request handler encounters an
  10. unhandled exception."""
  11. def __init__(self, inner_exception, stack_trace):
  12. import traceback
  13. Exception.__init__(
  14. self,
  15. """The request handler encountered an unknown exception.\n
  16. Inner exception: %s\n
  17. """ % (traceback.format_exc()))
  18. self.inner_exception = inner_exception
  19. self.stack_trace = stack_trace
  20. class RequestHandler(object):
  21. """A type of object that intercepts logic-related Modelverse requests, and
  22. forwards Modelverse state requests."""
  23. def __init__(self):
  24. # generator_stack is a stack of GeneratorStackEntry values.
  25. self.generator_stack = []
  26. # exception_handlers is a stack of
  27. # (generator_stack index, [(exception type, handler function)])
  28. # tuples.
  29. self.produce_stack_trace = True
  30. self.handlers = {
  31. 'CALL' : self.execute_call,
  32. 'CALL_ARGS' : self.execute_call_args,
  33. 'CALL_KWARGS' : self.execute_call_kwargs,
  34. 'SLEEP' : self.execute_sleep,
  35. }
  36. def is_active(self):
  37. """Tests if this request handler has a top-of-stack generator."""
  38. return len(self.generator_stack) > 0
  39. def handle_request(self, reply):
  40. """Replies to a request from the top-of-stack generator, and returns a new request."""
  41. if not self.generator_stack:
  42. raise ValueError('handle_request cannot be called with an empty generator stack.')
  43. # Append the server's replies to the list of replies.
  44. if reply is not None:
  45. gen = self.generator_stack[-1]
  46. if gen["replies"]:
  47. gen["replies"].extend(reply)
  48. else:
  49. gen["replies"] = reply
  50. while 1:
  51. # Silence pylint's warning about catching Exception.
  52. # pylint: disable=I0011,W0703
  53. try:
  54. gen = self.generator_stack[-1]
  55. if gen["finished_requests"]:
  56. gen["pending_requests"] = gen["generator"].send(gen["replies"])
  57. gen["finished_requests"] = False
  58. gen["replies"] = None
  59. return self.pop_requests()
  60. except KnownRequestHandled:
  61. pass
  62. except StopIteration:
  63. # Done, so remove the generator
  64. del self.generator_stack[-1]
  65. if self.generator_stack:
  66. # This generator was called from another generator.
  67. # Append 'None' to the caller's list of replies.
  68. self.append_reply(None)
  69. else:
  70. # Looks like we're done here.
  71. return None
  72. except primitive_functions.PrimitiveFinished as ex:
  73. # Done, so remove the generator
  74. del self.generator_stack[-1]
  75. if self.generator_stack:
  76. # This generator was called from another generator.
  77. # Append the callee's result to the caller's list of replies.
  78. self.append_reply(ex.result)
  79. else:
  80. # Looks like we're done here.
  81. return None
  82. def push_generator(self, gen):
  83. """Pushes a new generator onto the stack."""
  84. dd = defaultdict(lambda : None)
  85. dd["generator"] = gen
  86. dd["finished_requests"] = True
  87. self.generator_stack.append(dd)
  88. # print('Pushed generator %s. Generator count: %d' % (gen, len(self.generator_stack)))
  89. def append_reply(self, new_reply):
  90. """Appends a reply to the top-of-stack generator's list of pending replies."""
  91. if self.generator_stack[-1]["replies"] is None:
  92. self.generator_stack[-1]["replies"] = [new_reply]
  93. else:
  94. self.generator_stack[-1]["replies"].append(new_reply)
  95. def pop_requests(self):
  96. """Tries to pop a batch of Modelverse _state_ requests from the
  97. current list of requests. Known requests are executed immediately.
  98. A list of requests and a Boolean are returned. The latter is True
  99. if there are no more requests to process, and false otherwise."""
  100. requests = self.generator_stack[-1]["pending_requests"]
  101. if requests:
  102. if requests[0][0] in self.handlers:
  103. # First element is a known request
  104. elem = requests.pop(0)
  105. # The list of requests might be empty now. If so, then flag this
  106. # batch of requests as finished.
  107. if not requests:
  108. self.generator_stack[-1]["finished_requests"] = True
  109. # Handle the request.
  110. self.handlers[elem[0]](elem[1])
  111. raise KnownRequestHandled()
  112. else:
  113. for i, elem in enumerate(requests):
  114. if elem[0] in self.handlers:
  115. # Handle any requests that precede the known request first.
  116. pre_requests = requests[:i]
  117. del requests[:i]
  118. return pre_requests
  119. # We couldn't find a known request in the batch of requests, so we might as well
  120. # handle them all at once then.
  121. self.generator_stack[-1]["finished_requests"] = True
  122. return requests
  123. def execute_call(self, request_args):
  124. """Executes a CALL-request with the given argument list."""
  125. # Format: ("CALL", [gen])
  126. gen, = request_args
  127. self.push_generator(gen)
  128. def execute_call_kwargs(self, request_args):
  129. """Executes a CALL_KWARGS-request with the given argument list."""
  130. # Format: ("CALL_KWARGS", [func, kwargs])
  131. # This format is useful because it also works for functions that
  132. # throw an exception but never yield.
  133. func, kwargs = request_args
  134. # We need to be extra careful here, because func(**kwargs) might
  135. # not be a generator at all: it might simply be a method that
  136. # raises an exception. To cope with this we need to push a dummy
  137. # entry onto the stack if a StopIteration or PrimtiveFinished
  138. # exception is thrown. The logic in execute_yields will then pop
  139. # that dummy entry.
  140. try:
  141. self.push_generator(func(**kwargs))
  142. except StopIteration:
  143. self.push_generator(None)
  144. raise
  145. except primitive_functions.PrimitiveFinished:
  146. self.push_generator(None)
  147. raise
  148. except:
  149. print("EXCEPTION for " + str(locals()))
  150. raise
  151. def execute_call_args(self, request_args):
  152. """Executes a CALL_ARGS-request with the given argument list."""
  153. # Format: ("CALL_ARGS", [gen, args])
  154. func, args = request_args
  155. # We need to be extra careful here, because func(*args) might
  156. # not be a generator at all: it might simply be a method that
  157. # raises an exception. To cope with this we need to push a dummy
  158. # entry onto the stack if a StopIteration or PrimtiveFinished
  159. # exception is thrown. The logic in execute_yields will then pop
  160. # that dummy entry.
  161. try:
  162. self.push_generator(func(*args))
  163. except StopIteration:
  164. self.push_generator(None)
  165. raise
  166. except primitive_functions.PrimitiveFinished:
  167. self.push_generator(None)
  168. raise
  169. def execute_sleep(self, request_args):
  170. """Executes a SLEEP-request with the given argument list."""
  171. self.append_reply(None)
  172. raise primitive_functions.SleepKernel(request_args[0], request_args[1])