Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Asynchronous API, Networking and Asyncio Support #301

Merged
merged 33 commits into from
Feb 18, 2020
Merged

Conversation

lzpap
Copy link
Member

@lzpap lzpap commented Feb 11, 2020

Solves #224

Description

Motivation

So far, PyOTA has been relying on synchronous communication with nodes. This meant, that API requests could only be sent sequentially, one after the other. While this is simple and easy for developers to comprehend, it doesn't scale well and wastes a lot of precious time waiting for the results of network IO bound tasks.

Solution

With this PR, PyOTA is capable of doing asynchronous networking. Requests can be sent concurrently with the help of asyncio and httpx packages. When using the new AsyncIota API class, one can schedule API calls to execute concurrently. The API calls in the new class are actually python coroutines, but both their argument lists and return values are preserved from the previous version of PyOTA.

Regular, synchronous API calls are still supported with the "old" Iota API class. There should be no difference from a user perspective to issue a synchronous API call, however, the methods have been reworked to execute the aforementioned coroutines inside an event loop under the hood. This speeds up some extended API calls too.

Changes in source code

  • Use AsyncClient from httpx for networking instead of requests. httpx supports Python 3.6+.

  • Mimic the behavior of AsyncClient in MockAdapter, return an asyncio.Future response object for a request.

  • Refactor RoutingWrapper to work with async.

  • Create new API classes:

    • AsyncStrictIota and
    • AsyncIota.
      These API classes define the API calls as coroutines rather than methods. Hence, the API calls can be awaited by the application developer.
  • Refactor StrictIota and Iota classes to build on top of the two new classes. They provide the synchronous method interface by executing the parents' coroutines in an event loop and returning their result.

  • Refactor commands to work asynchronously. Modify BaseCommand.__call__() and BaseCommand._execute() to work as coroutines.

  • All core commands are async.

  • Update extended commands to work as async.

    • BroadcastAndStoreCommand is (nearly) twice as fast as before, which speeds up for example SendTrytesCommand and SendTransferCommand as well.
    • GetBundlesCommand is faster when called with multiple args.
    • GetNewAddressesCommand is faster as internal address checks are done concurrently.
  • Remove Helpers class as IsPromotableCommand is properly implemented as an extended API call.

  • Update iota.multisig module to work async, create new AsyncMultisigIota API class and rebase the original multisig api class on top of it, using the same logic as above.

  • Remove Support for Pyhton 2 and Python 3.5!
    Async modules do not work well with 3.6>.

Changes in test code

  • Make use of aiounittest module for testing async coroutines. @async_test decorator runs a test method inside an event loop.
  • Update adapter and api tests.
  • Update core API command tests for async.
  • Update extended command tests for async.
  • Update multisig tests for async.
  • Remove PY2 and PY3.5 scenarios from test configuration.

Future work in another PR(s):

  • Update documentation with new API classes and methods.
  • Write examples to show how to use the new async classes.
  • Clean up PY2 code snippets from codebase.
  • Transform the codebase to PY3 format.

lzpap added 25 commits February 5, 2020 12:54
- httpx supports both sync and async
- Introduce 'AsyncStrictIota' and 'AsyncIota'
  for async API
- Refactor original API classes to rely on their
  async counterpart
- Make 'BaseCommand' async
- Make 'AttachToTangleCommand' async
- update tests for async
- Refactor `BroadCastBundleCommand` and update its tests
- Refactor `GetBundleCommand` and its update tests
- Refactor `TraverseBundleCommand` and update its tests

`get_bundles` fetches bundles concurrently, which speeds it up
if it is called with more than one argument.
- Updated tests for async
- `iter_used_addresses` becomes an async generator
- `get_bundles_from_transaction_hashes` ready for async
- Update `utils_test.py`
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
`Helpers` added `is_promotable` method (wrapper for checkConsistency)
to the api class, which is now implemented as a standalone API command,
see `IsPromotableCommand`.
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- Update tests for async
- New class `AsyncMultisigIota`
- Original `MultisigIota` rebased on `AsyncMultisigIota`
- Updated multisig commands to work with async
- Updated test for async
The new async functionalities are incompatible with
anything older, than Python 3.6.

Future work:
 - Clean up legacy PY2 -> PY3 code snippets.
   (future, six, etc..)
 - Transform codebase to use PY3 style code.
@lzpap lzpap added enhancement deprecation Removal of obsolete functionality. labels Feb 11, 2020
@lzpap lzpap self-assigned this Feb 11, 2020
Copy link
Contributor

@todofixthis todofixthis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good with minor questions/suggestions.

iota/api.py Outdated
# Execute original coroutine inside an event loop to make this method
# synchronous
return asyncio.get_event_loop().run_until_complete(
super(StrictIota, self).add_neighbors(uris)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
super(StrictIota, self).add_neighbors(uris)
super().add_neighbors(uris)

💭 Since we're no longer supporting Python 2, we can use py3-style super() calls.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, @todofixthis could you explain me why super() is called in a base class?
For example here:

@add_metaclass(AdapterMeta)
class BaseAdapter(object):
    """
    Interface for IOTA API adapters.

    Adapters make it easy to customize the way an API instance
    communicates with a node.
    """
    supported_protocols = ()  # type: Tuple[Text]
    """
    Protocols that ``resolve_adapter`` can use to identify this adapter
    type.
    """

    def __init__(self):
        super(BaseAdapter, self).__init__()

Copy link
Contributor

@todofixthis todofixthis Feb 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Good question. I think the reason I did this is in case a derived class uses multiple inheritance, e.g., CustomAdapter(BaseAdapter, OtherBase): (or possibly the other way around; multiple inheritance is so confusing 😅) — without the super() call, it's possible that the constructor for one or more bases won't get called.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thanks for the answer!

Let's say we switch to py3-style super() calls everywhere and don't need to explicitly tell type and object to super(). If I do it with BaseAdapter:

@add_metaclass(AdapterMeta)
class BaseAdapter:
    """
    Interface for IOTA API adapters.

    Adapters make it easy to customize the way an API instance
    communicates with a node.
    """
    supported_protocols = ()  # type: Tuple[Text]
    """
    Protocols that ``resolve_adapter`` can use to identify this adapter
    type.
    """

    def __init__(self):
        super().__init__()

I get a TypeError in the super() call:

Traceback (most recent call last):
  File "/pyota/test/commands/extended/utils_test.py", line 16, in setUp
    self.adapter = MockAdapter()
  File "/pyota/iota/adapter/__init__.py", line 547, in __init__
    super(MockAdapter, self).__init__()
  File "/pyota/iota/adapter/__init__.py", line 172, in __init__
    super().__init__()
TypeError: super(type, obj): obj must be an instance or subtype of type

, but the MRO seems okay:

>>> from iota import BaseAdapter
>>> BaseAdapter.__mro__
(<class 'iota.adapter.BaseAdapter'>, <class 'object'>)

What am I missing here? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking that out! Hmmm.. maybe it's not needed in Python 3 then? I don't recall the exact situation where I thought it was necessary; maybe we just remove it and see if anyone complains 😅

iota/api.py Outdated Show resolved Hide resolved
iota/commands/extended/get_bundles.py Show resolved Hide resolved
iota/commands/extended/utils.py Outdated Show resolved Hide resolved
setup.py Outdated Show resolved Hide resolved
test/commands/extended/utils_test.py Show resolved Hide resolved
tox.ini Show resolved Hide resolved
- docstrings update
- info in comment for future devs about api classes
- Better reflect the diff between them,
- For easier code maintenance ,
- Add explanatory comment.
- AsyncClient instance attribute in HttpAdapter class
- Client will keep and reuse connections to the same host
@lzpap lzpap merged commit d7d7285 into iotaledger:develop Feb 18, 2020
This was referenced Feb 18, 2020
@lzpap lzpap mentioned this pull request Mar 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
deprecation Removal of obsolete functionality. enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants