-
Notifications
You must be signed in to change notification settings - Fork 387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement simple asyncio wrapper API with basic tests #646
Draft
Traktormaster
wants to merge
8
commits into
python-zk:master
Choose a base branch
from
Traktormaster:aiome
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
aa6554c
Implement simple asyncio wrapper API with basic tests
Traktormaster 2d80d64
Line lengths
Traktormaster 45f4924
Line lengths
Traktormaster 040d649
Line lengths
Traktormaster e2419bc
Fix start_aio() and include it in test
Traktormaster 2b34499
Implement an asyncio compatible retry utility based on KazooRetry
Traktormaster 2297a9c
Improved implementation to avoid creating state-objects
Traktormaster dd9a441
Differentiate the purpose/scope of new async-result objects to optimi…
Traktormaster File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
""" | ||
Simple asyncio integration of the threaded async executor engine. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import asyncio | ||
|
||
from kazoo.aio.handler import AioSequentialThreadingHandler | ||
from kazoo.client import KazooClient, TransactionRequest | ||
|
||
|
||
class AioKazooClient(KazooClient): | ||
""" | ||
The asyncio compatibility mostly mimics the behaviour of the base async | ||
one. All calls are wrapped in asyncio.shield() to prevent cancellation | ||
that is not supported in the base async implementation. | ||
|
||
The sync and base-async API are still completely functional. Mixing the | ||
use of any of the 3 should be okay. | ||
""" | ||
|
||
def __init__(self, *args, **kwargs): | ||
if not kwargs.get("handler"): | ||
kwargs["handler"] = AioSequentialThreadingHandler() | ||
KazooClient.__init__(self, *args, **kwargs) | ||
|
||
# asyncio compatible api wrappers | ||
async def start_aio(self, timeout=15): | ||
""" | ||
There is no protection for calling this multiple times in parallel. | ||
The start_async() seems to lack that as well. Maybe it is allowed and | ||
handled internally. | ||
""" | ||
await self.handler.loop.run_in_executor(None, self.start, timeout) | ||
|
||
async def add_auth_aio(self, *args, **kwargs): | ||
return await asyncio.shield( | ||
self.add_auth_async(*args, **kwargs).future | ||
) | ||
|
||
async def sync_aio(self, *args, **kwargs): | ||
return await asyncio.shield(self.sync_async(*args, **kwargs).future) | ||
|
||
async def create_aio(self, *args, **kwargs): | ||
return await asyncio.shield(self.create_async(*args, **kwargs).future) | ||
|
||
async def ensure_path_aio(self, *args, **kwargs): | ||
return await asyncio.shield( | ||
self.ensure_path_async(*args, **kwargs).future | ||
) | ||
|
||
async def exists_aio(self, *args, **kwargs): | ||
return await asyncio.shield(self.exists_async(*args, **kwargs).future) | ||
|
||
async def get_aio(self, *args, **kwargs): | ||
return await asyncio.shield(self.get_async(*args, **kwargs).future) | ||
|
||
async def get_children_aio(self, *args, **kwargs): | ||
return await asyncio.shield( | ||
self.get_children_async(*args, **kwargs).future | ||
) | ||
|
||
async def get_acls_aio(self, *args, **kwargs): | ||
return await asyncio.shield( | ||
self.get_acls_async(*args, **kwargs).future | ||
) | ||
|
||
async def set_acls_aio(self, *args, **kwargs): | ||
return await asyncio.shield( | ||
self.set_acls_async(*args, **kwargs).future | ||
) | ||
|
||
async def set_aio(self, *args, **kwargs): | ||
return await asyncio.shield(self.set_async(*args, **kwargs).future) | ||
|
||
def transaction_aio(self): | ||
return AioTransactionRequest(self) | ||
|
||
async def delete_aio(self, *args, **kwargs): | ||
return await asyncio.shield(self.delete_async(*args, **kwargs).future) | ||
|
||
async def reconfig_aio(self, *args, **kwargs): | ||
return await asyncio.shield( | ||
self.reconfig_async(*args, **kwargs).future | ||
) | ||
|
||
|
||
class AioTransactionRequest(TransactionRequest): | ||
async def commit_aio(self): | ||
return await asyncio.shield(self.commit_async().future) | ||
|
||
async def __aenter__(self): | ||
return self | ||
|
||
async def __aexit__(self, exc_type, exc_value, exc_tb): | ||
if not exc_type: | ||
await self.commit_aio() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import asyncio | ||
import threading | ||
|
||
from kazoo.handlers.threading import AsyncResult, SequentialThreadingHandler | ||
|
||
|
||
class AioAsyncResult(AsyncResult): | ||
def __init__(self, handler): | ||
self.future = handler.loop.create_future() | ||
AsyncResult.__init__(self, handler) | ||
|
||
def set(self, value=None): | ||
""" | ||
The completion of the future has the same guarantees as the | ||
notification emitting of the condition. | ||
Provided that no callbacks raise it will complete. | ||
""" | ||
AsyncResult.set(self, value) | ||
self._handler.loop.call_soon_threadsafe(self.future.set_result, value) | ||
|
||
def set_exception(self, exception): | ||
""" | ||
The completion of the future has the same guarantees as the | ||
notification emitting of the condition. | ||
Provided that no callbacks raise it will complete. | ||
""" | ||
AsyncResult.set_exception(self, exception) | ||
self._handler.loop.call_soon_threadsafe( | ||
self.future.set_exception, exception | ||
) | ||
|
||
|
||
class AioSequentialThreadingHandler(SequentialThreadingHandler): | ||
def __init__(self): | ||
""" | ||
Creating the handler must be done on the asyncio-loop's thread. | ||
""" | ||
self.loop = asyncio.get_running_loop() | ||
self._aio_thread = threading.current_thread() | ||
SequentialThreadingHandler.__init__(self) | ||
|
||
def async_result(self, api=False): | ||
""" | ||
Almost all async-result objects are created by a method that is | ||
invoked from the user's thead. The one exception I'm aware of is | ||
in the PatientChildrenWatch utility, that creates an async-result | ||
in its worker thread. Just because of that it is imperative to | ||
only create asyncio compatible results when the invoking code is | ||
from the loop's thread. There is no PEP/API guarantee that | ||
implementing the create_future() has to be thread-safe. The default | ||
is mostly thread-safe. The only thing that may get synchronization | ||
issue is a debug-feature for asyncio development. Quickly looking at | ||
the alternate implementation of uvloop, they use the default Future | ||
implementation, so no change there. | ||
For now, just to be safe, we check the current thread and create an | ||
async-result object based on the invoking thread's identity. | ||
""" | ||
if api and threading.current_thread() is self._aio_thread: | ||
return AioAsyncResult(self) | ||
return AsyncResult(self) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import asyncio | ||
import random | ||
import time | ||
from functools import partial | ||
|
||
from kazoo.exceptions import ( | ||
ConnectionClosedError, | ||
ConnectionLoss, | ||
OperationTimeoutError, | ||
SessionExpiredError, | ||
) | ||
from kazoo.retry import ForceRetryError, RetryFailedError | ||
|
||
|
||
EXCEPTIONS = ( | ||
ConnectionLoss, | ||
OperationTimeoutError, | ||
ForceRetryError, | ||
) | ||
|
||
EXCEPTIONS_WITH_EXPIRED = EXCEPTIONS + (SessionExpiredError,) | ||
|
||
|
||
def kazoo_retry_aio( | ||
max_tries=1, | ||
delay=0.1, | ||
backoff=2, | ||
max_jitter=0.4, | ||
max_delay=60.0, | ||
ignore_expire=True, | ||
deadline=None, | ||
): | ||
""" | ||
This is similar to KazooRetry, but they do not have compatible | ||
interfaces. The threaded and asyncio constructs are too different | ||
to easily wrap the KazooRetry implementation. Unless, all retries | ||
always get their own thread to work in. This is much more lightweight | ||
compared to the object-copying and resetting implementation. | ||
|
||
There is no equivalent analogue to the interrupt API. | ||
If interrupting the retry is necessary, it must be wrapped in | ||
an asyncio.Task, which can be cancelled. Be aware though that | ||
this will quit waiting on the Zookeeper API call immediately | ||
unlike the threaded API. There is no way to interrupt/cancel an | ||
internal request thread so it will continue and stop eventually | ||
on its own. This means caller can't know if the call is still | ||
in progress and may succeed or the retry was cancelled while it | ||
was waiting for delay. | ||
|
||
Usage example. These are equivalent except that the latter lines | ||
will retry the requests on specific exceptions: | ||
await zk.create_aio("/x") | ||
await zk.create_aio("/x/y") | ||
|
||
aio_retry = kazoo_retry_aio() | ||
await aio_retry(zk.create_aio, "/x") | ||
await aio_retry(zk.create_aio, "/x/y") | ||
""" | ||
retry_exceptions = ( | ||
EXCEPTIONS_WITH_EXPIRED if ignore_expire else EXCEPTIONS | ||
) | ||
max_jitter = max(min(max_jitter, 1.0), 0.0) | ||
get_jitter = partial(random.uniform, 1.0 - max_jitter, 1.0 + max_jitter) | ||
del max_jitter | ||
|
||
async def _retry(func, *args, **kwargs): | ||
attempts = 0 | ||
cur_delay = delay | ||
stop_time = ( | ||
None if deadline is None else time.perf_counter() + deadline | ||
) | ||
while True: | ||
try: | ||
return await func(*args, **kwargs) | ||
except ConnectionClosedError: | ||
raise | ||
except retry_exceptions: | ||
# Note: max_tries == -1 means infinite tries. | ||
if attempts == max_tries: | ||
raise RetryFailedError("Too many retry attempts") | ||
attempts += 1 | ||
sleep_time = cur_delay * get_jitter() | ||
if ( | ||
stop_time is not None | ||
and time.perf_counter() + sleep_time >= stop_time | ||
): | ||
raise RetryFailedError("Exceeded retry deadline") | ||
await asyncio.sleep(sleep_time) | ||
cur_delay = min(sleep_time * backoff, max_delay) | ||
|
||
return _retry |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,12 @@ | ||
from kazoo.testing.harness import KazooTestCase, KazooTestHarness | ||
from kazoo.testing.harness import ( | ||
KazooAioTestCase, | ||
KazooTestCase, | ||
KazooTestHarness, | ||
) | ||
|
||
|
||
__all__ = ('KazooTestHarness', 'KazooTestCase', ) | ||
__all__ = ( | ||
"KazooTestHarness", | ||
"KazooTestCase", | ||
"KazooAioTestCase", | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
line break before binary operator