图片解析应用
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.

452 lines
15 KiB

  1. """Higher level child and data watching API's.
  2. :Maintainer: Ben Bangert <ben@groovie.org>
  3. :Status: Production
  4. .. note::
  5. :ref:`DataWatch` and :ref:`ChildrenWatch` may only handle a single
  6. function, attempts to associate a single instance with multiple functions
  7. will result in an exception being thrown.
  8. """
  9. from functools import partial, wraps
  10. import logging
  11. import time
  12. import warnings
  13. from kazoo.exceptions import ConnectionClosedError, NoNodeError, KazooException
  14. from kazoo.protocol.states import KazooState
  15. from kazoo.retry import KazooRetry
  16. log = logging.getLogger(__name__)
  17. _STOP_WATCHING = object()
  18. def _ignore_closed(func):
  19. @wraps(func)
  20. def wrapper(*args, **kwargs):
  21. try:
  22. return func(*args, **kwargs)
  23. except ConnectionClosedError:
  24. pass
  25. return wrapper
  26. class DataWatch(object):
  27. """Watches a node for data updates and calls the specified
  28. function each time it changes
  29. The function will also be called the very first time its
  30. registered to get the data.
  31. Returning `False` from the registered function will disable future
  32. data change calls. If the client connection is closed (using the
  33. close command), the DataWatch will no longer get updates.
  34. If the function supplied takes three arguments, then the third one
  35. will be a :class:`~kazoo.protocol.states.WatchedEvent`. It will
  36. only be set if the change to the data occurs as a result of the
  37. server notifying the watch that there has been a change. Events
  38. like reconnection or the first call will not include an event.
  39. If the node does not exist, then the function will be called with
  40. ``None`` for all values.
  41. .. tip::
  42. Because :class:`DataWatch` can watch nodes that don't exist, it
  43. can be used alternatively as a higher-level Exists watcher that
  44. survives reconnections and session loss.
  45. Example with client:
  46. .. code-block:: python
  47. @client.DataWatch('/path/to/watch')
  48. def my_func(data, stat):
  49. print("Data is %s" % data)
  50. print("Version is %s" % stat.version)
  51. # Above function is called immediately and prints
  52. # Or if you want the event object
  53. @client.DataWatch('/path/to/watch')
  54. def my_func(data, stat, event):
  55. print("Data is %s" % data)
  56. print("Version is %s" % stat.version)
  57. print("Event is %s" % event)
  58. .. versionchanged:: 1.2
  59. DataWatch now ignores additional arguments that were previously
  60. passed to it and warns that they are no longer respected.
  61. """
  62. def __init__(self, client, path, func=None, *args, **kwargs):
  63. """Create a data watcher for a path
  64. :param client: A zookeeper client.
  65. :type client: :class:`~kazoo.client.KazooClient`
  66. :param path: The path to watch for data changes on.
  67. :type path: str
  68. :param func: Function to call initially and every time the
  69. node changes. `func` will be called with a
  70. tuple, the value of the node and a
  71. :class:`~kazoo.client.ZnodeStat` instance.
  72. :type func: callable
  73. """
  74. self._client = client
  75. self._path = path
  76. self._func = func
  77. self._stopped = False
  78. self._run_lock = client.handler.lock_object()
  79. self._version = None
  80. self._retry = KazooRetry(
  81. max_tries=None, sleep_func=client.handler.sleep_func
  82. )
  83. self._include_event = None
  84. self._ever_called = False
  85. self._used = False
  86. if args or kwargs:
  87. warnings.warn(
  88. "Passing additional arguments to DataWatch is"
  89. " deprecated. ignore_missing_node is now assumed "
  90. " to be True by default, and the event will be "
  91. " sent if the function can handle receiving it",
  92. DeprecationWarning,
  93. stacklevel=2,
  94. )
  95. # Register our session listener if we're going to resume
  96. # across session losses
  97. if func is not None:
  98. self._used = True
  99. self._client.add_listener(self._session_watcher)
  100. self._get_data()
  101. def __call__(self, func):
  102. """Callable version for use as a decorator
  103. :param func: Function to call initially and every time the
  104. data changes. `func` will be called with a
  105. tuple, the value of the node and a
  106. :class:`~kazoo.client.ZnodeStat` instance.
  107. :type func: callable
  108. """
  109. if self._used:
  110. raise KazooException(
  111. "A function has already been associated with this "
  112. "DataWatch instance."
  113. )
  114. self._func = func
  115. self._used = True
  116. self._client.add_listener(self._session_watcher)
  117. self._get_data()
  118. return func
  119. def _log_func_exception(self, data, stat, event=None):
  120. try:
  121. # For backwards compatibility, don't send event to the
  122. # callback unless the send_event is set in constructor
  123. if not self._ever_called:
  124. self._ever_called = True
  125. try:
  126. result = self._func(data, stat, event)
  127. except TypeError:
  128. result = self._func(data, stat)
  129. if result is False:
  130. self._stopped = True
  131. self._func = None
  132. self._client.remove_listener(self._session_watcher)
  133. except Exception as exc:
  134. log.exception(exc)
  135. raise
  136. @_ignore_closed
  137. def _get_data(self, event=None):
  138. # Ensure this runs one at a time, possible because the session
  139. # watcher may trigger a run
  140. with self._run_lock:
  141. if self._stopped:
  142. return
  143. initial_version = self._version
  144. try:
  145. data, stat = self._retry(
  146. self._client.get, self._path, self._watcher
  147. )
  148. except NoNodeError:
  149. data = None
  150. # This will set 'stat' to None if the node does not yet
  151. # exist.
  152. stat = self._retry(
  153. self._client.exists, self._path, self._watcher
  154. )
  155. if stat:
  156. self._client.handler.spawn(self._get_data)
  157. return
  158. # No node data, clear out version
  159. if stat is None:
  160. self._version = None
  161. else:
  162. self._version = stat.mzxid
  163. # Call our function if its the first time ever, or if the
  164. # version has changed
  165. if initial_version != self._version or not self._ever_called:
  166. self._log_func_exception(data, stat, event)
  167. def _watcher(self, event):
  168. self._get_data(event=event)
  169. def _set_watch(self, state):
  170. with self._run_lock:
  171. self._watch_established = state
  172. def _session_watcher(self, state):
  173. if state == KazooState.CONNECTED:
  174. self._client.handler.spawn(self._get_data)
  175. class ChildrenWatch(object):
  176. """Watches a node for children updates and calls the specified
  177. function each time it changes
  178. The function will also be called the very first time its
  179. registered to get children.
  180. Returning `False` from the registered function will disable future
  181. children change calls. If the client connection is closed (using
  182. the close command), the ChildrenWatch will no longer get updates.
  183. if send_event=True in __init__, then the function will always be
  184. called with second parameter, ``event``. Upon initial call or when
  185. recovering a lost session the ``event`` is always ``None``.
  186. Otherwise it's a :class:`~kazoo.prototype.state.WatchedEvent`
  187. instance.
  188. Example with client:
  189. .. code-block:: python
  190. @client.ChildrenWatch('/path/to/watch')
  191. def my_func(children):
  192. print "Children are %s" % children
  193. # Above function is called immediately and prints children
  194. """
  195. def __init__(
  196. self,
  197. client,
  198. path,
  199. func=None,
  200. allow_session_lost=True,
  201. send_event=False,
  202. ):
  203. """Create a children watcher for a path
  204. :param client: A zookeeper client.
  205. :type client: :class:`~kazoo.client.KazooClient`
  206. :param path: The path to watch for children on.
  207. :type path: str
  208. :param func: Function to call initially and every time the
  209. children change. `func` will be called with a
  210. single argument, the list of children.
  211. :type func: callable
  212. :param allow_session_lost: Whether the watch should be
  213. re-registered if the zookeeper
  214. session is lost.
  215. :type allow_session_lost: bool
  216. :type send_event: bool
  217. :param send_event: Whether the function should be passed the
  218. event sent by ZooKeeper or None upon
  219. initialization (see class documentation)
  220. The path must already exist for the children watcher to
  221. run.
  222. """
  223. self._client = client
  224. self._path = path
  225. self._func = func
  226. self._send_event = send_event
  227. self._stopped = False
  228. self._watch_established = False
  229. self._allow_session_lost = allow_session_lost
  230. self._run_lock = client.handler.lock_object()
  231. self._prior_children = None
  232. self._used = False
  233. # Register our session listener if we're going to resume
  234. # across session losses
  235. if func is not None:
  236. self._used = True
  237. if allow_session_lost:
  238. self._client.add_listener(self._session_watcher)
  239. self._get_children()
  240. def __call__(self, func):
  241. """Callable version for use as a decorator
  242. :param func: Function to call initially and every time the
  243. children change. `func` will be called with a
  244. single argument, the list of children.
  245. :type func: callable
  246. """
  247. if self._used:
  248. raise KazooException(
  249. "A function has already been associated with this "
  250. "ChildrenWatch instance."
  251. )
  252. self._func = func
  253. self._used = True
  254. if self._allow_session_lost:
  255. self._client.add_listener(self._session_watcher)
  256. self._get_children()
  257. return func
  258. @_ignore_closed
  259. def _get_children(self, event=None):
  260. with self._run_lock: # Ensure this runs one at a time
  261. if self._stopped:
  262. return
  263. try:
  264. children = self._client.retry(
  265. self._client.get_children, self._path, self._watcher
  266. )
  267. except NoNodeError:
  268. self._stopped = True
  269. return
  270. if not self._watch_established:
  271. self._watch_established = True
  272. if (
  273. self._prior_children is not None
  274. and self._prior_children == children
  275. ):
  276. return
  277. self._prior_children = children
  278. try:
  279. if self._send_event:
  280. result = self._func(children, event)
  281. else:
  282. result = self._func(children)
  283. if result is False:
  284. self._stopped = True
  285. self._func = None
  286. if self._allow_session_lost:
  287. self._client.remove_listener(self._session_watcher)
  288. except Exception as exc:
  289. log.exception(exc)
  290. raise
  291. def _watcher(self, event):
  292. if event.type != "NONE":
  293. self._get_children(event)
  294. def _session_watcher(self, state):
  295. if state in (KazooState.LOST, KazooState.SUSPENDED):
  296. self._watch_established = False
  297. elif (
  298. state == KazooState.CONNECTED
  299. and not self._watch_established
  300. and not self._stopped
  301. ):
  302. self._client.handler.spawn(self._get_children)
  303. class PatientChildrenWatch(object):
  304. """Patient Children Watch that returns values after the children
  305. of a node don't change for a period of time
  306. A separate watcher for the children of a node, that ignores
  307. changes within a boundary time and sets the result only when the
  308. boundary time has elapsed with no children changes.
  309. Example::
  310. watcher = PatientChildrenWatch(client, '/some/path',
  311. time_boundary=5)
  312. async_object = watcher.start()
  313. # Blocks until the children have not changed for time boundary
  314. # (5 in this case) seconds, returns children list and an
  315. # async_result that will be set if the children change in the
  316. # future
  317. children, child_async = async_object.get()
  318. .. note::
  319. This Watch is different from :class:`DataWatch` and
  320. :class:`ChildrenWatch` as it only returns once, does not take
  321. a function that is called, and provides an
  322. :class:`~kazoo.interfaces.IAsyncResult` object that can be
  323. checked to see if the children have changed later.
  324. """
  325. def __init__(self, client, path, time_boundary=30):
  326. self.client = client
  327. self.path = path
  328. self.children = []
  329. self.time_boundary = time_boundary
  330. self.children_changed = client.handler.event_object()
  331. def start(self):
  332. """Begin the watching process asynchronously
  333. :returns: An :class:`~kazoo.interfaces.IAsyncResult` instance
  334. that will be set when no change has occurred to the
  335. children for time boundary seconds.
  336. """
  337. self.asy = asy = self.client.handler.async_result()
  338. self.client.handler.spawn(self._inner_start)
  339. return asy
  340. def _inner_start(self):
  341. try:
  342. while True:
  343. async_result = self.client.handler.async_result()
  344. self.children = self.client.retry(
  345. self.client.get_children,
  346. self.path,
  347. partial(self._children_watcher, async_result),
  348. )
  349. self.client.handler.sleep_func(self.time_boundary)
  350. if self.children_changed.is_set():
  351. self.children_changed.clear()
  352. else:
  353. break
  354. self.asy.set((self.children, async_result))
  355. except Exception as exc:
  356. self.asy.set_exception(exc)
  357. def _children_watcher(self, async_result, event):
  358. self.children_changed.set()
  359. async_result.set(time.time())