Skip to content

Commit 74534ea

Browse files
Improve publisher pool reliability []
1 parent b277592 commit 74534ea

2 files changed

Lines changed: 50 additions & 10 deletions

File tree

  • dev_tools

‎dev_tools/pylibs/edgacknowledg/forge/publisher/pool.py‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,25 @@ async def _replace(self, slot: _Slot) -> None:
327327

328328
async def _start(self, slot: _Slot) -> None:
329329
'''Start ``slot``'s process and wait until it is ready; raise
330-
``PoolError`` (and delay the next try) if it isn't.
330+
``PoolError`` (and delay the next try) if it isn't, whatever the
331+
reason (e.g. no process could be started at all).
332+
'''
333+
try:
334+
await self._launch(slot)
335+
except Exception as ex:
336+
returncode = await self._stop(slot)
337+
slot.process = None
338+
slot.failed_starts += 1
339+
delay = min(2.0 ** (slot.failed_starts - 1), _MAX_RESTART_DELAY_S)
340+
slot.not_before = time.monotonic() + delay
341+
_log.error('pool process %d failed to start (%s; exit status %s); '
342+
'trying again in %g s', slot.index, ex, returncode, delay)
343+
raise PoolError(f"pool process {slot.index} failed to start") from ex
344+
slot.failed_starts = 0
345+
346+
async def _launch(self, slot: _Slot) -> None:
347+
'''Start ``slot``'s process and wait until it is ready; raise if it
348+
isn't.
331349
'''
332350
parent_end, process_end = socket.socketpair()
333351
try:
@@ -360,15 +378,7 @@ async def _start(self, slot: _Slot) -> None:
360378
self._kill(slot)
361379
raise
362380
if ready.strip() != b'{"ready": true}':
363-
returncode = await self._stop(slot)
364-
slot.process = None
365-
slot.failed_starts += 1
366-
delay = min(2.0 ** (slot.failed_starts - 1), _MAX_RESTART_DELAY_S)
367-
slot.not_before = time.monotonic() + delay
368-
_log.error('pool process %d failed to start (%s); trying again in '
369-
'%g s', slot.index, returncode, delay)
370-
raise PoolError(f"pool process {slot.index} failed to start")
371-
slot.failed_starts = 0
381+
raise PoolError('it never said it was ready')
372382
_log.info('started pool process %d (pid %d)', slot.index, process.pid)
373383

374384
def _kill(self, slot: _Slot) -> None:

‎dev_tools/services/acknowledg-forge/tests/test_pool.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
'''
99

1010
import asyncio
11+
import errno
1112
import os
1213
import signal
1314
import time
@@ -236,6 +237,35 @@ async def test_a_process_failing_to_start_backs_off(
236237
await pool.close()
237238

238239

240+
async def test_no_process_starting_at_all_backs_off(
241+
env: _Env,
242+
monkeypatch: pytest.MonkeyPatch) -> None:
243+
create_subprocess_exec = asyncio.create_subprocess_exec
244+
failing = True
245+
246+
async def out_of_processes(*args: Any, **kwargs: Any) -> Any:
247+
if failing:
248+
raise OSError(errno.EAGAIN, 'Resource temporarily unavailable')
249+
return await create_subprocess_exec(*args, **kwargs)
250+
251+
monkeypatch.setattr(asyncio, 'create_subprocess_exec', out_of_processes)
252+
pool = ProcessPool(tools_service_url = env.tools.url, processes = 1)
253+
try:
254+
# As when it starts but isn't ready.
255+
with pytest.raises(PoolError, match = 'failed to start'):
256+
await pool.start()
257+
with pytest.raises(PoolError, match = 'restarting'):
258+
await pool.answer(env.section, _show_source('suite/a.ifc'))
259+
260+
# Its dispatcher still tries again, once it may.
261+
failing = False
262+
await asyncio.sleep(1.5)
263+
answer = await pool.answer(env.section, _show_source('suite/a.ifc'))
264+
assert 'dumped a.ifc' in _content(answer)
265+
finally:
266+
await pool.close()
267+
268+
239269
class _Channel:
240270
'''One request in; the replies the stand-in handler relays, out.'''
241271

0 commit comments

Comments
 (0)