Skip to content

Commit b689c15

Browse files
Santti4gowjwwood
authored andcommitted
Add maximum times for a process to respawn (#696)
* Add support to set respawn process amount Signed-off-by: Santti4go <[email protected]>
1 parent b5ad5e6 commit b689c15

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

launch/launch/actions/execute_local.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def __init__(
100100
]] = None,
101101
respawn: Union[bool, SomeSubstitutionsType] = False,
102102
respawn_delay: Optional[float] = None,
103+
respawn_max_retries: int = -1,
103104
**kwargs
104105
) -> None:
105106
"""
@@ -202,6 +203,8 @@ def __init__(
202203
:param: respawn if 'True', relaunch the process that abnormally died.
203204
Either a boolean or a Substitution to be resolved at runtime. Defaults to 'False'.
204205
:param: respawn_delay a delay time to relaunch the died process if respawn is 'True'.
206+
:param: respawn_max_retries number of times to respawn the process if respawn is 'True'.
207+
A negative value will respawn an infinite number of times (default behavior).
205208
"""
206209
super().__init__(**kwargs)
207210
self.__process_description = process_description
@@ -228,6 +231,9 @@ def __init__(
228231
self.__respawn = normalize_typed_substitution(respawn, bool)
229232
self.__respawn_delay = respawn_delay
230233

234+
self.__respawn_max_retries = respawn_max_retries
235+
self.__respawn_retries = 0
236+
231237
self.__process_event_args = None # type: Optional[Dict[Text, Any]]
232238
self._subprocess_protocol = None # type: Optional[Any]
233239
self._subprocess_transport = None
@@ -637,7 +643,11 @@ async def __execute_process(self, context: LaunchContext) -> None:
637643
if not context.is_shutdown\
638644
and self.__shutdown_future is not None\
639645
and not self.__shutdown_future.done()\
640-
and self.__respawn:
646+
and self.__respawn and \
647+
(self.__respawn_max_retries < 0 or
648+
self.__respawn_retries < self.__respawn_max_retries):
649+
# Increase the respawn_retries counter
650+
self.__respawn_retries += 1
641651
if self.__respawn_delay is not None and self.__respawn_delay > 0.0:
642652
# wait for a timeout(`self.__respawn_delay`) to respawn the process
643653
# and handle shutdown event with future(`self.__shutdown_future`)

launch/launch/actions/execute_process.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ def __init__(
232232
:param: respawn if 'True', relaunch the process that abnormally died.
233233
Defaults to 'False'.
234234
:param: respawn_delay a delay time to relaunch the died process if respawn is 'True'.
235+
:param: respawn_max_retries number of times to respawn the process if respawn is 'True'.
236+
A negative value will respawn an infinite number of times (default behavior).
235237
"""
236238
executable = Executable(
237239
cmd=cmd,
@@ -350,6 +352,12 @@ def parse(
350352
if respawn is not None:
351353
kwargs['respawn'] = parser.parse_substitution(respawn)
352354

355+
if 'respawn_max_retries' not in ignore:
356+
respawn_max_retries = entity.get_attr('respawn_max_retries', data_type=int,
357+
optional=True)
358+
if respawn_max_retries is not None:
359+
kwargs['respawn_max_retries'] = respawn_max_retries
360+
353361
if 'respawn_delay' not in ignore:
354362
respawn_delay = entity.get_attr('respawn_delay', data_type=float, optional=True)
355363
if respawn_delay is not None:

launch/test/launch/test_execute_local.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,46 @@ def generate_launch_description():
129129
assert expected_called_count == on_exit_callback.called_count
130130

131131

132+
def test_execute_process_with_respawn_max_retries():
133+
"""Test launching a process with respawn_max_retries attribute."""
134+
def on_exit_callback(event, context):
135+
on_exit_callback.called_count += 1
136+
if on_exit_callback.called_count == expected_called_count:
137+
timer = TimerAction(
138+
period=2., # wait to verify if the process continues to respawn itself
139+
actions=[
140+
Shutdown(reason='Timer expired')
141+
]
142+
)
143+
timer.execute(context)
144+
on_exit_callback.called_count = 0
145+
146+
respawn_max_retries = 2 # we want the process to respawn this amount of times
147+
expected_called_count = 3 # normal exit + respawn_max_retries exits
148+
shutdown_time = 10.0 # security timer to kill the process
149+
150+
def generate_launch_description():
151+
return LaunchDescription([
152+
153+
ExecuteLocal(
154+
process_description=Executable(cmd=[sys.executable, '-c', "print('action')"]),
155+
respawn=True, respawn_max_retries=respawn_max_retries, on_exit=on_exit_callback
156+
),
157+
158+
TimerAction(
159+
period=shutdown_time,
160+
actions=[
161+
Shutdown(reason='Timer expired')
162+
]
163+
)
164+
])
165+
166+
ls = LaunchService()
167+
ls.include_launch_description(generate_launch_description())
168+
assert 0 == ls.run()
169+
assert expected_called_count == on_exit_callback.called_count
170+
171+
132172
def test_execute_process_with_output_dictionary():
133173
"""Test launching a process works when output is specified as a dictionary."""
134174
executable = ExecuteLocal(

launch/test/launch/test_execute_process.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,47 @@ def generate_launch_description():
220220
assert expected_called_count == on_exit_callback.called_count
221221

222222

223+
def test_execute_process_with_respawn_max_retries():
224+
"""Test launching a process with respawn_max_retries attribute."""
225+
def on_exit_callback(event, context):
226+
on_exit_callback.called_count += 1
227+
if on_exit_callback.called_count == expected_called_count:
228+
timer = TimerAction(
229+
period=2., # wait to verify if the process continues to respawn itself
230+
actions=[
231+
Shutdown(reason='Timer expired')
232+
]
233+
)
234+
timer.execute(context)
235+
on_exit_callback.called_count = 0
236+
237+
respawn_max_retries = 2 # we want the process to respawn this amount of times
238+
expected_called_count = 3 # normal exit + respawn_max_retries exits
239+
shutdown_time = 10.0 # security timer to kill the process
240+
241+
def generate_launch_description():
242+
return LaunchDescription([
243+
244+
ExecuteProcess(
245+
cmd=[sys.executable, '-c', "print('action')"],
246+
respawn=True, respawn_max_retries=respawn_max_retries,
247+
on_exit=on_exit_callback
248+
),
249+
250+
TimerAction(
251+
period=shutdown_time,
252+
actions=[
253+
Shutdown(reason='Timer expired')
254+
]
255+
)
256+
])
257+
258+
ls = LaunchService()
259+
ls.include_launch_description(generate_launch_description())
260+
assert 0 == ls.run()
261+
assert expected_called_count == on_exit_callback.called_count
262+
263+
223264
def test_execute_process_prefix_filter_match():
224265
lc = LaunchContext()
225266
lc._set_asyncio_loop(osrf_pycommon.process_utils.get_loop())

0 commit comments

Comments
 (0)