aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From wick...@apache.org
Subject aurora git commit: Split http lifecycle into a composition layer.
Date Wed, 01 Jul 2015 17:44:11 GMT
Repository: aurora
Updated Branches:
  refs/heads/master 616ef10e6 -> 9a613deae


Split http lifecycle into a composition layer.

Move shutdown endpoints to the Job config since the lifecycle is controlled
by Aurora and not Thermos.  Split the lifecycle management into a
composition layer that can more readily be tested.

Bugs closed: AURORA-1368

Reviewed at https://reviews.apache.org/r/35847/


Project: http://git-wip-us.apache.org/repos/asf/aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/9a613dea
Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/9a613dea
Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/9a613dea

Branch: refs/heads/master
Commit: 9a613deaec23006446b336aaf9f54e7bef77babb
Parents: 616ef10
Author: Brian Wickman <wickman@apache.org>
Authored: Wed Jul 1 10:38:21 2015 -0700
Committer: Brian Wickman <wickman@apache.org>
Committed: Wed Jul 1 10:38:21 2015 -0700

----------------------------------------------------------------------
 docs/configuration-reference.md                 | 46 +++++++---
 .../python/apache/aurora/config/schema/base.py  | 35 ++++++--
 src/main/python/apache/aurora/config/thrift.py  |  2 +
 src/main/python/apache/aurora/executor/BUILD    | 14 ++-
 .../apache/aurora/executor/http_lifecycle.py    | 93 ++++++++++++++++++++
 .../aurora/executor/thermos_task_runner.py      | 24 +----
 .../python/apache/thermos/config/schema_base.py |  4 -
 src/test/python/apache/aurora/executor/BUILD    | 11 +++
 .../apache/aurora/executor/common/fixtures.py   |  7 +-
 .../aurora/executor/test_http_lifecycle.py      | 86 ++++++++++++++++++
 .../aurora/executor/test_thermos_task_runner.py | 20 +++--
 11 files changed, 287 insertions(+), 55 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/docs/configuration-reference.md
----------------------------------------------------------------------
diff --git a/docs/configuration-reference.md b/docs/configuration-reference.md
index 7bfd633..dafd306 100644
--- a/docs/configuration-reference.md
+++ b/docs/configuration-reference.md
@@ -30,6 +30,7 @@ Aurora + Thermos Configuration Reference
     - [HealthCheckConfig Objects](#healthcheckconfig-objects)
     - [Announcer Objects](#announcer-objects)
     - [Container Objects](#container)
+    - [LifecycleConfig Objects](#lifecycleconfig-objects)
 - [Specifying Scheduling Constraints](#specifying-scheduling-constraints)
 - [Template Namespaces](#template-namespaces)
     - [mesos Namespace](#mesos-namespace)
@@ -162,8 +163,6 @@ can be omitted. In Mesos, `resources` is also required.
    ```max_failures```      | Integer                          | Maximum process failures
before being considered failed (Default: 1)
    ```max_concurrency```   | Integer                          | Maximum number of concurrent
processes (Default: 0, unlimited concurrency.)
    ```finalization_wait``` | Integer                          | Amount of time allocated
for finalizing processes, in seconds. (Default: 30)
-   ```graceful_shutdown_endpoint``` | String                  | Endpoint to hit to indicate
that a task should gracefully shutdown. (Default: /quitquitquit)
-   ```shutdown_endpoint``` | String                           | Endpoint to hit to give a
task its final warning before being killed. (Default: /abortabortabort)
 
 #### name
 `name` is a string denoting the name of this task. It defaults to the name of the first Process
in
@@ -279,17 +278,6 @@ Client applications with higher priority may force a shorter
 finalization wait (e.g. through parameters to `thermos kill`), so this
 is mostly a best-effort signal.
 
-#### graceful_shutdown_endpoint
-
-If the Job has a port named `health`, a HTTP POST request will be sent over
-localhost to this endpoint to request that the task gracefully shut itself
-down.
-
-#### shutdown_endpoint
-
-If the Job has a port named `health`, a HTTP POST request will be sent over
-localhost to this endpoint to request as a final warning before being shut
-down.
 
 ### Constraint Object
 
@@ -339,6 +327,7 @@ Job Schema
   ```production``` | Boolean |  Whether or not this is a production task backed by quota
(Default: False). Production jobs may preempt any non-production job, and may only be preempted
by production jobs in the same role and of higher priority. To run jobs at this level, the
job role must have the appropriate quota. To grant quota to a particular role in production,
operators use the ``aurora_admin set_quota`` command.
   ```health_check_config``` | ```heath_check_config``` object | Parameters for controlling
a task's health checks via HTTP. Only used if a  health port was assigned with a command line
wildcard.
   ```container``` | ```Container``` object | An optional container to run all processes inside
of.
+  ```lifecycle``` | ```LifecycleConfig``` object | An optional task lifecycle configuration
that dictates commands to be executed on startup/teardown.  HTTP lifecycle is enabled by default
if the "health" port is requested.  See [LifecycleConfig Objects](#lifecycleconfig-objects)
for more information.
 
 ### Services
 
@@ -432,6 +421,37 @@ Describes the container the job's processes will run inside.
   -----          | :----:         | -----------
   ```image```    | String         | The name of the docker image to execute.  If the image
does not exist locally it will be pulled with ```docker pull```.
 
+### LifecycleConfig Objects
+
+*Note: The only lifecycle configuration supported is the HTTP lifecycle via the HTTPLifecycleConfig.*
+
+  param          | type                | description
+  -----          | :----:              | -----------
+  ```http```     | HTTPLifecycleConfig | Configure the lifecycle manager to send lifecycle
commands to the task via HTTP.
+
+### HTTPLifecycleConfig Objects
+
+  param          | type            | description
+  -----          | :----:          | -----------
+  ```port```     | String          | The named port to send POST commands (Default: health)
+  ```graceful_shutdown_endpoint``` | String | Endpoint to hit to indicate that a task should
gracefully shutdown. (Default: /quitquitquit)
+  ```shutdown_endpoint``` | String | Endpoint to hit to give a task its final warning before
being killed. (Default: /abortabortabort)
+
+#### graceful_shutdown_endpoint
+
+If the Job is listening on the port as specified by the HTTPLifecycleConfig
+(default: `health`), a HTTP POST request will be sent over localhost to this
+endpoint to request that the task gracefully shut itself down.  This is a
+courtesy call before the `shutdown_endpoint` is invoked a fixed amount of
+time later.
+
+#### shutdown_endpoint
+
+If the Job is listening on the port as specified by the HTTPLifecycleConfig
+(default: `health`), a HTTP POST request will be sent over localhost to this
+endpoint to request as a final warning before being shut down.  If the task
+does not shut down on its own after this, it will be forcefully killed
+
 
 Specifying Scheduling Constraints
 =================================

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/main/python/apache/aurora/config/schema/base.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/config/schema/base.py b/src/main/python/apache/aurora/config/schema/base.py
index 9a6f8a1..d1f1e4f 100644
--- a/src/main/python/apache/aurora/config/schema/base.py
+++ b/src/main/python/apache/aurora/config/schema/base.py
@@ -45,6 +45,25 @@ class HealthCheckConfig(Struct):
   expected_response_code   = Default(Integer, 0)
 
 
+class HttpLifecycleConfig(Struct):
+  # Named port to POST shutdown endpoints
+  port = Default(String, 'health')
+
+  # Endpoint to hit to indicate that a task should gracefully shutdown.
+  graceful_shutdown_endpoint = Default(String, '/quitquitquit')
+
+  # Endpoint to hit to give a task it's final warning before being killed.
+  shutdown_endpoint = Default(String, '/abortabortabort')
+
+
+class LifecycleConfig(Struct):
+  http = HttpLifecycleConfig
+
+
+DisableLifecycle = LifecycleConfig()
+DefaultLifecycleConfig = LifecycleConfig(http = HttpLifecycleConfig())
+
+
 class Announcer(Struct):
   primary_port = Default(String, 'http')
 
@@ -61,12 +80,13 @@ class Announcer(Struct):
 
 # The executorConfig populated inside of TaskConfig.
 class MesosTaskInstance(Struct):
-  task                       = Required(Task)
-  instance                   = Required(Integer)
-  role                       = Required(String)
-  announce                   = Announcer
-  environment                = Required(String)
-  health_check_config        = Default(HealthCheckConfig, HealthCheckConfig())
+  task                = Required(Task)
+  instance            = Required(Integer)
+  role                = Required(String)
+  announce            = Announcer
+  environment         = Required(String)
+  health_check_config = Default(HealthCheckConfig, HealthCheckConfig())
+  lifecycle           = LifecycleConfig
 
 
 class Docker(Struct):
@@ -98,11 +118,14 @@ class MesosJob(Struct):
   production                 = Default(Boolean, False)
   priority                   = Default(Integer, 0)
   health_check_config        = Default(HealthCheckConfig, HealthCheckConfig())
+  # TODO(wickman) Make Default(Any, LifecycleConfig()) once pystachio #17 is addressed.
+  lifecycle                  = Default(LifecycleConfig, DefaultLifecycleConfig)
   task_links                 = Map(String, String)  # Unsupported.  See AURORA-739
 
   enable_hooks = Default(Boolean, False)  # enable client API hooks; from env python-list
'hooks'
 
   container = Container
 
+
 Job = MesosJob
 Service = Job(service = True)

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/main/python/apache/aurora/config/thrift.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/config/thrift.py b/src/main/python/apache/aurora/config/thrift.py
index 0a3e910..88dd1c7 100644
--- a/src/main/python/apache/aurora/config/thrift.py
+++ b/src/main/python/apache/aurora/config/thrift.py
@@ -90,6 +90,8 @@ def task_instance_from_job(job, instance):
     ti = ti(announce=job.announce())
   if job.has_environment():
     ti = ti(environment=job.environment())
+  if job.has_lifecycle():
+    ti = ti(lifecycle=job.lifecycle())
   return ti.bind(mesos=instance_context)
 
 

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/main/python/apache/aurora/executor/BUILD
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/executor/BUILD b/src/main/python/apache/aurora/executor/BUILD
index 52be02b..891cbe6 100644
--- a/src/main/python/apache/aurora/executor/BUILD
+++ b/src/main/python/apache/aurora/executor/BUILD
@@ -29,14 +29,13 @@ python_library(
     'src/main/python/apache/thermos/config:schema',
     'src/main/python/apache/thermos/core',
     'src/main/python/apache/thermos/monitoring:monitor',
-    'src/main/python/apache/aurora/common:http_signaler',
     'src/main/python/apache/aurora/executor/common:status_checker',
     'src/main/python/apache/aurora/executor/common:task_info',
     'src/main/python/apache/aurora/executor/common:task_runner',
+    ':http_lifecycle',
   ]
 )
 
-
 python_library(
   name = 'executor_vars',
   sources = ['executor_vars.py'],
@@ -51,6 +50,17 @@ python_library(
 )
 
 python_library(
+  name = 'http_lifecycle',
+  sources = ['http_lifecycle.py'],
+  dependencies = [
+    '3rdparty/python:twitter.common.log',
+    '3rdparty/python:twitter.common.quantity',
+    'src/main/python/apache/aurora/common:http_signaler',
+    'src/main/python/apache/aurora/executor/common:task_runner',
+  ]
+)
+
+python_library(
   name = 'status_manager',
   sources = ['status_manager.py'],
   dependencies = [

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/main/python/apache/aurora/executor/http_lifecycle.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/executor/http_lifecycle.py b/src/main/python/apache/aurora/executor/http_lifecycle.py
new file mode 100644
index 0000000..6d578cc
--- /dev/null
+++ b/src/main/python/apache/aurora/executor/http_lifecycle.py
@@ -0,0 +1,93 @@
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+
+from twitter.common import log
+from twitter.common.quantity import Amount, Time
+
+from apache.aurora.common.http_signaler import HttpSignaler
+
+from .common.task_runner import TaskError, TaskRunner
+
+
+class HttpLifecycleManager(TaskRunner):
+  """A wrapper around a TaskRunner that performs HTTP lifecycle management."""
+
+  ESCALATION_WAIT = Amount(5, Time.SECONDS)
+
+  @classmethod
+  def wrap(cls, runner, task_instance, portmap):
+    """Return a task runner that manages the http lifecycle if lifecycle is present."""
+
+    if not task_instance.has_lifecycle() or not task_instance.lifecycle().has_http():
+      return runner
+
+    http_lifecycle = task_instance.lifecycle().http()
+    http_lifecycle_port = http_lifecycle.port().get()
+
+    if not portmap or http_lifecycle_port not in portmap:
+      # If DefaultLifecycle is ever to disable task lifecycle by default, we should
+      # raise a TaskError here, since the user has requested http lifecycle without
+      # binding a port to send lifecycle commands.
+      return runner
+
+    escalation_endpoints = [
+        http_lifecycle.graceful_shutdown_endpoint().get(),
+        http_lifecycle.shutdown_endpoint().get()
+    ]
+    return cls(runner, portmap[http_lifecycle_port], escalation_endpoints)
+
+  def __init__(self,
+               runner,
+               lifecycle_port,
+               escalation_endpoints,
+               clock=time):
+    self._runner = runner
+    self._lifecycle_port = lifecycle_port
+    self._escalation_endpoints = escalation_endpoints
+    self._clock = clock
+    self.__started = False
+
+  def _terminate_http(self):
+    http_signaler = HttpSignaler(self._lifecycle_port)
+
+    for endpoint in self._escalation_endpoints:
+      handled, _ = http_signaler(endpoint, use_post_method=True)
+
+      if handled:
+        self._clock.sleep(self.ESCALATION_WAIT.as_(Time.SECONDS))
+        if self._runner.status is not None:
+          return True
+
+  # --- public interface
+  def start(self, timeout=None):
+    self.__started = True
+    return self._runner.start(timeout=timeout if timeout is not None else self._runner.MAX_WAIT)
+
+  def stop(self, timeout=None):
+    """Stop the runner.  If it's already completed, no-op.  If it's still running, issue
a kill."""
+    if not self.__started:
+      raise TaskError('Failed to call TaskRunner.start.')
+
+    log.info('Invoking runner HTTP teardown.')
+    self._terminate_http()
+
+    return self._runner.stop(timeout=timeout if timeout is not None else self._runner.MAX_WAIT)
+
+  @property
+  def status(self):
+    """Return the StatusResult of this task runner.  This returns None as
+       long as no terminal state is reached."""
+    return self._runner.status

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/main/python/apache/aurora/executor/thermos_task_runner.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/executor/thermos_task_runner.py b/src/main/python/apache/aurora/executor/thermos_task_runner.py
index 7bcd6c4..14e8b4b 100644
--- a/src/main/python/apache/aurora/executor/thermos_task_runner.py
+++ b/src/main/python/apache/aurora/executor/thermos_task_runner.py
@@ -29,7 +29,6 @@ from twitter.common.dirutil import safe_mkdtemp
 from twitter.common.log.options import LogOptions
 from twitter.common.quantity import Amount, Time
 
-from apache.aurora.common.http_signaler import HttpSignaler
 from apache.thermos.common.statuses import (
     INTERNAL_ERROR,
     INVALID_TASK,
@@ -44,6 +43,7 @@ from apache.thermos.monitoring.monitor import TaskMonitor
 from .common.status_checker import StatusResult
 from .common.task_info import mesos_task_instance_from_assigned_task, resolve_ports
 from .common.task_runner import TaskError, TaskRunner, TaskRunnerProvider
+from .http_lifecycle import HttpLifecycleManager
 
 from gen.apache.thermos.ttypes import TaskState
 
@@ -112,21 +112,6 @@ class ThermosTaskRunner(TaskRunner):
     except ThermosTaskWrapper.InvalidTask as e:
       raise TaskError('Failed to load task: %s' % e)
 
-  def _terminate_http(self):
-    if 'health' not in self._ports:
-      return
-
-    http_signaler = HttpSignaler(self._ports['health'])
-
-    for exit_endpoint in [
-        self._task.graceful_shutdown_endpoint().get(),
-        self._task.shutdown_endpoint().get()]:
-      handled, _ = http_signaler(exit_endpoint, use_post_method=True)
-      if handled:
-        self._clock.sleep(self.ESCALATION_WAIT.as_(Time.SECONDS))
-        if self.status is not None:
-          return
-
   @property
   def artifact_dir(self):
     return self._artifact_dir
@@ -318,9 +303,6 @@ class ThermosTaskRunner(TaskRunner):
     if not self.forking.is_set():
       raise TaskError('Failed to call TaskRunner.start.')
 
-    log.info('Invoking runner HTTP teardown.')
-    self._terminate_http()
-
     log.info('Invoking runner.kill')
     self.kill()
 
@@ -387,7 +369,7 @@ class DefaultThermosTaskRunnerProvider(TaskRunnerProvider):
       POLL_INTERVAL = self._poll_interval
       THERMOS_PREEMPTION_WAIT = self._preemption_wait
 
-    return ProvidedThermosTaskRunner(
+    runner = ProvidedThermosTaskRunner(
         self._pex_location,
         task_id,
         mesos_task.task(),
@@ -399,6 +381,8 @@ class DefaultThermosTaskRunnerProvider(TaskRunnerProvider):
         clock=self._clock,
         hostname=assigned_task.slaveHost)
 
+    return HttpLifecycleManager.wrap(runner, mesos_task, mesos_ports)
+
 
 class UserOverrideThermosTaskRunnerProvider(DefaultThermosTaskRunnerProvider):
   def set_role(self, role):

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/main/python/apache/thermos/config/schema_base.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/thermos/config/schema_base.py b/src/main/python/apache/thermos/config/schema_base.py
index a85def9..f9143cc 100644
--- a/src/main/python/apache/thermos/config/schema_base.py
+++ b/src/main/python/apache/thermos/config/schema_base.py
@@ -74,10 +74,6 @@ class Task(Struct):
                                             # > 0 is max concurrent processes.
   finalization_wait = Default(Integer, 30)  # the amount of time in seconds we allocate to
run the
                                             # finalization schedule.
-  # Endpoint to hit to indicate that a task should gracefully shutdown.
-  graceful_shutdown_endpoint = Default(String, "/quitquitquit")
-  # Endpoint to hit to give a task it's final warning before being killed.
-  shutdown_endpoint = Default(String, "/abortabortabort")
 
   # TODO(jon): remove/replace with proper solution to MESOS-3546
   user = String

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/test/python/apache/aurora/executor/BUILD
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/executor/BUILD b/src/test/python/apache/aurora/executor/BUILD
index e1c635c..8fff66e 100644
--- a/src/test/python/apache/aurora/executor/BUILD
+++ b/src/test/python/apache/aurora/executor/BUILD
@@ -25,6 +25,7 @@ python_test_suite(
   dependencies = [
     ':executor_base',
     ':executor_vars',
+    ':http_lifecycle',
     ':status_manager',
     ':thermos_task_runner',
     'src/test/python/apache/aurora/executor/common:all',
@@ -103,3 +104,13 @@ python_tests(name = 'executor_vars',
     'src/main/python/apache/aurora/executor:executor_vars',
   ]
 )
+
+python_tests(name = 'http_lifecycle',
+  sources = ['test_http_lifecycle.py'],
+  dependencies = [
+    '3rdparty/python:mock',
+    'src/main/python/apache/aurora/config:schema',
+    'src/main/python/apache/aurora/executor:http_lifecycle',
+    'src/main/python/apache/aurora/executor:thermos_task_runner',
+  ],
+)

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/test/python/apache/aurora/executor/common/fixtures.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/executor/common/fixtures.py b/src/test/python/apache/aurora/executor/common/fixtures.py
index 37d032b..ebcbefa 100644
--- a/src/test/python/apache/aurora/executor/common/fixtures.py
+++ b/src/test/python/apache/aurora/executor/common/fixtures.py
@@ -15,6 +15,7 @@
 import getpass
 
 from apache.aurora.config.schema.base import (
+    DefaultLifecycleConfig,
     MB,
     MesosJob,
     MesosTaskInstance,
@@ -23,7 +24,11 @@ from apache.aurora.config.schema.base import (
     Task
 )
 
-BASE_MTI = MesosTaskInstance(instance=0, role=getpass.getuser())
+BASE_MTI = MesosTaskInstance(
+    instance=0,
+    lifecycle=DefaultLifecycleConfig,
+    role=getpass.getuser(),
+)
 BASE_TASK = Task(resources=Resources(cpu=1.0, ram=16 * MB, disk=32 * MB))
 
 HELLO_WORLD_TASK_ID = 'hello_world-001'

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/test/python/apache/aurora/executor/test_http_lifecycle.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/executor/test_http_lifecycle.py b/src/test/python/apache/aurora/executor/test_http_lifecycle.py
new file mode 100644
index 0000000..a967e34
--- /dev/null
+++ b/src/test/python/apache/aurora/executor/test_http_lifecycle.py
@@ -0,0 +1,86 @@
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from contextlib import contextmanager
+
+import mock
+
+from apache.aurora.config.schema.base import HttpLifecycleConfig, LifecycleConfig, MesosTaskInstance
+from apache.aurora.executor.http_lifecycle import HttpLifecycleManager
+from apache.aurora.executor.thermos_task_runner import ThermosTaskRunner
+
+
+def test_http_lifecycle_wrapper_without_lifecycle():
+  mti_without_lifecycle = MesosTaskInstance()
+  mti_without_http_lifecycle = MesosTaskInstance(lifecycle=LifecycleConfig())
+
+  runner_mock = mock.create_autospec(ThermosTaskRunner)
+  assert HttpLifecycleManager.wrap(runner_mock, mti_without_lifecycle, {}) is runner_mock
+  assert HttpLifecycleManager.wrap(runner_mock, mti_without_http_lifecycle, {}) is runner_mock
+
+
+@contextmanager
+def make_mocks(mesos_task_instance, portmap):
+  # wrap it once health is available and validate the constructor is called as expected
+  runner_mock = mock.create_autospec(ThermosTaskRunner)
+  with mock.patch.object(HttpLifecycleManager, '__init__', return_value=None) as wrapper_init:
+    runner_wrapper = HttpLifecycleManager.wrap(runner_mock, mesos_task_instance, portmap)
+    yield runner_mock, runner_wrapper, wrapper_init
+
+
+def test_http_lifecycle_wrapper_with_lifecycle():
+  runner_mock = mock.create_autospec(ThermosTaskRunner)
+  mti = MesosTaskInstance(lifecycle=LifecycleConfig(http=HttpLifecycleConfig()))
+
+  # pass-thru if no health port has been defined -- see comment in http_lifecycle.py regarding
+  # the rationalization for this behavior.
+  with make_mocks(mti, {}) as (runner_mock, runner_wrapper, wrapper_init):
+    assert runner_mock is runner_wrapper
+    assert wrapper_init.mock_calls == []
+
+  # wrap it once health is available and validate the constructor is called as expected
+  with make_mocks(mti, {'health': 31337}) as (runner_mock, runner_wrapper, wrapper_init):
+    assert isinstance(runner_wrapper, HttpLifecycleManager)
+    assert wrapper_init.mock_calls == [
+      mock.call(runner_mock, 31337, ['/quitquitquit', '/abortabortabort'])
+    ]
+
+  # Validate that we can override ports
+  mti = MesosTaskInstance(lifecycle=LifecycleConfig(http=HttpLifecycleConfig(
+      port='http',
+      graceful_shutdown_endpoint='/frankfrankfrank',
+      shutdown_endpoint='/bobbobbob',
+  )))
+  portmap = {'http': 12345, 'admin': 54321}
+  with make_mocks(mti, portmap) as (runner_mock, runner_wrapper, wrapper_init):
+    assert isinstance(runner_wrapper, HttpLifecycleManager)
+    assert wrapper_init.mock_calls == [
+      mock.call(runner_mock, 12345, ['/frankfrankfrank', '/bobbobbob'])
+    ]
+
+
+def test_http_lifecycle_wraps_start_and_stop():
+  mti = MesosTaskInstance(lifecycle=LifecycleConfig(http=HttpLifecycleConfig()))
+  runner_mock = mock.create_autospec(ThermosTaskRunner)
+  with mock.patch.object(HttpLifecycleManager, '_terminate_http', return_value=None) as http_mock:
+    runner_wrapper = HttpLifecycleManager.wrap(runner_mock, mti, {'health': 31337})
+
+    # ensure that start and stop are properly wrapped
+    runner_wrapper.start(23.3)
+    assert runner_mock.start.mock_calls == [mock.call(timeout=23.3)]
+
+    # ensure that http teardown called when stopped
+    runner_wrapper.stop(32.2)
+    assert http_mock.mock_calls == [mock.call()]
+    assert runner_mock.stop.mock_calls == [mock.call(timeout=32.2)]

http://git-wip-us.apache.org/repos/asf/aurora/blob/9a613dea/src/test/python/apache/aurora/executor/test_thermos_task_runner.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/executor/test_thermos_task_runner.py b/src/test/python/apache/aurora/executor/test_thermos_task_runner.py
index 3569a6a..3909aa2 100644
--- a/src/test/python/apache/aurora/executor/test_thermos_task_runner.py
+++ b/src/test/python/apache/aurora/executor/test_thermos_task_runner.py
@@ -32,6 +32,7 @@ from twitter.common.quantity import Amount, Time
 
 from apache.aurora.config.schema.base import MB, MesosTaskInstance, Process, Resources, Task
 from apache.aurora.executor.common.sandbox import DirectorySandbox
+from apache.aurora.executor.http_lifecycle import HttpLifecycleManager
 from apache.aurora.executor.thermos_task_runner import ThermosTaskRunner
 from apache.thermos.common.statuses import (
     INTERNAL_ERROR,
@@ -217,29 +218,30 @@ class TestThermosTaskRunnerIntegration(object):
       assert task_runner.status is not None
       assert task_runner.status.status == mesos_pb2.TASK_KILLED
 
-  @patch('apache.aurora.executor.thermos_task_runner.HttpSignaler')
+  @patch('apache.aurora.executor.http_lifecycle.HttpSignaler')
   def test_integration_http_teardown(self, SignalerClass):
     signaler = SignalerClass.return_value
     signaler.side_effect = lambda path, use_post_method: (path != '/quitquitquit', None)
 
     clock = Mock(wraps=time)
 
-    class ShortEscalationRunner(ThermosTaskRunner):
-      ESCALATION_WAIT = Amount(1, Time.MICROSECONDS)
-
     with self.yield_sleepy(
-        ShortEscalationRunner,
+        ThermosTaskRunner,
         portmap={'health': 3141},
         clock=clock,
         sleep=1000,
         exit_code=0) as task_runner:
 
-      task_runner.start()
-      task_runner.forked.wait()
+      class ImmediateHttpLifecycleManager(HttpLifecycleManager):
+        ESCALATION_WAIT = Amount(1, Time.MICROSECONDS)
 
-      task_runner.stop()
+      http_task_runner = ImmediateHttpLifecycleManager(
+          task_runner, 3141, ['/quitquitquit', '/abortabortabort'], clock=clock)
+      http_task_runner.start()
+      task_runner.forked.wait()
+      http_task_runner.stop()
 
-      escalation_wait = call(ShortEscalationRunner.ESCALATION_WAIT.as_(Time.SECONDS))
+      escalation_wait = call(ImmediateHttpLifecycleManager.ESCALATION_WAIT.as_(Time.SECONDS))
       assert clock.sleep.mock_calls.count(escalation_wait) == 1
       assert signaler.mock_calls == [
         call('/quitquitquit', use_post_method=True),


Mime
View raw message