added 'task timeout' feature (#69284)

* added 'task timeout' feature


Co-authored-by: Abhijeet Kasurde <akasurde@redhat.com>
This commit is contained in:
Brian Coca 2020-05-19 17:20:08 -04:00 committed by GitHub
parent ac533403e3
commit df4e83deda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 46 additions and 0 deletions

View file

@ -68,6 +68,7 @@ serial: |
strategy: Allows you to choose the connection plugin to use for the play. strategy: Allows you to choose the connection plugin to use for the play.
tags: Tags applied to the task or included tasks, this allows selecting subsets of tasks from the command line. tags: Tags applied to the task or included tasks, this allows selecting subsets of tasks from the command line.
tasks: Main list of tasks to execute in the play, they run after :term:`roles` and before :term:`post_tasks`. tasks: Main list of tasks to execute in the play, they run after :term:`roles` and before :term:`post_tasks`.
timeout: Time limit for task to execute in, if exceeded Ansible will interrupt and fail the task.
throttle: Limit number of concurrent task runs on task, block and playbook level. This is independent of the forks and serial settings, but cannot be set higher than those limits. For example, if forks is set to 10 and the throttle is set to 15, at most 10 hosts will be operated on in parallel. throttle: Limit number of concurrent task runs on task, block and playbook level. This is independent of the forks and serial settings, but cannot be set higher than those limits. For example, if forks is set to 10 and the throttle is set to 15, at most 10 hosts will be operated on in parallel.
until: "This keyword implies a ':term:`retries` loop' that will go on until the condition supplied here is met or we hit the :term:`retries` limit." until: "This keyword implies a ':term:`retries` loop' that will go on until the condition supplied here is met or we hit the :term:`retries` limit."
vars: Dictionary/map of variables vars: Dictionary/map of variables

View file

@ -1844,6 +1844,17 @@ TAGS_SKIP:
ini: ini:
- {key: skip, section: tags} - {key: skip, section: tags}
version_added: "2.5" version_added: "2.5"
TASK_TIMEOUT:
name: Task Timeout
default: 0
description:
- Set the maximum time (in seconds) that a task can run for.
- If set to 0 (the default) there is no timeout.
env: [{name: ANSIBLE_TASK_TIMEOUT}]
ini:
- {key: task_timeout, section: defaults}
type: integer
version_added: '2.10'
WORKER_SHUTDOWN_POLL_COUNT: WORKER_SHUTDOWN_POLL_COUNT:
name: Worker Shutdown Poll Count name: Worker Shutdown Poll Count
default: 0 default: 0

View file

@ -9,6 +9,7 @@ import re
import pty import pty
import time import time
import json import json
import signal
import subprocess import subprocess
import sys import sys
import termios import termios
@ -39,6 +40,14 @@ display = Display()
__all__ = ['TaskExecutor'] __all__ = ['TaskExecutor']
class TaskTimeoutError(BaseException):
pass
def task_timeout(signum, frame):
raise TaskTimeoutError
def remove_omit(task_args, omit_token): def remove_omit(task_args, omit_token):
''' '''
Remove args with a value equal to the ``omit_token`` recursively Remove args with a value equal to the ``omit_token`` recursively
@ -651,6 +660,9 @@ class TaskExecutor:
for attempt in xrange(1, retries + 1): for attempt in xrange(1, retries + 1):
display.debug("running the handler") display.debug("running the handler")
try: try:
if self._task.timeout:
old_sig = signal.signal(signal.SIGALRM, task_timeout)
signal.alarm(self._task.timeout)
result = self._handler.run(task_vars=variables) result = self._handler.run(task_vars=variables)
except AnsibleActionSkip as e: except AnsibleActionSkip as e:
return dict(skipped=True, msg=to_text(e)) return dict(skipped=True, msg=to_text(e))
@ -658,7 +670,13 @@ class TaskExecutor:
return dict(failed=True, msg=to_text(e)) return dict(failed=True, msg=to_text(e))
except AnsibleConnectionFailure as e: except AnsibleConnectionFailure as e:
return dict(unreachable=True, msg=to_text(e)) return dict(unreachable=True, msg=to_text(e))
except TaskTimeoutError as e:
msg = 'The %s action failed to execute in the expected time frame (%d) and was terminated' % (self._task.action, self._task.timeout)
return dict(failed=True, msg=msg)
finally: finally:
if self._task.timeout:
signal.alarm(0)
old_sig = signal.signal(signal.SIGALRM, old_sig)
self._handler.cleanup() self._handler.cleanup()
display.debug("handler run complete") display.debug("handler run complete")

View file

@ -614,6 +614,7 @@ class Base(FieldAttributeBase):
_diff = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('diff')) _diff = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('diff'))
_any_errors_fatal = FieldAttribute(isa='bool', default=C.ANY_ERRORS_FATAL) _any_errors_fatal = FieldAttribute(isa='bool', default=C.ANY_ERRORS_FATAL)
_throttle = FieldAttribute(isa='int', default=0) _throttle = FieldAttribute(isa='int', default=0)
_timeout = FieldAttribute(isa='int', default=C.TASK_TIMEOUT)
# explicitly invoke a debugger on tasks # explicitly invoke a debugger on tasks
_debugger = FieldAttribute(isa='string') _debugger = FieldAttribute(isa='string')

View file

@ -4,3 +4,6 @@ set -eux
# run type tests # run type tests
ansible-playbook -i ../../inventory types.yml -v "$@" ansible-playbook -i ../../inventory types.yml -v "$@"
# test timeout
ansible-playbook -i ../../inventory timeout.yml -v "$@"

View file

@ -0,0 +1,12 @@
- hosts: localhost
gather_facts: false
tasks:
- shell: sleep 100
timeout: 1
ignore_errors: true
register: time
- assert:
that:
- time is failed
- '"The command action failed to execute in the expected time frame" in time["msg"]'