Skip to content

Commit

Permalink
Prevent concurrent stop / start attempts
Browse files Browse the repository at this point in the history
Add a few more tests too
  • Loading branch information
yuvipanda committed Dec 23, 2018
1 parent e893b83 commit 2f87e08
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 21 deletions.
41 changes: 26 additions & 15 deletions simperviser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,45 @@ def __init__(self, *args, always_restart=False, **kwargs):
self._proc_args = args
self._proc_kwargs = kwargs
self.proc: asyncio.Process = None

# asyncio.Process has no 'poll', so we keep that state internally
self.running = False

# Don't restart process if we explicitly kill it
self._killed = False

# Only one coroutine should be starting or killing a process at a time
self._start_stop_lock = asyncio.Lock()

async def start(self):
"""
Start the process
"""
self.proc = await asyncio.create_subprocess_exec(
*self._proc_args, **self._proc_kwargs
)
with (await self._start_stop_lock):
if self.running:
# Don't wanna start it again, if we're already running
return
self.proc = await asyncio.create_subprocess_exec(
*self._proc_args, **self._proc_kwargs
)
self._killed = False
self.running = True

# Spin off a coroutine to watch, reap & restart process if needed
asyncio.ensure_future(self._restart_process_if_needed())

async def _restart_process_if_needed(self):
retcode = await self.proc.wait()
self.running = False
if self.always_restart or retcode != 0:
if (not self._killed) and (self.always_restart or retcode != 0):
await self.start()

async def kill(self):
self._killed = True
with (await self._start_stop_lock):
self.proc.kill()
return await self.proc.wait()

@property
def pid(self):
return self.proc.pid


async def main():
sp = SupervisedProcess('/bin/bash', '-c', 'echo $HI; exit 1', always_restart=False, env={'HI': 'BOO'})
await sp.start()

if __name__ == '__main__':
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.run_forever()
return self.proc.pid
63 changes: 57 additions & 6 deletions test_simperviser.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,86 @@
import asyncio
import pytest
import sys

import simperviser

SLEEP_TIME = 0.5
SLEEP_WAIT_TIME = 0.7

def sleep(retcode=0, time=SLEEP_TIME):
"""
Sleep for {time} seconds & return code {retcode}
"""
return [
sys.executable,
'-c', f'import sys, time; time.sleep({time}); sys.exit({retcode})'
]

@pytest.mark.asyncio
async def test_start_success():
"""
Start a process & check its running status
"""
proc = simperviser.SupervisedProcess(
'/bin/bash', '-c', 'sleep 0.5', always_restart=False
*sleep(0), always_restart=False
)
await proc.start()
assert proc.running
await asyncio.sleep(0.51)
await asyncio.sleep(SLEEP_WAIT_TIME)
assert not proc.running

@pytest.mark.asyncio
async def test_start_always_restarting():
"""
Start a process & check it restarts even when it succeeds
"""
proc = simperviser.SupervisedProcess(
*sleep(0), always_restart=True
)
await proc.start()
assert proc.running
first_pid = proc.pid
await asyncio.sleep(SLEEP_WAIT_TIME)
# Process should have restarted by now
assert proc.running
# Make sure it is a new process
assert proc.pid != first_pid

await proc.kill()
assert not proc.running

@pytest.mark.asyncio
async def test_start_restarting():
async def test_start_fail_restarting():
"""
Start a process that fails & make sure it restarts
"""
proc = simperviser.SupervisedProcess(
'/bin/bash', '-c', 'sleep 0.5; exit 1', always_restart=False
*sleep(1), always_restart=True
)
await proc.start()
assert proc.running
first_pid = proc.pid
await asyncio.sleep(0.51)
await asyncio.sleep(SLEEP_WAIT_TIME)
# Process should have restarted by now
assert proc.running
# Make sure it is a new process
assert proc.pid != first_pid
assert proc.pid != first_pid

await proc.kill()
assert not proc.running


@pytest.mark.asyncio
async def test_start_multiple_start():
"""
Starting the same process multiple times should be a noop
"""
proc = simperviser.SupervisedProcess(
*sleep(0), always_restart=True
)
await proc.start()
assert proc.running
first_pid = proc.pid
await proc.start()
assert proc.running
assert proc.pid == first_pid

0 comments on commit 2f87e08

Please sign in to comment.