Slide 1

Slide 1 text

FROM SYNC TO ASYNC PYTHON, A ASYNCIO MIGRATION BORIS FELD - PYCONFR 2015

Slide 2

Slide 2 text

ZeroServices MicroServices framework Peer communication Peer discovery Real-time notifications

Slide 3

Slide 3 text

Asynchronous Synchronous -> process stuck during IO Asynchronous -> process could do something else during IO

Slide 4

Slide 4 text

V0

Slide 5

Slide 5 text

No tests Proof of concept validate the feasibility. Small enough. Tests aren’t worth it.

Slide 6

Slide 6 text

V0 ZMQ communication Tornado integration with pyzmq Not WSGI: Request (client) -> Response (server) Doesn’t works well with web socket, etc…

Slide 7

Slide 7 text

Tornado example class GenAsyncHandler(RequestHandler): @gen.coroutine def get(self): result = yield self.fetch() self.write(result) @gen.coroutine def fetch(self): http_client = AsyncHTTPClient() response = yield http_client.fetch("http://ifconfig.me") raise Return(response.body)

Slide 8

Slide 8 text

Pyzmq tips ZmqStream are your friends. stream_sub = zmqstream.ZMQStream(socket_sub) stream_sub.on_recv(process_message)

Slide 9

Slide 9 text

Result Works! Validate the design / architecture. Some parts were asynchronous.

Slide 10

Slide 10 text

Why changes? Want to test python 3.4 because it’s the future. Want to use asyncio because of the highlight.

Slide 11

Slide 11 text

V1

Slide 12

Slide 12 text

V1 PYTHON 3!

Slide 13

Slide 13 text

Python 3.4 migration Mostly done thanks to 2to3. Dependencies were migrated. No six, drop support to python2.

Slide 14

Slide 14 text

Python 3 <3 Str/bytes separation

Slide 15

Slide 15 text

Python 2 Mix of unicode and str, conversion when UnicodeEncodeError and UnicodeDecodeError.

Slide 16

Slide 16 text

Environnement - Bytes Str / Bytes Application Str

Slide 17

Slide 17 text

Str / Bytes Everything from outside is bytes and needs to be converted to str. Everything to outside is str and needs to be converted to bytes.

Slide 18

Slide 18 text

Synchronous Still synchronous design: Easier to think about Easier to reason about design Small design tuning Tests!

Slide 19

Slide 19 text

TDD rewriting Redesign using TDD Add test, copy « good » V0 code, refactoring.

Slide 20

Slide 20 text

Dependency injection Make IO parts interchangeable with the same API. So your tests could use in-memory stubs.

Slide 21

Slide 21 text

Interface testing class _BaseMediumTestCase(TestCase): def setUp(self): self.medium_1 = self.get_medium() self.medium_2 = self.get_medium() def test_send_response(self): return_value = {'data': 'ReturnValue'} message_type = 'MYMESSAGETYPE' message = {'foo': 'bar'} self.medium_2.service.on_message_mock.return_value = return_value result = self.medium_1.send(self.medium_2.node_id, message, message_type=message_type) self.assertEqual(result, return_value)

Slide 22

Slide 22 text

One test case per implementation class MemoryMediumTestCase(_BaseMediumTestCase): def get_medium(self): medium = MemoryMedium(discovery_class=MemoryDiscoveryMedium) medium.set_service(TestService('test_service', medium)) medium.start() return medium class ZeroMQMediumTestCase(_BaseMediumTestCase): def get_medium(self): medium = ZeroMQMedium(discovery_class=MemoryDiscoveryMedium) medium.set_service(TestService('test_service', medium)) medium.start() return medium

Slide 23

Slide 23 text

What is better ? Design more clean, clear separation of concerns. Modern Python. Tests \o/

Slide 24

Slide 24 text

Why changes? Because still synchronous.

Slide 25

Slide 25 text

V2

Slide 26

Slide 26 text

Asyncio Young but promising community Lots of libraries: aiozmq aiohttp

Slide 27

Slide 27 text

Asynchronous migration Easy to do with: Clean design Tests

Slide 28

Slide 28 text

Yield from Basically changes: for i in f(): yield i yield from f()

Slide 29

Slide 29 text

Tornado example in asyncio @asyncio.coroutine def handle(request): result = yield from fetch() return web.Response(body=result) def fetch(): response = yield from aiohttp.request('GET', 'http://ifconfig.me') return (yield from response.read())

Slide 30

Slide 30 text

Tornado example class GenAsyncHandler(RequestHandler): @gen.coroutine def get(self): result = yield self.fetch() self.write(result) @gen.coroutine def fetch(self): http_client = AsyncHTTPClient() response = yield http_client.fetch("http://ifconfig.me") raise Return(response.body)

Slide 31

Slide 31 text

Yield from Avoid the stack of gen.coroutine or gen.Task Replace by asyncio.coroutine async / await python 3.5 ?

Slide 32

Slide 32 text

Tests Already has some tests. Tests are your best friend

Slide 33

Slide 33 text

Tests Rewrite tests in asynchronous with this snippet: def _async_test(f): @wraps(f) def wrapper(self, *args, **kwargs): if not self.loop.is_running(): coro = asyncio.coroutine(f) future = coro(self, *args, **kwargs) self.loop.run_until_complete(asyncio.wait_for(future, 2, loop=self.loop)) else: return f(self, *args, **kwargs) return wrapper

Slide 34

Slide 34 text

Test migration class _BaseMediumTestCase(TestCase): def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) self.medium_1 = self.loop.run_until_complete(self.get_medium(self.loop)) self.medium_2 = self.loop.run_until_complete(self.get_medium(self.loop)) @_async_test def test_send_response(self): yield from asyncio.sleep(0.1, loop=self.loop) return_value = {'data': 'ReturnValue'} message_type = 'MYMESSAGETYPE' message = {'foo': 'bar'} self.medium_2.service.on_message_mock.return_value = return_value result = yield from self.medium_1.send(self.medium_2.node_id, message, message_type=message_type) self.assertEqual(result, return_value)

Slide 35

Slide 35 text

Test migration II class MemoryMediumTestCase(_BaseMediumTestCase): @asyncio.coroutine def get_medium(self, loop): medium = MemoryMedium(loop=loop, discovery_class=MemoryDiscoveryMedium) medium.set_service(TestService('test_service', medium)) yield from medium.start() return medium class ZeroMQMediumTestCase(_BaseMediumTestCase): @asyncio.coroutine def get_medium(self, loop): medium = ZeroMQMedium(loop=loop, discovery_class=MemoryDiscoveryMedium) medium.set_service(TestService('test_service', medium)) yield from medium.start() return medium

Slide 36

Slide 36 text

Tip Asyncio queue are a pain in the ass to test You want to send a query and when the yield from respond back, everything is fine.

Slide 37

Slide 37 text

Queue class ZmqDealerProtocol(aiozmq.ZmqProtocol): def msg_received(self, msg): self.queue.put_nowait(msg) while True: msg = yield from queue.get()

Slide 38

Slide 38 text

VS class ServerProtocol(object): def msg_received(self, msg): asyncio.async(self.callback(msg), loop=self.loop)

Slide 39

Slide 39 text

Refactoring Little refactoring on how parts interact which each other. Which is doing asynchronous and which doesn’t.

Slide 40

Slide 40 text

Asynchronous Good for scalability, not raw performances. Overhead worth it at scale.

Slide 41

Slide 41 text

Conclusion Python 3.4 migration is not so hard Asyncio migration is not so hard With clear design And tests And asynchronous mindset