You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

server.py 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import asyncio
  2. import logging
  3. import time
  4. import traceback
  5. logger = logging.getLogger(__name__)
  6. class StatelessServer:
  7. """
  8. Base server class that handles basic concepts like application instance
  9. creation/pooling, exception handling, and similar, for stateless protocols
  10. (i.e. ones without actual incoming connections to the process)
  11. Your code should override the handle() method, doing whatever it needs to,
  12. and calling get_or_create_application_instance with a unique `scope_id`
  13. and `scope` for the scope it wants to get.
  14. If an application instance is found with the same `scope_id`, you are
  15. given its input queue, otherwise one is made for you with the scope provided
  16. and you are given that fresh new input queue. Either way, you should do
  17. something like:
  18. input_queue = self.get_or_create_application_instance(
  19. "user-123456",
  20. {"type": "testprotocol", "user_id": "123456", "username": "andrew"},
  21. )
  22. input_queue.put_nowait(message)
  23. If you try and create an application instance and there are already
  24. `max_application` instances, the oldest/least recently used one will be
  25. reclaimed and shut down to make space.
  26. Application coroutines that error will be found periodically (every 100ms
  27. by default) and have their exceptions printed to the console. Override
  28. application_exception() if you want to do more when this happens.
  29. If you override run(), make sure you handle things like launching the
  30. application checker.
  31. """
  32. application_checker_interval = 0.1
  33. def __init__(self, application, max_applications=1000):
  34. # Parameters
  35. self.application = application
  36. self.max_applications = max_applications
  37. # Initialisation
  38. self.application_instances = {}
  39. ### Mainloop and handling
  40. def run(self):
  41. """
  42. Runs the asyncio event loop with our handler loop.
  43. """
  44. event_loop = asyncio.get_event_loop()
  45. asyncio.ensure_future(self.application_checker())
  46. try:
  47. event_loop.run_until_complete(self.handle())
  48. except KeyboardInterrupt:
  49. logger.info("Exiting due to Ctrl-C/interrupt")
  50. async def handle(self):
  51. raise NotImplementedError("You must implement handle()")
  52. async def application_send(self, scope, message):
  53. """
  54. Receives outbound sends from applications and handles them.
  55. """
  56. raise NotImplementedError("You must implement application_send()")
  57. ### Application instance management
  58. def get_or_create_application_instance(self, scope_id, scope):
  59. """
  60. Creates an application instance and returns its queue.
  61. """
  62. if scope_id in self.application_instances:
  63. self.application_instances[scope_id]["last_used"] = time.time()
  64. return self.application_instances[scope_id]["input_queue"]
  65. # See if we need to delete an old one
  66. while len(self.application_instances) > self.max_applications:
  67. self.delete_oldest_application_instance()
  68. # Make an instance of the application
  69. input_queue = asyncio.Queue()
  70. application_instance = self.application(scope=scope)
  71. # Run it, and stash the future for later checking
  72. future = asyncio.ensure_future(
  73. application_instance(
  74. receive=input_queue.get,
  75. send=lambda message: self.application_send(scope, message),
  76. )
  77. )
  78. self.application_instances[scope_id] = {
  79. "input_queue": input_queue,
  80. "future": future,
  81. "scope": scope,
  82. "last_used": time.time(),
  83. }
  84. return input_queue
  85. def delete_oldest_application_instance(self):
  86. """
  87. Finds and deletes the oldest application instance
  88. """
  89. oldest_time = min(
  90. details["last_used"] for details in self.application_instances.values()
  91. )
  92. for scope_id, details in self.application_instances.items():
  93. if details["last_used"] == oldest_time:
  94. self.delete_application_instance(scope_id)
  95. # Return to make sure we only delete one in case two have
  96. # the same oldest time
  97. return
  98. def delete_application_instance(self, scope_id):
  99. """
  100. Removes an application instance (makes sure its task is stopped,
  101. then removes it from the current set)
  102. """
  103. details = self.application_instances[scope_id]
  104. del self.application_instances[scope_id]
  105. if not details["future"].done():
  106. details["future"].cancel()
  107. async def application_checker(self):
  108. """
  109. Goes through the set of current application instance Futures and cleans up
  110. any that are done/prints exceptions for any that errored.
  111. """
  112. while True:
  113. await asyncio.sleep(self.application_checker_interval)
  114. for scope_id, details in list(self.application_instances.items()):
  115. if details["future"].done():
  116. exception = details["future"].exception()
  117. if exception:
  118. await self.application_exception(exception, details)
  119. try:
  120. del self.application_instances[scope_id]
  121. except KeyError:
  122. # Exception handling might have already got here before us. That's fine.
  123. pass
  124. async def application_exception(self, exception, application_details):
  125. """
  126. Called whenever an application coroutine has an exception.
  127. """
  128. logging.error(
  129. "Exception inside application: %s\n%s%s",
  130. exception,
  131. "".join(traceback.format_tb(exception.__traceback__)),
  132. " {}".format(exception),
  133. )