Actually reproduce the de-registration problem
This truly reproduces #141. It turns out the problem only occurs when we're cancelled in the middle of consuming "infinite streams". Good news is this tests a lot of edge cases :)ensure_deregister
							parent
							
								
									699bfd1857
								
							
						
					
					
						commit
						a5279f80a7
					
				|  | @ -5,6 +5,7 @@ import os | ||||||
| import signal | import signal | ||||||
| import platform | import platform | ||||||
| from functools import partial | from functools import partial | ||||||
|  | import itertools | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| import tractor | import tractor | ||||||
|  | @ -87,10 +88,35 @@ async def test_trynamic_trio(func, start_method): | ||||||
|         print("CUTTTT CUUTT CUT!!?! Donny!! You're supposed to say...") |         print("CUTTTT CUUTT CUT!!?! Donny!! You're supposed to say...") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | async def stream_forever(): | ||||||
|  |     for i in itertools.count(): | ||||||
|  |         yield i | ||||||
|  |         await trio.sleep(0.01) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def cancel(use_signal, delay=0): | ||||||
|  |     # hold on there sally | ||||||
|  |     await trio.sleep(delay) | ||||||
|  | 
 | ||||||
|  |     # trigger cancel | ||||||
|  |     if use_signal: | ||||||
|  |         if platform.system() == 'Windows': | ||||||
|  |             pytest.skip("SIGINT not supported on windows") | ||||||
|  |         os.kill(os.getpid(), signal.SIGINT) | ||||||
|  |     else: | ||||||
|  |         raise KeyboardInterrupt | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def stream_from(portal): | ||||||
|  |     async for value in await portal.result(): | ||||||
|  |         print(value) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| async def spawn_and_check_registry( | async def spawn_and_check_registry( | ||||||
|     arb_addr: tuple, |     arb_addr: tuple, | ||||||
|     use_signal: bool, |     use_signal: bool, | ||||||
|     remote_arbiter: bool = False, |     remote_arbiter: bool = False, | ||||||
|  |     with_streaming: bool = False, | ||||||
| ) -> None: | ) -> None: | ||||||
|     actor = tractor.current_actor() |     actor = tractor.current_actor() | ||||||
| 
 | 
 | ||||||
|  | @ -101,9 +127,7 @@ async def spawn_and_check_registry( | ||||||
|         if actor.is_arbiter: |         if actor.is_arbiter: | ||||||
|             async def get_reg(): |             async def get_reg(): | ||||||
|                 return actor._registry |                 return actor._registry | ||||||
| 
 |  | ||||||
|             extra = 1  # arbiter is local root actor |             extra = 1  # arbiter is local root actor | ||||||
| 
 |  | ||||||
|         else: |         else: | ||||||
|             get_reg = partial(portal.run, 'self', 'get_registry') |             get_reg = partial(portal.run, 'self', 'get_registry') | ||||||
|             extra = 2  # local root actor + remote arbiter |             extra = 2  # local root actor + remote arbiter | ||||||
|  | @ -112,13 +136,18 @@ async def spawn_and_check_registry( | ||||||
|         registry = await get_reg() |         registry = await get_reg() | ||||||
|         assert actor.uid in registry |         assert actor.uid in registry | ||||||
| 
 | 
 | ||||||
|  |         if with_streaming: | ||||||
|  |             to_run = stream_forever | ||||||
|  |         else: | ||||||
|  |             to_run = trio.sleep_forever | ||||||
|  | 
 | ||||||
|  |         async with trio.open_nursery() as trion: | ||||||
|             try: |             try: | ||||||
|                 async with tractor.open_nursery() as n: |                 async with tractor.open_nursery() as n: | ||||||
|                     portals = {} |                     portals = {} | ||||||
|                     for i in range(3): |                     for i in range(3): | ||||||
|                         name = f'a{i}' |                         name = f'a{i}' | ||||||
|                     portals[name] = await n.run_in_actor( |                         portals[name] = await n.run_in_actor(name, to_run) | ||||||
|                         name, trio.sleep_forever) |  | ||||||
| 
 | 
 | ||||||
|                     # wait on last actor to come up |                     # wait on last actor to come up | ||||||
|                     async with tractor.wait_for_actor(name): |                     async with tractor.wait_for_actor(name): | ||||||
|  | @ -128,50 +157,74 @@ async def spawn_and_check_registry( | ||||||
| 
 | 
 | ||||||
|                     assert len(portals) + extra == len(registry) |                     assert len(portals) + extra == len(registry) | ||||||
| 
 | 
 | ||||||
|                 # trigger cancel |                     if with_streaming: | ||||||
|                 if use_signal: |                         await trio.sleep(0.1) | ||||||
|                     if platform.system() == 'Windows': | 
 | ||||||
|                         pytest.skip("SIGINT not supported on windows") |                         pts = list(portals.values()) | ||||||
|                     os.kill(os.getpid(), signal.SIGINT) |                         for p in pts[:-1]: | ||||||
|  |                             trion.start_soon(stream_from, p) | ||||||
|  | 
 | ||||||
|  |                         # stream for 1 sec | ||||||
|  |                         trion.start_soon(cancel, use_signal, 1) | ||||||
|  | 
 | ||||||
|  |                         last_p = pts[-1] | ||||||
|  |                         async for value in await last_p.result(): | ||||||
|  |                             print(value) | ||||||
|                     else: |                     else: | ||||||
|                     raise KeyboardInterrupt |                         await cancel(use_signal) | ||||||
|  | 
 | ||||||
|             finally: |             finally: | ||||||
|             # all subactors should have de-registered |                 with trio.CancelScope(shield=True): | ||||||
|                     await trio.sleep(0.5) |                     await trio.sleep(0.5) | ||||||
|  | 
 | ||||||
|  |                     # all subactors should have de-registered | ||||||
|                     registry = await get_reg() |                     registry = await get_reg() | ||||||
|                     assert len(registry) == extra |                     assert len(registry) == extra | ||||||
|                     assert actor.uid in registry |                     assert actor.uid in registry | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize('use_signal', [False, True]) | @pytest.mark.parametrize('use_signal', [False, True]) | ||||||
|  | @pytest.mark.parametrize('with_streaming', [False, True]) | ||||||
| def test_subactors_unregister_on_cancel( | def test_subactors_unregister_on_cancel( | ||||||
|     start_method, |  | ||||||
|     use_signal, |  | ||||||
|     arb_addr |  | ||||||
| ): |  | ||||||
|     """Verify that cancelling a nursery results in all subactors |  | ||||||
|     deregistering themselves with the arbiter. |  | ||||||
|     """ |  | ||||||
|     with pytest.raises(KeyboardInterrupt): |  | ||||||
|         tractor.run(spawn_and_check_registry, arb_addr, use_signal) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @pytest.mark.parametrize('use_signal', [False, True]) |  | ||||||
| def test_subactors_unregister_on_cancel_remote_daemon( |  | ||||||
|     daemon, |  | ||||||
|     start_method, |     start_method, | ||||||
|     use_signal, |     use_signal, | ||||||
|     arb_addr, |     arb_addr, | ||||||
|  |     with_streaming, | ||||||
| ): | ): | ||||||
|     """Verify that cancelling a nursery results in all subactors |     """Verify that cancelling a nursery results in all subactors | ||||||
|     deregistering themselves with the arbiter. |     deregistering themselves with the arbiter. | ||||||
|     """ |     """ | ||||||
|  |     with pytest.raises(KeyboardInterrupt): | ||||||
|  |         tractor.run( | ||||||
|  |             spawn_and_check_registry, | ||||||
|  |             arb_addr, | ||||||
|  |             use_signal, | ||||||
|  |             False, | ||||||
|  |             with_streaming, | ||||||
|  |             arbiter_addr=arb_addr | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize('use_signal', [False, True]) | ||||||
|  | @pytest.mark.parametrize('with_streaming', [False, True]) | ||||||
|  | def test_subactors_unregister_on_cancel_remote_daemon( | ||||||
|  |     daemon, | ||||||
|  |     start_method, | ||||||
|  |     use_signal, | ||||||
|  |     arb_addr, | ||||||
|  |     with_streaming, | ||||||
|  | ): | ||||||
|  |     """Verify that cancelling a nursery results in all subactors | ||||||
|  |     deregistering themselves with a **remote** (not in the local process | ||||||
|  |     tree) arbiter. | ||||||
|  |     """ | ||||||
|     with pytest.raises(KeyboardInterrupt): |     with pytest.raises(KeyboardInterrupt): | ||||||
|         tractor.run( |         tractor.run( | ||||||
|             spawn_and_check_registry, |             spawn_and_check_registry, | ||||||
|             arb_addr, |             arb_addr, | ||||||
|             use_signal, |             use_signal, | ||||||
|             True, |             True, | ||||||
|  |             with_streaming, | ||||||
|             # XXX: required to use remote daemon! |             # XXX: required to use remote daemon! | ||||||
|             arbiter_addr=arb_addr |             arbiter_addr=arb_addr | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue