This is an automated email from the ASF dual-hosted git repository.

JiaLiangC pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 1e753e06ab AMBARI-26640: Prevent auto-recovery from blocking cluster 
start operations (#4187)
1e753e06ab is described below

commit 1e753e06abb2d5219b01a80befe57301381a143f
Author: jialiang <[email protected]>
AuthorDate: Mon Aug 31 08:42:19 2026 +0000

    AMBARI-26640: Prevent auto-recovery from blocking cluster start operations 
(#4187)
    
    * AMBARI-26640: Add stateful recovery topology model
    
    * AMBARI-26640: Track recovery topology by agent session
    
    * AMBARI-26640: Prevent auto-recovery from blocking cluster start operations
    
    * AMBARI-26640: Handle recovery topology during service removal
    
    * AMBARI-26640: Retrigger CI after Node cache failure
---
 .../src/main/python/ambari_agent/ActionQueue.py    | 100 ++++++++++++---
 .../python/ambari_agent/ComponentStatusExecutor.py |  98 +++++++++++---
 .../main/python/ambari_agent/HeartbeatThread.py    |   5 +-
 .../main/python/ambari_agent/RecoveryManager.py    |  86 ++++++++++++-
 .../test/python/ambari_agent/TestActionQueue.py    |  85 +++++++++++++
 .../ambari_agent/TestComponentStatusExecutor.py    |  91 +++++++++++++
 .../python/ambari_agent/TestRecoveryManager.py     | 141 +++++++++++++++++++++
 .../server/agent/ComponentStatusAgentReport.java   |  26 +++-
 .../ambari/server/agent/HeartbeatMonitor.java      |   8 ++
 .../apache/ambari/server/agent/RecoveryConfig.java |  46 ++++++-
 .../server/agent/RecoveryConfigComponent.java      |  23 +++-
 .../server/agent/RecoveryConfigDependency.java     | 113 +++++++++++++++++
 .../ambari/server/agent/RecoveryConfigHelper.java  |  81 +++++++++++-
 .../server/agent/RecoveryTopologyManager.java      |  82 ++++++++++++
 .../server/agent/stomp/AgentReportsController.java |  12 +-
 .../server/agent/stomp/HeartbeatController.java    |  12 +-
 .../server/agent/stomp/HostLevelParamsHolder.java  |  18 ++-
 .../agent/stomp/dto/ComponentStatusReports.java    |  10 ++
 .../ambari/server/metadata/RoleCommandOrder.java   |   9 ++
 .../ambari/server/metadata/RoleCommandPair.java    |   4 +-
 .../agent/ComponentStatusAgentReportTest.java      |  45 +++++++
 .../server/agent/RecoveryTopologyManagerTest.java  |  59 +++++++++
 .../agent/stomp/HostLevelParamsHolderTest.java     |  17 +++
 .../configuration/RecoveryConfigHelperTest.java    |  51 ++++++++
 24 files changed, 1172 insertions(+), 50 deletions(-)

diff --git a/ambari-agent/src/main/python/ambari_agent/ActionQueue.py 
b/ambari-agent/src/main/python/ambari_agent/ActionQueue.py
index f0f9f6ef76..9a168fde71 100644
--- a/ambari-agent/src/main/python/ambari_agent/ActionQueue.py
+++ b/ambari-agent/src/main/python/ambari_agent/ActionQueue.py
@@ -80,6 +80,8 @@ class ActionQueue(threading.Thread):
     self.parallel_execution = self.config.get_parallel_exec_option()
     self.taskIdsToCancel = set()
     self.cancelEvent = threading.Event()
+    self.recovery_command_lock = threading.RLock()
+    self.active_recovery_task_ids = set()
     self.component_status_executor = 
initializer_module.component_status_executor
     if self.parallel_execution == 1:
       logger.info(
@@ -88,22 +90,69 @@ class ActionQueue(threading.Thread):
     self.lock = threading.Lock()
 
   def put(self, commands):
-    for command in commands:
-      if "serviceName" not in command:
-        command["serviceName"] = "null"
-      if "clusterId" not in command:
-        command["clusterId"] = "null"
+    commands = list(commands)
+    with self.recovery_command_lock:
+      if any(
+        command.get("commandType") == AgentCommand.execution for command in 
commands
+      ):
+        self.preempt_recovery_commands()
 
-      logger.info(
-        "Adding {commandType} for role {role} for service {serviceName} of 
cluster_id {clusterId} to the queue".format(
-          **command
+      for command in commands:
+        if "serviceName" not in command:
+          command["serviceName"] = "null"
+        if "clusterId" not in command:
+          command["clusterId"] = "null"
+
+        logger.info(
+          "Adding {commandType} for role {role} for service {serviceName} of 
cluster_id {clusterId} to the queue".format(
+            **command
+          )
         )
+
+        if command["commandType"] == AgentCommand.background_execution:
+          self.backgroundCommandQueue.put(self.create_command_handle(command))
+        else:
+          self.commandQueue.put(command)
+
+  def preempt_recovery_commands(self):
+    """Stop hidden recovery work so server-issued commands can run 
immediately."""
+    queued_recovery_task_ids = []
+    with self.commandQueue.mutex:
+      retained_commands = []
+      for command in self.commandQueue.queue:
+        if (
+          command is not None
+          and command.get("commandType") == AgentCommand.auto_execution
+        ):
+          queued_recovery_task_ids.append(command["taskId"])
+        else:
+          retained_commands.append(command)
+      self.commandQueue.queue.clear()
+      self.commandQueue.queue.extend(retained_commands)
+
+    active_recovery_task_ids = list(self.active_recovery_task_ids)
+    if queued_recovery_task_ids or active_recovery_task_ids:
+      logger.info(
+        "Preempting auto recovery for server-issued commands. Queued task IDs: 
%s; active task IDs: %s",
+        queued_recovery_task_ids,
+        active_recovery_task_ids,
       )
 
-      if command["commandType"] == AgentCommand.background_execution:
-        self.backgroundCommandQueue.put(self.create_command_handle(command))
-      else:
-        self.commandQueue.put(command)
+    reason = "Preempted by a server-issued command"
+    for task_id in active_recovery_task_ids:
+      self.taskIdsToCancel.add(task_id)
+      self.customServiceOrchestrator.cancel_command(task_id, reason)
+
+    if active_recovery_task_ids:
+      self.cancelEvent.set()
+
+  def has_queued_server_command(self):
+    with self.commandQueue.mutex:
+      return any(
+        command is not None
+        and command.get("commandType") == AgentCommand.execution
+        for command in self.commandQueue.queue
+      )
 
   def interrupt(self):
     self.commandQueue.put(None)
@@ -186,8 +235,9 @@ class ActionQueue(threading.Thread):
     logger.info("ActionQueue thread has successfully finished")
 
   def fill_recovery_commands(self):
-    if self.recovery_manager.enabled() and not 
self.tasks_in_progress_or_pending():
-      self.put(self.recovery_manager.get_recovery_commands())
+    with self.recovery_command_lock:
+      if self.recovery_manager.enabled() and not 
self.tasks_in_progress_or_pending():
+        self.put(self.recovery_manager.get_recovery_commands())
 
   def process_background_queue_safe_empty(self):
     while not self.backgroundCommandQueue.empty():
@@ -211,6 +261,17 @@ class ActionQueue(threading.Thread):
     # make sure we log failures
     command_type = command["commandType"]
     logger.debug("Took an element of Queue (command type = %s).", command_type)
+    is_recovery_command = command_type == AgentCommand.auto_execution
+    if is_recovery_command:
+      with self.recovery_command_lock:
+        if self.has_queued_server_command():
+          logger.info(
+            "Skipping auto recovery task %s because a server-issued command is 
queued",
+            command["taskId"],
+          )
+          return
+        self.active_recovery_task_ids.add(command["taskId"])
+
     try:
       if command_type in AgentCommand.AUTO_EXECUTION_COMMAND_GROUP:
         try:
@@ -226,6 +287,11 @@ class ActionQueue(threading.Thread):
         logger.error("Unrecognized command %s", pprint.pformat(command))
     except Exception:
       logger.exception(f"Exception while processing {command_type} command")
+    finally:
+      if is_recovery_command:
+        with self.recovery_command_lock:
+          self.active_recovery_task_ids.discard(command["taskId"])
+          self.taskIdsToCancel.discard(command["taskId"])
 
   def tasks_in_progress_or_pending(self):
     return not self.commandQueue.empty() or 
self.recovery_manager.has_active_command()
@@ -244,7 +310,8 @@ class ActionQueue(threading.Thread):
     delay = 1
     log_command_output = True
     command_canceled = False
-    command_result = {}
+    status = CommandStatus.failed
+    command_result = {"stdout": "", "stderr": "", "exitcode": -signal.SIGTERM}
 
     message = (
       "Executing command with id = {commandId}, taskId = {taskId} for role = 
{role} of "
@@ -315,7 +382,8 @@ class ActionQueue(threading.Thread):
 
     self.cancelEvent.clear()
     # for case of command reschedule (e.g. command and cancel for the same 
taskId are send at the same time)
-    self.taskIdsToCancel.discard(taskId)
+    if command_type != AgentCommand.auto_execution:
+      self.taskIdsToCancel.discard(taskId)
 
     while retry_duration >= 0:
       if taskId in self.taskIdsToCancel:
diff --git 
a/ambari-agent/src/main/python/ambari_agent/ComponentStatusExecutor.py 
b/ambari-agent/src/main/python/ambari_agent/ComponentStatusExecutor.py
index d0b3955d11..8c868b83d4 100644
--- a/ambari-agent/src/main/python/ambari_agent/ComponentStatusExecutor.py
+++ b/ambari-agent/src/main/python/ambari_agent/ComponentStatusExecutor.py
@@ -49,6 +49,8 @@ class ComponentStatusExecutor(threading.Thread):
     self.reports_to_discard = []
     self.reports_to_discard_lock = threading.RLock()
     self.reported_component_status_lock = threading.RLock()
+    self.component_status_snapshot_complete = False
+    self.component_status_snapshot_generation = 0
     threading.Thread.__init__(self)
 
   def run(self):
@@ -65,40 +67,52 @@ class ComponentStatusExecutor(threading.Thread):
       try:
         self.clean_not_existing_clusters_info()
         cluster_reports = defaultdict(lambda: [])
+        cluster_ids = self.topology_cache.get_cluster_ids()
+        with self.reported_component_status_lock:
+          snapshot_generation = self.component_status_snapshot_generation
+          snapshot_complete = (
+            not self.component_status_snapshot_complete and bool(cluster_ids)
+          )
 
         with self.reports_to_discard_lock:
           self.reports_to_discard = []
 
-        for cluster_id in self.topology_cache.get_cluster_ids():
+        for cluster_id in cluster_ids:
           # TODO: check if we can make clusters immutable too
           try:
             topology_cache = self.topology_cache[cluster_id]
             metadata_cache = self.metadata_cache[cluster_id]
           except KeyError:
             # multithreading: if cluster was deleted during iteration
+            snapshot_complete = False
             continue
 
           if "status_commands_to_run" not in metadata_cache:
+            snapshot_complete = False
             continue
 
           status_commands_to_run = metadata_cache.status_commands_to_run
 
           if "components" not in topology_cache:
+            snapshot_complete = False
             continue
 
           current_host_id = self.topology_cache.get_current_host_id(cluster_id)
 
           if current_host_id is None:
+            snapshot_complete = False
             continue
 
           cluster_components = topology_cache.components
           for component_dict in cluster_components:
             for command_name in status_commands_to_run:
               if self.stop_event.is_set():
+                snapshot_complete = False
                 break
 
               # cluster was already removed
               if cluster_id not in self.topology_cache.get_cluster_ids():
+                snapshot_complete = False
                 break
 
               # check if component is installed on current host
@@ -115,6 +129,7 @@ class ComponentStatusExecutor(threading.Thread):
                 self.logger.info(
                   f"Skipping status command for {component_name}. Since 
command for it is running"
                 )
+                snapshot_complete = False
                 continue
 
               result = self.check_component_status(
@@ -125,7 +140,11 @@ class ComponentStatusExecutor(threading.Thread):
                 cluster_reports[cluster_id].append(result)
 
         cluster_reports = self.discard_stale_reports(cluster_reports)
-        self.send_updates_to_server(cluster_reports)
+        self.send_updates_to_server(
+          cluster_reports,
+          snapshot_complete=snapshot_complete,
+          snapshot_generation=snapshot_generation,
+        )
       except (
         ConnectionIsAlreadyClosed
       ):  # server and agent disconnected during sending data. Not an issue
@@ -212,12 +231,12 @@ class ComponentStatusExecutor(threading.Thread):
       "clusterId": cluster_id,
     }
 
-    if (
-      status
-      != 
self.reported_component_status[cluster_id][f"{service_name}/{component_name}"][
-        command_name
-      ]
-    ):
+    with self.reported_component_status_lock:
+      previous_status = self.reported_component_status[cluster_id][
+        f"{service_name}/{component_name}"
+      ][command_name]
+
+    if status != previous_status:
       logging.info(f"Status for {component_name} has changed to {status}")
       self.recovery_manager.handle_status_change(component_name, status)
 
@@ -257,22 +276,61 @@ class ComponentStatusExecutor(threading.Thread):
 
             cluster_reports[cluster_id].append(report)
 
-    self.send_updates_to_server(cluster_reports)
+      self.component_status_snapshot_complete = False
+      self.component_status_snapshot_generation += 1
+      self.reported_component_status.clear()
+
+    self.send_updates_to_server(
+      cluster_reports, snapshot_complete=False, save_reported_status=False
+    )
 
-  def send_updates_to_server(self, cluster_reports):
-    if not cluster_reports or not self.initializer_module.is_registered:
+  def send_updates_to_server(
+    self,
+    cluster_reports,
+    snapshot_complete=False,
+    save_reported_status=True,
+    snapshot_generation=None,
+  ):
+    if (
+      (not cluster_reports and not snapshot_complete and save_reported_status)
+      or not self.initializer_module.is_registered
+    ):
       return
 
-    correlation_id = self.initializer_module.connection.send(
-      message={"clusters": cluster_reports},
-      destination=Constants.COMPONENT_STATUS_REPORTS_ENDPOINT,
-    )
-    
self.server_responses_listener.listener_functions_on_success[correlation_id] = (
-      lambda headers, message: 
self.save_reported_component_status(cluster_reports)
-    )
+    with self.reported_component_status_lock:
+      if snapshot_generation is None:
+        snapshot_generation = self.component_status_snapshot_generation
+      elif snapshot_generation != self.component_status_snapshot_generation:
+        return
+
+      correlation_id = self.initializer_module.connection.send(
+        message={
+          "clusters": cluster_reports,
+          "snapshotComplete": snapshot_complete,
+        },
+        destination=Constants.COMPONENT_STATUS_REPORTS_ENDPOINT,
+      )
+    if save_reported_status:
+      
self.server_responses_listener.listener_functions_on_success[correlation_id] = (
+        lambda headers, message: self.save_reported_component_status(
+          cluster_reports, snapshot_complete, snapshot_generation
+        )
+      )
+    else:
+      
self.server_responses_listener.listener_functions_on_success[correlation_id] = (
+        lambda headers, message: None
+      )
 
-  def save_reported_component_status(self, cluster_reports):
+  def save_reported_component_status(
+    self, cluster_reports, snapshot_complete=False, snapshot_generation=None
+  ):
     with self.reported_component_status_lock:
+      if (
+        snapshot_generation is not None
+        and snapshot_generation != self.component_status_snapshot_generation
+      ):
+        return
+
       for cluster_id, reports in cluster_reports.items():
         for report in reports:
           component_name = report["componentName"]
@@ -283,6 +341,8 @@ class ComponentStatusExecutor(threading.Thread):
           self.reported_component_status[cluster_id][
             f"{service_name}/{component_name}"
           ][command] = status
+      if snapshot_complete:
+        self.component_status_snapshot_complete = True
 
   def clean_not_existing_clusters_info(self):
     """
diff --git a/ambari-agent/src/main/python/ambari_agent/HeartbeatThread.py 
b/ambari-agent/src/main/python/ambari_agent/HeartbeatThread.py
index a8bb70740c..5525c577ba 100644
--- a/ambari-agent/src/main/python/ambari_agent/HeartbeatThread.py
+++ b/ambari-agent/src/main/python/ambari_agent/HeartbeatThread.py
@@ -208,12 +208,13 @@ class HeartbeatThread(threading.Thread):
 
     self.run_post_registration_actions()
 
-    self.initializer_module.is_registered = True
+    # Invalidate cached status before the periodic scanner can use the new 
session.
+    self.force_component_status_update()
     # now when registration is done we can expose connection to other threads.
     self.initializer_module._connection = self.connection
+    self.initializer_module.is_registered = True
 
     self.report_components_initial_versions()
-    self.force_component_status_update()
 
   def run_post_registration_actions(self):
     for post_registration_action in self.post_registration_actions:
diff --git a/ambari-agent/src/main/python/ambari_agent/RecoveryManager.py 
b/ambari-agent/src/main/python/ambari_agent/RecoveryManager.py
index 0a2e693da0..43663190c8 100644
--- a/ambari-agent/src/main/python/ambari_agent/RecoveryManager.py
+++ b/ambari-agent/src/main/python/ambari_agent/RecoveryManager.py
@@ -98,6 +98,11 @@ class RecoveryManager:
     self.allowed_desired_states = [self.STARTED, self.INSTALLED]
     self.allowed_current_states = [self.INIT, self.INSTALLED]
     self.enabled_components = []
+    self.component_dependencies = {}
+    self.topology_managed = False
+    self.topology_epoch = None
+    self.topology_version = -1
+    self.topology_complete = True
     self.statuses = {}
     self.__component_to_service_map = {}  # component => service map TODO: fix 
it later(hack here)
     self.__status_lock = threading.RLock()
@@ -374,7 +379,7 @@ class RecoveryManager:
             elif status["current"] == self.STARTED:
               command = self.get_restart_command(component)
 
-        if command:
+        if command and self.recovery_topology_allows(command):
           self.execute(component)
           logger.info(
             "Created recovery command %s for component %s",
@@ -385,6 +390,54 @@ class RecoveryManager:
 
     return commands
 
+  def recovery_topology_allows(self, command):
+    if not self.topology_managed:
+      return True
+
+    if not self.topology_complete:
+      logger.info(
+        "Recovery for %s is blocked until the component status topology is 
complete",
+        command[self.ROLE],
+      )
+      return False
+
+    is_start = command[self.ROLE_COMMAND] == RoleCommand.start
+    is_restart = (
+      command[self.ROLE_COMMAND] == RoleCommand.custom_command
+      and command.get("custom_command") == CustomCommand.restart
+    )
+    if not is_start and not is_restart:
+      return True
+
+    for dependency in self.component_dependencies.get(command[self.ROLE], []):
+      required_state = dependency.get("required_state", self.STARTED)
+      if (
+        not dependency.get("fresh", False)
+        or not self.dependency_state_satisfies(
+          dependency.get("current_state"), required_state
+        )
+        or not self.dependency_state_satisfies(
+          dependency.get("desired_state"), required_state
+        )
+      ):
+        logger.info(
+          "Recovery for %s is blocked by %s on %s: current=%s, desired=%s, 
required=%s, fresh=%s",
+          command[self.ROLE],
+          dependency.get("component_name"),
+          dependency.get("host_name"),
+          dependency.get("current_state"),
+          dependency.get("desired_state"),
+          required_state,
+          dependency.get("fresh", False),
+        )
+        return False
+    return True
+
+  def dependency_state_satisfies(self, state, required_state):
+    if required_state == self.INSTALLED:
+      return state in (self.INSTALLED, self.STARTED)
+    return state == required_state
+
   def may_execute(self, action):
     """
     Check if an action can be executed
@@ -554,9 +607,34 @@ class RecoveryManager:
       if logger.isEnabledFor(logging.INFO):
         logger.info("RecoverConfig = %s", 
pprint.pformat(dictionary["recoveryConfig"]))
       config = dictionary["recoveryConfig"]
+      if "topology_epoch" in config and "topology_version" in config:
+        topology_epoch = config["topology_epoch"]
+        topology_version = int(config["topology_version"])
+        if (
+          topology_epoch == self.topology_epoch
+          and topology_version < self.topology_version
+        ):
+          logger.warning(
+            "Ignoring stale recovery topology version %s; current version is 
%s",
+            topology_version,
+            self.topology_version,
+          )
+          return
+
+        self.topology_managed = True
+        self.topology_epoch = topology_epoch
+        self.topology_version = topology_version
+        self.topology_complete = config.get("topology_complete", False)
+      else:
+        self.topology_managed = False
+        self.topology_epoch = None
+        self.topology_version = -1
+        self.topology_complete = True
+
       if "components" in config:
         enabled_components = config["components"]
         enabled_components_list = []
+        component_dependencies = {}
 
         components = [
           (item["service_name"], item["component_name"], item["desired_state"])
@@ -572,7 +650,13 @@ class RecoveryManager:
           #  push another service <-> component relation
           self.__component_to_service_map[component] = service
 
+        for item in enabled_components:
+          component_dependencies[item["component_name"]] = item.get(
+            "dependencies", []
+          )
+
         self.enabled_components = enabled_components_list
+        self.component_dependencies = component_dependencies
 
   def on_config_update(self):
     recovery_enabled = False
diff --git a/ambari-agent/src/test/python/ambari_agent/TestActionQueue.py 
b/ambari-agent/src/test/python/ambari_agent/TestActionQueue.py
index 032c1aaa8d..32ec1dae7c 100644
--- a/ambari-agent/src/test/python/ambari_agent/TestActionQueue.py
+++ b/ambari-agent/src/test/python/ambari_agent/TestActionQueue.py
@@ -64,6 +64,14 @@ class TestActionQueue(TestCase):
 
   logger = logging.getLogger()
 
+  def create_mock_action_queue(self):
+    initializer_module = MagicMock()
+    initializer_module.config.get.return_value = "/tmp"
+    initializer_module.config.get_parallel_exec_option.return_value = 0
+    initializer_module.stop_event = threading.Event()
+    initializer_module.recovery_manager.enabled.return_value = False
+    return ActionQueue(initializer_module)
+
   datanode_install_command = {
     "commandType": "EXECUTION_COMMAND",
     "role": "DATANODE",
@@ -108,6 +116,83 @@ class TestActionQueue(TestCase):
     "clusterId": CLUSTER_ID,
   }
 
+  def test_server_command_removes_queued_recovery_commands(self):
+    action_queue = self.create_mock_action_queue()
+    recovery_command = copy.deepcopy(self.datanode_auto_start_command)
+    server_command = copy.deepcopy(self.namenode_install_command)
+
+    action_queue.put([recovery_command])
+    action_queue.put([server_command])
+
+    self.assertEqual(1, action_queue.commandQueue.qsize())
+    self.assertEqual(server_command, action_queue.commandQueue.get_nowait())
+
+  def test_server_command_cancels_active_recovery_commands(self):
+    action_queue = self.create_mock_action_queue()
+    recovery_command = copy.deepcopy(self.datanode_auto_start_command)
+    server_command = copy.deepcopy(self.namenode_install_command)
+    execution_started = threading.Event()
+    allow_execution_to_finish = threading.Event()
+
+    def wait_for_preemption(command):
+      execution_started.set()
+      allow_execution_to_finish.wait(5)
+
+    action_queue.execute_command = MagicMock(side_effect=wait_for_preemption)
+    recovery_thread = threading.Thread(
+      target=action_queue.process_command, args=(recovery_command,)
+    )
+    recovery_thread.start()
+    self.assertTrue(execution_started.wait(5))
+
+    action_queue.put([server_command])
+
+    
action_queue.customServiceOrchestrator.cancel_command.assert_called_once_with(
+      recovery_command["taskId"], "Preempted by a server-issued command"
+    )
+    self.assertIn(recovery_command["taskId"], action_queue.taskIdsToCancel)
+    self.assertEqual(server_command, action_queue.commandQueue.get_nowait())
+
+    allow_execution_to_finish.set()
+    recovery_thread.join(5)
+    self.assertFalse(recovery_thread.is_alive())
+    self.assertNotIn(recovery_command["taskId"], action_queue.taskIdsToCancel)
+
+  def test_dequeued_recovery_command_yields_to_queued_server_command(self):
+    action_queue = self.create_mock_action_queue()
+    recovery_command = copy.deepcopy(self.datanode_auto_start_command)
+    server_command = copy.deepcopy(self.namenode_install_command)
+    action_queue.commandQueue.put(server_command)
+    action_queue.execute_command = MagicMock()
+
+    action_queue.process_command(recovery_command)
+
+    action_queue.execute_command.assert_not_called()
+    self.assertNotIn(recovery_command["taskId"], 
action_queue.active_recovery_task_ids)
+
+  def test_recovery_command_canceled_before_script_execution(self):
+    action_queue = self.create_mock_action_queue()
+    recovery_command = copy.deepcopy(self.datanode_auto_start_command)
+    action_queue.taskIdsToCancel.add(recovery_command["taskId"])
+    action_queue.commandStatuses.generate_report_template.return_value = {}
+    action_queue.config.get.side_effect = (
+      lambda section, key: "0" if key == "log_command_executes" else "/tmp"
+    )
+
+    action_queue.execute_command(recovery_command)
+
+    action_queue.customServiceOrchestrator.runCommand.assert_not_called()
+    self.assertNotIn(recovery_command["taskId"], action_queue.taskIdsToCancel)
+
+  def test_recovery_command_does_not_preempt_active_recovery(self):
+    action_queue = self.create_mock_action_queue()
+    active_task_id = 41
+    action_queue.active_recovery_task_ids.add(active_task_id)
+
+    action_queue.put([copy.deepcopy(self.datanode_auto_start_command)])
+
+    action_queue.customServiceOrchestrator.cancel_command.assert_not_called()
+
   datanode_upgrade_command = {
     "commandId": 17,
     "role": "role",
diff --git 
a/ambari-agent/src/test/python/ambari_agent/TestComponentStatusExecutor.py 
b/ambari-agent/src/test/python/ambari_agent/TestComponentStatusExecutor.py
new file mode 100644
index 0000000000..092769d602
--- /dev/null
+++ b/ambari-agent/src/test/python/ambari_agent/TestComponentStatusExecutor.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+"""
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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 threading
+from unittest import TestCase
+
+from ambari_agent.ComponentStatusExecutor import ComponentStatusExecutor
+from ambari_agent import Constants
+from mock.mock import MagicMock
+
+
+class TestComponentStatusExecutor(TestCase):
+  def create_executor(self):
+    initializer_module = MagicMock()
+    initializer_module.config.status_commands_run_interval = 10
+    initializer_module.stop_event = threading.Event()
+    initializer_module.is_registered = True
+    initializer_module.server_responses_listener.listener_functions_on_success 
= {}
+    initializer_module.connection.send.return_value = "correlation-1"
+    return ComponentStatusExecutor(initializer_module), initializer_module
+
+  def test_complete_snapshot_is_marked_only_after_server_ack(self):
+    executor, initializer_module = self.create_executor()
+    reports = {
+      "1": [
+        {
+          "serviceName": "HDFS",
+          "componentName": "NAMENODE",
+          "command": "STATUS",
+          "status": "STARTED",
+          "clusterId": "1",
+        }
+      ]
+    }
+
+    executor.send_updates_to_server(reports, snapshot_complete=True)
+
+    initializer_module.connection.send.assert_called_once_with(
+      message={"clusters": reports, "snapshotComplete": True},
+      destination=Constants.COMPONENT_STATUS_REPORTS_ENDPOINT,
+    )
+    self.assertFalse(executor.component_status_snapshot_complete)
+
+    callback = 
initializer_module.server_responses_listener.listener_functions_on_success[
+      "correlation-1"
+    ]
+    callback({}, {})
+    self.assertTrue(executor.component_status_snapshot_complete)
+
+  def test_ack_from_snapshot_before_forced_refresh_is_ignored(self):
+    executor, initializer_module = self.create_executor()
+    initializer_module.connection.send.side_effect = ["scan-1", 
"forced-refresh"]
+    reports = {
+      "1": [
+        {
+          "serviceName": "HDFS",
+          "componentName": "NAMENODE",
+          "command": "STATUS",
+          "status": "STARTED",
+          "clusterId": "1",
+        }
+      ]
+    }
+
+    executor.send_updates_to_server(reports, snapshot_complete=True)
+    stale_callback = (
+      
initializer_module.server_responses_listener.listener_functions_on_success[
+        "scan-1"
+      ]
+    )
+    executor.force_send_component_statuses()
+    stale_callback({}, {})
+
+    self.assertFalse(executor.component_status_snapshot_complete)
+    self.assertEqual({}, dict(executor.reported_component_status))
diff --git a/ambari-agent/src/test/python/ambari_agent/TestRecoveryManager.py 
b/ambari-agent/src/test/python/ambari_agent/TestRecoveryManager.py
index 1a098a028c..ff9f4759de 100644
--- a/ambari-agent/src/test/python/ambari_agent/TestRecoveryManager.py
+++ b/ambari-agent/src/test/python/ambari_agent/TestRecoveryManager.py
@@ -563,6 +563,147 @@ class _TestRecoveryManager(TestCase):
     self.assertFalse(rm.configured_for_recovery("E"))
     self.assertTrue(rm.configured_for_recovery("F"))
 
+  def test_topology_requires_fresh_started_dependencies(self):
+    rm = RecoveryManager(MagicMock())
+    rm.update_config(12, 5, 1, 15, True, True, False)
+    component = {
+      "component_name": "RESOURCEMANAGER",
+      "service_name": "YARN",
+      "desired_state": "STARTED",
+      "dependencies": [
+        {
+          "component_name": "NAMENODE",
+          "service_name": "HDFS",
+          "host_name": "host1",
+          "current_state": "INSTALLED",
+          "desired_state": "STARTED",
+          "required_state": "STARTED",
+          "fresh": True,
+        }
+      ],
+    }
+
+    rm.update_recovery_config(
+      {
+        "recoveryConfig": {
+          "topology_epoch": "server-1",
+          "topology_version": 1,
+          "topology_complete": False,
+          "components": [component],
+        }
+      }
+    )
+    rm.update_current_status("RESOURCEMANAGER", "INSTALLED")
+    self.assertEqual([], rm.get_recovery_commands())
+
+    rm.update_recovery_config(
+      {
+        "recoveryConfig": {
+          "topology_epoch": "server-1",
+          "topology_version": 2,
+          "topology_complete": True,
+          "components": [component],
+        }
+      }
+    )
+    self.assertEqual([], rm.get_recovery_commands())
+
+    component["dependencies"][0]["current_state"] = "STARTED"
+    rm.update_recovery_config(
+      {
+        "recoveryConfig": {
+          "topology_epoch": "server-1",
+          "topology_version": 3,
+          "topology_complete": True,
+          "components": [component],
+        }
+      }
+    )
+    commands = rm.get_recovery_commands()
+    self.assertEqual(1, len(commands))
+    self.assertEqual("RESOURCEMANAGER", commands[0]["role"])
+    self.assertEqual("START", commands[0]["roleCommand"])
+
+  def test_recovery_config_rejects_older_topology_version(self):
+    rm = RecoveryManager(MagicMock())
+    rm.update_recovery_config(
+      {
+        "recoveryConfig": {
+          "topology_epoch": "server-1",
+          "topology_version": 5,
+          "topology_complete": True,
+          "components": [
+            {
+              "component_name": "NODEMANAGER",
+              "service_name": "YARN",
+              "desired_state": "STARTED",
+            }
+          ],
+        }
+      }
+    )
+    rm.update_recovery_config(
+      {
+        "recoveryConfig": {
+          "topology_epoch": "server-1",
+          "topology_version": 4,
+          "topology_complete": False,
+          "components": [
+            {
+              "component_name": "NODEMANAGER",
+              "service_name": "YARN",
+              "desired_state": "INSTALLED",
+            }
+          ],
+        }
+      }
+    )
+
+    self.assertEqual(5, rm.topology_version)
+    self.assertTrue(rm.topology_complete)
+    self.assertEqual("STARTED", rm.get_desired_status("NODEMANAGER"))
+
+    rm.update_recovery_config(
+      {
+        "recoveryConfig": {
+          "topology_epoch": "server-2",
+          "topology_version": 1,
+          "topology_complete": False,
+          "components": [],
+        }
+      }
+    )
+    self.assertEqual("server-2", rm.topology_epoch)
+    self.assertEqual(1, rm.topology_version)
+    self.assertFalse(rm.topology_complete)
+
+  def test_started_dependency_satisfies_installed_requirement(self):
+    rm = RecoveryManager(MagicMock())
+    rm.topology_managed = True
+    rm.topology_complete = True
+    rm.component_dependencies = {
+      "RESOURCEMANAGER": [
+        {
+          "component_name": "NAMENODE",
+          "service_name": "HDFS",
+          "host_name": "host1",
+          "current_state": "STARTED",
+          "desired_state": "STARTED",
+          "required_state": "INSTALLED",
+          "fresh": True,
+        }
+      ]
+    }
+
+    self.assertTrue(
+      rm.recovery_topology_allows(
+        {
+          "role": "RESOURCEMANAGER",
+          "roleCommand": "START",
+        }
+      )
+    )
+
   @patch.object(RecoveryManager, "_now_")
   def test_reset_if_window_passed_since_last_attempt(self, time_mock):
     time_mock.side_effect = [1000, 1071, 1372]
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/ComponentStatusAgentReport.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/ComponentStatusAgentReport.java
index e6a3813190..0bc0aa4da0 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/ComponentStatusAgentReport.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/ComponentStatusAgentReport.java
@@ -20,17 +20,41 @@ package org.apache.ambari.server.agent;
 import java.util.List;
 
 import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.agent.stomp.HostLevelParamsHolder;
 
 public class ComponentStatusAgentReport extends 
AgentReport<List<ComponentStatus>> {
   private final HeartBeatHandler hh;
+  private final HostLevelParamsHolder hostLevelParamsHolder;
+  private final RecoveryTopologyManager recoveryTopologyManager;
+  private final Long hostId;
+  private final String sessionId;
+  private final boolean snapshotComplete;
 
-  public ComponentStatusAgentReport(HeartBeatHandler hh, String hostName, 
List<ComponentStatus> componentStatuses) {
+  public ComponentStatusAgentReport(HeartBeatHandler hh, String hostName, 
List<ComponentStatus> componentStatuses,
+      HostLevelParamsHolder hostLevelParamsHolder, RecoveryTopologyManager 
recoveryTopologyManager,
+      Long hostId, String sessionId, boolean snapshotComplete) {
     super(hostName, componentStatuses);
     this.hh = hh;
+    this.hostLevelParamsHolder = hostLevelParamsHolder;
+    this.recoveryTopologyManager = recoveryTopologyManager;
+    this.hostId = hostId;
+    this.sessionId = sessionId;
+    this.snapshotComplete = snapshotComplete;
   }
 
   @Override
   protected void process(List<ComponentStatus> report, String hostName) throws 
AmbariException {
+    if (!recoveryTopologyManager.isActiveSession(hostId, sessionId)) {
+      return;
+    }
+
     hh.handleComponentReportStatus(report, hostName);
+    if (!report.isEmpty() || snapshotComplete) {
+      recoveryTopologyManager.componentStateUpdated();
+      if (snapshotComplete) {
+        recoveryTopologyManager.markSnapshotComplete(hostId, sessionId);
+      }
+      hostLevelParamsHolder.updateRecoveryTopology(hostName);
+    }
   }
 }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/HeartbeatMonitor.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/HeartbeatMonitor.java
index 007e49ceef..d7cf416b95 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/HeartbeatMonitor.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/HeartbeatMonitor.java
@@ -37,6 +37,7 @@ import java.util.TreeMap;
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.RoleCommand;
 import org.apache.ambari.server.actionmanager.ActionManager;
+import org.apache.ambari.server.agent.stomp.HostLevelParamsHolder;
 import org.apache.ambari.server.api.services.AmbariMetaInfo;
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.controller.AmbariManagementController;
@@ -81,6 +82,8 @@ public class HeartbeatMonitor implements Runnable {
   private final Configuration configuration;
   private final AgentRequests agentRequests;
   private final AmbariEventPublisher ambariEventPublisher;
+  private final HostLevelParamsHolder hostLevelParamsHolder;
+  private final RecoveryTopologyManager recoveryTopologyManager;
 
   public HeartbeatMonitor(Clusters clusters, ActionManager am,
                           int threadWakeupInterval, Injector injector) {
@@ -94,6 +97,8 @@ public class HeartbeatMonitor implements Runnable {
     configuration = injector.getInstance(Configuration.class);
     agentRequests = new AgentRequests();
     ambariEventPublisher = injector.getInstance(AmbariEventPublisher.class);
+    hostLevelParamsHolder = injector.getInstance(HostLevelParamsHolder.class);
+    recoveryTopologyManager = 
injector.getInstance(RecoveryTopologyManager.class);
     ambariEventPublisher.register(this);
   }
 
@@ -346,6 +351,9 @@ public class HeartbeatMonitor implements Runnable {
       }
     }
 
+    recoveryTopologyManager.endAgentSession(hostId);
+    hostLevelParamsHolder.updateRecoveryTopology(host);
+
     //Purge action queue
     //notify action manager
     actionManager.handleLostHost(host);
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfig.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfig.java
index e71d9ccc88..a71a17b096 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfig.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfig.java
@@ -33,14 +33,46 @@ public class RecoveryConfig {
   @JsonProperty("components")
   private List<RecoveryConfigComponent> enabledComponents;
 
+  @SerializedName("topology_epoch")
+  @JsonProperty("topology_epoch")
+  private String topologyEpoch;
+
+  @SerializedName("topology_version")
+  @JsonProperty("topology_version")
+  private long topologyVersion;
+
+  @SerializedName("topology_complete")
+  @JsonProperty("topology_complete")
+  private boolean topologyComplete;
+
   public RecoveryConfig(List<RecoveryConfigComponent> enabledComponents) {
+    this(enabledComponents, null, 0, true);
+  }
+
+  public RecoveryConfig(List<RecoveryConfigComponent> enabledComponents, 
String topologyEpoch,
+      long topologyVersion, boolean topologyComplete) {
     this.enabledComponents = enabledComponents;
+    this.topologyEpoch = topologyEpoch;
+    this.topologyVersion = topologyVersion;
+    this.topologyComplete = topologyComplete;
   }
 
   public List<RecoveryConfigComponent> getEnabledComponents() {
     return enabledComponents == null ? null : 
Collections.unmodifiableList(enabledComponents);
   }
 
+  public String getTopologyEpoch() {
+    return topologyEpoch;
+  }
+
+  public long getTopologyVersion() {
+    return topologyVersion;
+  }
+
+  public boolean isTopologyComplete() {
+    return topologyComplete;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
@@ -48,12 +80,21 @@ public class RecoveryConfig {
 
     RecoveryConfig that = (RecoveryConfig) o;
 
-    return enabledComponents != null ? 
enabledComponents.equals(that.enabledComponents) : that.enabledComponents == 
null;
+    if (topologyVersion != that.topologyVersion || topologyComplete != 
that.topologyComplete) {
+      return false;
+    }
+    if (enabledComponents != null ? 
!enabledComponents.equals(that.enabledComponents) : that.enabledComponents != 
null) {
+      return false;
+    }
+    return topologyEpoch != null ? topologyEpoch.equals(that.topologyEpoch) : 
that.topologyEpoch == null;
   }
 
   @Override
   public int hashCode() {
     int result = (enabledComponents != null ? enabledComponents.hashCode() : 
0);
+    result = 31 * result + (topologyEpoch != null ? topologyEpoch.hashCode() : 
0);
+    result = 31 * result + Long.hashCode(topologyVersion);
+    result = 31 * result + (topologyComplete ? 1 : 0);
     return result;
   }
 
@@ -61,6 +102,9 @@ public class RecoveryConfig {
   public String toString() {
     StringBuilder buffer = new StringBuilder("RecoveryConfig{");
     buffer.append(", components=").append(enabledComponents);
+    buffer.append(", topologyEpoch=").append(topologyEpoch);
+    buffer.append(", topologyVersion=").append(topologyVersion);
+    buffer.append(", topologyComplete=").append(topologyComplete);
     buffer.append('}');
     return buffer.toString();
   }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigComponent.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigComponent.java
index 50f13b4ea2..29b2157c14 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigComponent.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigComponent.java
@@ -18,6 +18,8 @@
 
 package org.apache.ambari.server.agent;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 import org.apache.ambari.server.state.ServiceComponentHost;
@@ -43,15 +45,23 @@ public class RecoveryConfigComponent{
   @JsonProperty("desired_state")
   private String desiredState;
 
+  private List<RecoveryConfigDependency> dependencies;
+
   /**
    * Creates new instance of {@link RecoveryConfigComponent}
    * @param componentName name of the component
    * @param desiredState desired desiredState of the component
    */
   public RecoveryConfigComponent(String componentName, String serviceName, 
State desiredState){
+    this(componentName, serviceName, desiredState, Collections.emptyList());
+  }
+
+  public RecoveryConfigComponent(String componentName, String serviceName, 
State desiredState,
+      List<RecoveryConfigDependency> dependencies) {
     this.setComponentName(componentName);
     this.setServiceName(serviceName);
     this.setDesiredState(desiredState);
+    this.dependencies = dependencies;
   }
 
   /**
@@ -96,6 +106,14 @@ public class RecoveryConfigComponent{
     this.serviceName = serviceName;
   }
 
+  public List<RecoveryConfigDependency> getDependencies() {
+    return dependencies == null ? Collections.emptyList() : 
Collections.unmodifiableList(dependencies);
+  }
+
+  public void setDependencies(List<RecoveryConfigDependency> dependencies) {
+    this.dependencies = dependencies;
+  }
+
   @Override
   public boolean equals(Object o){
     if (this == o) {
@@ -108,11 +126,12 @@ public class RecoveryConfigComponent{
     final RecoveryConfigComponent that = (RecoveryConfigComponent) o;
     return Objects.equals(componentName, that.componentName) &&
       Objects.equals(serviceName, that.serviceName) &&
-      Objects.equals(desiredState, that.desiredState);
+      Objects.equals(desiredState, that.desiredState) &&
+      Objects.equals(dependencies, that.dependencies);
   }
 
   @Override
   public int hashCode(){
-    return Objects.hash(componentName, serviceName, desiredState);
+    return Objects.hash(componentName, serviceName, desiredState, 
dependencies);
   }
 }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigDependency.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigDependency.java
new file mode 100644
index 0000000000..abb5aa9de3
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigDependency.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.ambari.server.agent;
+
+import java.util.Objects;
+
+import org.apache.ambari.server.state.ServiceComponentHost;
+import org.apache.ambari.server.state.State;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+/** Runtime state for one component instance that blocks recovery. */
+public class RecoveryConfigDependency {
+  @SerializedName("component_name")
+  @JsonProperty("component_name")
+  private final String componentName;
+
+  @SerializedName("service_name")
+  @JsonProperty("service_name")
+  private final String serviceName;
+
+  @SerializedName("host_name")
+  @JsonProperty("host_name")
+  private final String hostName;
+
+  @SerializedName("current_state")
+  @JsonProperty("current_state")
+  private final State currentState;
+
+  @SerializedName("desired_state")
+  @JsonProperty("desired_state")
+  private final State desiredState;
+
+  @SerializedName("required_state")
+  @JsonProperty("required_state")
+  private final State requiredState;
+
+  private final boolean fresh;
+
+  public RecoveryConfigDependency(ServiceComponentHost sch, State 
requiredState, boolean fresh) {
+    componentName = sch.getServiceComponentName();
+    serviceName = sch.getServiceName();
+    hostName = sch.getHostName();
+    currentState = sch.getState();
+    desiredState = sch.getDesiredState();
+    this.requiredState = requiredState;
+    this.fresh = fresh;
+  }
+
+  public String getComponentName() {
+    return componentName;
+  }
+
+  public String getServiceName() {
+    return serviceName;
+  }
+
+  public String getHostName() {
+    return hostName;
+  }
+
+  public State getCurrentState() {
+    return currentState;
+  }
+
+  public State getDesiredState() {
+    return desiredState;
+  }
+
+  public State getRequiredState() {
+    return requiredState;
+  }
+
+  public boolean isFresh() {
+    return fresh;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    RecoveryConfigDependency that = (RecoveryConfigDependency) o;
+    return fresh == that.fresh && Objects.equals(componentName, 
that.componentName)
+        && Objects.equals(serviceName, that.serviceName) && 
Objects.equals(hostName, that.hostName)
+        && currentState == that.currentState && desiredState == 
that.desiredState
+        && requiredState == that.requiredState;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(componentName, serviceName, hostName, currentState, 
desiredState, requiredState, fresh);
+  }
+}
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigHelper.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigHelper.java
index 75a88afb95..c60cdd5f44 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigHelper.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryConfigHelper.java
@@ -19,18 +19,24 @@
 package org.apache.ambari.server.agent;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.Role;
+import org.apache.ambari.server.RoleCommand;
 import org.apache.ambari.server.events.ClusterConfigChangedEvent;
 import org.apache.ambari.server.events.MaintenanceModeEvent;
 import org.apache.ambari.server.events.ServiceComponentInstalledEvent;
 import org.apache.ambari.server.events.ServiceComponentRecoveryChangedEvent;
 import org.apache.ambari.server.events.ServiceComponentUninstalledEvent;
 import org.apache.ambari.server.events.publishers.AmbariEventPublisher;
+import org.apache.ambari.server.metadata.RoleCommandOrder;
+import org.apache.ambari.server.metadata.RoleCommandOrderProvider;
+import org.apache.ambari.server.metadata.RoleCommandPair;
 import org.apache.ambari.server.state.Cluster;
 import org.apache.ambari.server.state.Clusters;
 import org.apache.ambari.server.state.Config;
@@ -39,6 +45,8 @@ import org.apache.ambari.server.state.Host;
 import org.apache.ambari.server.state.MaintenanceState;
 import org.apache.ambari.server.state.Service;
 import org.apache.ambari.server.state.ServiceComponentHost;
+import org.apache.ambari.server.state.StackId;
+import org.apache.ambari.server.state.State;
 import org.apache.commons.lang.StringUtils;
 
 import com.google.common.eventbus.AllowConcurrentEvents;
@@ -66,6 +74,12 @@ public class RecoveryConfigHelper {
   @Inject
   private Clusters clusters;
 
+  @Inject
+  private RoleCommandOrderProvider roleCommandOrderProvider;
+
+  @Inject
+  private RecoveryTopologyManager recoveryTopologyManager;
+
   /**
    * Cluster --> Host --> Timestamp
    */
@@ -101,10 +115,75 @@ public class RecoveryConfigHelper {
 
     AutoStartConfig autoStartConfig = new AutoStartConfig(clusterName);
 
-    RecoveryConfig recoveryConfig = new 
RecoveryConfig(autoStartConfig.getEnabledComponents(hostname));
+    List<RecoveryConfigComponent> enabledComponents = 
autoStartConfig.getEnabledComponents(hostname);
+    boolean topologyComplete = addTopologyState(autoStartConfig.cluster, 
hostname, enabledComponents);
+    RecoveryConfig recoveryConfig = new RecoveryConfig(enabledComponents, 
recoveryTopologyManager.getEpoch(),
+        recoveryTopologyManager.getVersion(), topologyComplete);
     return recoveryConfig;
   }
 
+  private boolean addTopologyState(Cluster cluster, String hostname,
+      List<RecoveryConfigComponent> enabledComponents) throws AmbariException {
+    if (cluster == null || StringUtils.isEmpty(hostname)) {
+      return false;
+    }
+
+    Host recoveryHost = clusters.getHost(hostname);
+    if (recoveryHost == null) {
+      return false;
+    }
+
+    boolean topologyComplete = 
recoveryTopologyManager.isFresh(recoveryHost.getHostId());
+    if (enabledComponents.isEmpty()) {
+      return topologyComplete;
+    }
+
+    for (Service service : cluster.getServices().values()) {
+      StackId stackId = service.getDesiredStackId();
+      if (stackId == null) {
+        return false;
+      }
+    }
+
+    RoleCommandOrder roleCommandOrder = 
roleCommandOrderProvider.getRoleCommandOrder(cluster);
+    if (roleCommandOrder == null) {
+      return false;
+    }
+
+    for (RecoveryConfigComponent component : enabledComponents) {
+      List<RecoveryConfigDependency> dependencies = new ArrayList<>();
+      try {
+        for (RoleCommandPair dependency : roleCommandOrder.getDependencies(
+            Role.valueOf(component.getComponentName()), RoleCommand.START)) {
+          State requiredState = getRequiredState(dependency.getCmd());
+          for (Service service : cluster.getServices().values()) {
+            if 
(!service.getServiceComponents().containsKey(dependency.getRole().toString())) {
+              continue;
+            }
+            for (ServiceComponentHost dependencyHost : 
service.getServiceComponent(
+                
dependency.getRole().toString()).getServiceComponentHosts().values()) {
+              boolean fresh = 
recoveryTopologyManager.isFresh(dependencyHost.getHost().getHostId());
+              dependencies.add(new RecoveryConfigDependency(dependencyHost, 
requiredState, fresh));
+              topologyComplete &= fresh;
+            }
+          }
+        }
+      } catch (IllegalArgumentException e) {
+        topologyComplete = false;
+      }
+
+      
dependencies.sort(Comparator.comparing(RecoveryConfigDependency::getServiceName)
+          .thenComparing(RecoveryConfigDependency::getComponentName)
+          .thenComparing(RecoveryConfigDependency::getHostName));
+      component.setDependencies(dependencies);
+    }
+    return topologyComplete;
+  }
+
+  private State getRequiredState(RoleCommand command) {
+    return command == RoleCommand.INSTALL ? State.INSTALLED : State.STARTED;
+  }
+
   /**
    * Computes if the recovery configuration was updated since the last time it 
was sent to the agent.
    *
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryTopologyManager.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryTopologyManager.java
new file mode 100644
index 0000000000..8a4aff05c8
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/RecoveryTopologyManager.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.ambari.server.agent;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import com.google.inject.Singleton;
+
+/**
+ * Tracks the server lifecycle and the agent sessions that supplied fresh 
component state.
+ */
+@Singleton
+public class RecoveryTopologyManager {
+  private final String epoch = UUID.randomUUID().toString();
+  private final AtomicLong version = new AtomicLong(1);
+  private final Map<Long, String> activeSessions = new ConcurrentHashMap<>();
+  private final Map<Long, String> completeSnapshotSessions = new 
ConcurrentHashMap<>();
+
+  public void beginAgentSession(Long hostId, String sessionId) {
+    activeSessions.put(hostId, sessionId);
+    completeSnapshotSessions.remove(hostId);
+    version.incrementAndGet();
+  }
+
+  public boolean markSnapshotComplete(Long hostId, String sessionId) {
+    if (!sessionId.equals(activeSessions.get(hostId))) {
+      return false;
+    }
+
+    String previousSession = completeSnapshotSessions.put(hostId, sessionId);
+    if (!sessionId.equals(previousSession)) {
+      version.incrementAndGet();
+      return true;
+    }
+    return false;
+  }
+
+  public boolean isActiveSession(Long hostId, String sessionId) {
+    return sessionId.equals(activeSessions.get(hostId));
+  }
+
+  public void endAgentSession(Long hostId) {
+    activeSessions.remove(hostId);
+    completeSnapshotSessions.remove(hostId);
+    version.incrementAndGet();
+  }
+
+  public void componentStateUpdated() {
+    version.incrementAndGet();
+  }
+
+  public boolean isFresh(Long hostId) {
+    String activeSession = activeSessions.get(hostId);
+    return activeSession != null && 
activeSession.equals(completeSnapshotSessions.get(hostId));
+  }
+
+  public String getEpoch() {
+    return epoch;
+  }
+
+  public long getVersion() {
+    return version.get();
+  }
+}
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentReportsController.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentReportsController.java
index 2830b15590..521eccb376 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentReportsController.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentReportsController.java
@@ -36,6 +36,7 @@ import 
org.apache.ambari.server.agent.ComponentStatusAgentReport;
 import org.apache.ambari.server.agent.ComponentVersionAgentReport;
 import org.apache.ambari.server.agent.HeartBeatHandler;
 import org.apache.ambari.server.agent.HostStatusAgentReport;
+import org.apache.ambari.server.agent.RecoveryTopologyManager;
 import org.apache.ambari.server.agent.stomp.dto.AckReport;
 import org.apache.ambari.server.agent.stomp.dto.CommandStatusReports;
 import org.apache.ambari.server.agent.stomp.dto.ComponentStatusReport;
@@ -44,6 +45,7 @@ import 
org.apache.ambari.server.agent.stomp.dto.ComponentVersionReports;
 import org.apache.ambari.server.agent.stomp.dto.HostStatusReport;
 import org.apache.ambari.server.events.DefaultMessageEmitter;
 import org.apache.ambari.server.state.Alert;
+import org.apache.ambari.server.state.Host;
 import org.apache.ambari.server.state.fsm.InvalidStateTransitionException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -67,11 +69,15 @@ public class AgentReportsController {
   private final HeartBeatHandler hh;
   private final AgentSessionManager agentSessionManager;
   private final AgentReportsProcessor agentReportsProcessor;
+  private final HostLevelParamsHolder hostLevelParamsHolder;
+  private final RecoveryTopologyManager recoveryTopologyManager;
 
   public AgentReportsController(Injector injector) {
     hh = injector.getInstance(HeartBeatHandler.class);
     agentSessionManager = injector.getInstance(AgentSessionManager.class);
     agentReportsProcessor = injector.getInstance(AgentReportsProcessor.class);
+    hostLevelParamsHolder = injector.getInstance(HostLevelParamsHolder.class);
+    recoveryTopologyManager = 
injector.getInstance(RecoveryTopologyManager.class);
   }
 
   @MessageMapping("/component_version")
@@ -98,8 +104,10 @@ public class AgentReportsController {
       }
     }
 
-    agentReportsProcessor.addAgentReport(new ComponentStatusAgentReport(hh,
-        agentSessionManager.getHost(simpSessionId).getHostName(), statuses));
+    Host host = agentSessionManager.getHost(simpSessionId);
+    agentReportsProcessor.addAgentReport(new ComponentStatusAgentReport(hh, 
host.getHostName(), statuses,
+        hostLevelParamsHolder, recoveryTopologyManager, host.getHostId(), 
simpSessionId,
+        message.isSnapshotComplete()));
     return new ReportsResponse();
   }
 
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HeartbeatController.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HeartbeatController.java
index 0447160e82..78db2c1e7e 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HeartbeatController.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HeartbeatController.java
@@ -34,11 +34,13 @@ import org.apache.ambari.server.agent.AgentSessionManager;
 import org.apache.ambari.server.agent.HeartBeat;
 import org.apache.ambari.server.agent.HeartBeatHandler;
 import org.apache.ambari.server.agent.HeartBeatResponse;
+import org.apache.ambari.server.agent.RecoveryTopologyManager;
 import org.apache.ambari.server.agent.Register;
 import org.apache.ambari.server.agent.RegistrationResponse;
 import org.apache.ambari.server.agent.RegistrationStatus;
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.configuration.spring.GuiceBeansConfig;
+import org.apache.ambari.server.state.Host;
 import org.apache.ambari.server.state.cluster.ClustersImpl;
 import org.apache.ambari.server.state.fsm.InvalidStateTransitionException;
 import org.slf4j.Logger;
@@ -63,6 +65,8 @@ public class HeartbeatController {
   private final HeartBeatHandler hh;
   private final ClustersImpl clusters;
   private final AgentSessionManager agentSessionManager;
+  private final HostLevelParamsHolder hostLevelParamsHolder;
+  private final RecoveryTopologyManager recoveryTopologyManager;
   private final LinkedBlockingQueue queue;
   private final ThreadFactory threadFactoryExecutor = new 
ThreadFactoryBuilder().setNameFormat("agent-register-processor-%d").build();
   private final ThreadFactory threadFactoryTimeout = new 
ThreadFactoryBuilder().setNameFormat("agent-register-timeout-%d").build();
@@ -78,6 +82,8 @@ public class HeartbeatController {
     clusters = injector.getInstance(ClustersImpl.class);
     unitOfWork = injector.getInstance(UnitOfWork.class);
     agentSessionManager = injector.getInstance(AgentSessionManager.class);
+    hostLevelParamsHolder = injector.getInstance(HostLevelParamsHolder.class);
+    recoveryTopologyManager = 
injector.getInstance(RecoveryTopologyManager.class);
 
     Configuration configuration = injector.getInstance(Configuration.class);
     queue = new 
LinkedBlockingQueue(configuration.getAgentsRegistrationQueueSize());
@@ -98,8 +104,10 @@ public class HeartbeatController {
         try {
           /* Call into the heartbeat handler */
           response = hh.handleRegistration(message);
-          agentSessionManager.register(simpSessionId,
-              clusters.getHost(message.getHostname()));
+          Host host = clusters.getHost(message.getHostname());
+          recoveryTopologyManager.beginAgentSession(host.getHostId(), 
simpSessionId);
+          hostLevelParamsHolder.updateRecoveryTopology(message.getHostname());
+          agentSessionManager.register(simpSessionId, host);
           LOG.debug("Sending registration response " + response);
         } catch (Exception ex) {
           LOG.info(ex.getMessage(), ex);
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
index 7674233830..3525ea6e1a 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
@@ -19,6 +19,7 @@ package org.apache.ambari.server.agent.stomp;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.TreeMap;
 
 import org.apache.ambari.server.AmbariException;
@@ -94,6 +95,14 @@ public class HostLevelParamsHolder extends 
AgentHostDataHolder<HostLevelParamsUp
     }
   }
 
+  public void updateRecoveryTopology(String reportingHostName) throws 
AmbariException {
+    for (Cluster cluster : clusters.getClustersForHost(reportingHostName)) {
+      for (Host host : cluster.getHosts()) {
+        updateDataOfHost(cluster.getClusterId(), cluster, host);
+      }
+    }
+  }
+
   @Override
   protected HostLevelParamsUpdateEvent handleUpdate(HostLevelParamsUpdateEvent 
current, HostLevelParamsUpdateEvent update) {
     HostLevelParamsUpdateEvent result = null;
@@ -117,7 +126,9 @@ public class HostLevelParamsHolder extends 
AgentHostDataHolder<HostLevelParamsUp
           RecoveryConfig mergedRecoveryConfig;
           Map<String, BlueprintProvisioningState> 
mergedBlueprintProvisioningStates;
 
-          if 
(!currentCluster.getRecoveryConfig().equals(updatedCluster.getRecoveryConfig()))
 {
+          if (isOlderRecoveryTopology(currentCluster.getRecoveryConfig(), 
updatedCluster.getRecoveryConfig())) {
+            mergedRecoveryConfig = currentCluster.getRecoveryConfig();
+          } else if 
(!currentCluster.getRecoveryConfig().equals(updatedCluster.getRecoveryConfig()))
 {
             mergedRecoveryConfig = updatedCluster.getRecoveryConfig();
             clusterChanged = true;
           } else {
@@ -152,6 +163,11 @@ public class HostLevelParamsHolder extends 
AgentHostDataHolder<HostLevelParamsUp
     return result;
   }
 
+  private boolean isOlderRecoveryTopology(RecoveryConfig current, 
RecoveryConfig update) {
+    return Objects.equals(current.getTopologyEpoch(), 
update.getTopologyEpoch())
+        && update.getTopologyVersion() < current.getTopologyVersion();
+  }
+
   @Override
   protected HostLevelParamsUpdateEvent getEmptyData() {
     return HostLevelParamsUpdateEvent.emptyUpdate();
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ComponentStatusReports.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ComponentStatusReports.java
index ac327d4ec3..783891a3c2 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ComponentStatusReports.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ComponentStatusReports.java
@@ -28,6 +28,8 @@ public class ComponentStatusReports {
   @JsonProperty("clusters")
   private TreeMap<String, List<ComponentStatusReport>> componentStatusReports;
 
+  private boolean snapshotComplete;
+
   public ComponentStatusReports() {
   }
 
@@ -42,4 +44,12 @@ public class ComponentStatusReports {
   public void setComponentStatusReports(TreeMap<String, 
List<ComponentStatusReport>> componentStatusReports) {
     this.componentStatusReports = componentStatusReports;
   }
+
+  public boolean isSnapshotComplete() {
+    return snapshotComplete;
+  }
+
+  public void setSnapshotComplete(boolean snapshotComplete) {
+    this.snapshotComplete = snapshotComplete;
+  }
 }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandOrder.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandOrder.java
index b748a2b4dc..da560df920 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandOrder.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandOrder.java
@@ -18,6 +18,7 @@
 package org.apache.ambari.server.metadata;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
@@ -188,6 +189,14 @@ public class RoleCommandOrder implements Cloneable {
     return 0;
   }
 
+  /**
+   * Returns the transitive role dependencies used by the stage planner.
+   */
+  public Set<RoleCommandPair> getDependencies(Role role, RoleCommand command) {
+    Set<RoleCommandPair> roleDependencies = dependencies.get(new 
RoleCommandPair(role, command));
+    return roleDependencies == null ? Collections.emptySet() : 
Collections.unmodifiableSet(roleDependencies);
+  }
+
   /**
    * Returns transitive dependencies as a services list
    * @param service to check if it depends on another services
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandPair.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandPair.java
index dccddc65c6..1431b41174 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandPair.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/metadata/RoleCommandPair.java
@@ -53,11 +53,11 @@ public class RoleCommandPair {
     return false;
   }
 
-  Role getRole() {
+  public Role getRole() {
     return role;
   }
 
-  RoleCommand getCmd() {
+  public RoleCommand getCmd() {
     return cmd;
   }
 
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/agent/ComponentStatusAgentReportTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/agent/ComponentStatusAgentReportTest.java
new file mode 100644
index 0000000000..67237bcede
--- /dev/null
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/agent/ComponentStatusAgentReportTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.ambari.server.agent;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import java.util.Collections;
+
+import org.apache.ambari.server.agent.stomp.HostLevelParamsHolder;
+import org.junit.Test;
+
+public class ComponentStatusAgentReportTest {
+  @Test
+  public void testReportFromSupersededSessionIsIgnored() throws Exception {
+    HeartBeatHandler heartbeatHandler = mock(HeartBeatHandler.class);
+    HostLevelParamsHolder hostLevelParamsHolder = 
mock(HostLevelParamsHolder.class);
+    RecoveryTopologyManager recoveryTopologyManager = new 
RecoveryTopologyManager();
+    Long hostId = 1L;
+    recoveryTopologyManager.beginAgentSession(hostId, "current-session");
+
+    ComponentStatusAgentReport report = new 
ComponentStatusAgentReport(heartbeatHandler, "host1",
+        Collections.emptyList(), hostLevelParamsHolder, 
recoveryTopologyManager, hostId,
+        "superseded-session", true);
+
+    report.process();
+
+    verifyNoInteractions(heartbeatHandler, hostLevelParamsHolder);
+  }
+}
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/agent/RecoveryTopologyManagerTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/agent/RecoveryTopologyManagerTest.java
new file mode 100644
index 0000000000..a2aeb96fea
--- /dev/null
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/agent/RecoveryTopologyManagerTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.ambari.server.agent;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class RecoveryTopologyManagerTest {
+  @Test
+  public void testSnapshotIsFreshOnlyForTheActiveAgentSession() {
+    RecoveryTopologyManager manager = new RecoveryTopologyManager();
+    Long hostId = 1L;
+
+    manager.beginAgentSession(hostId, "session-1");
+    assertFalse(manager.isFresh(hostId));
+    assertTrue(manager.markSnapshotComplete(hostId, "session-1"));
+    assertTrue(manager.isFresh(hostId));
+
+    manager.beginAgentSession(hostId, "session-2");
+    assertFalse(manager.isFresh(hostId));
+    assertFalse(manager.markSnapshotComplete(hostId, "session-1"));
+    assertFalse(manager.isFresh(hostId));
+    assertTrue(manager.markSnapshotComplete(hostId, "session-2"));
+    assertTrue(manager.isFresh(hostId));
+  }
+
+  @Test
+  public void testEndingAgentSessionInvalidatesSnapshotAndVersion() {
+    RecoveryTopologyManager manager = new RecoveryTopologyManager();
+    Long hostId = 1L;
+
+    manager.beginAgentSession(hostId, "session-1");
+    manager.markSnapshotComplete(hostId, "session-1");
+    long completeVersion = manager.getVersion();
+
+    manager.endAgentSession(hostId);
+
+    assertFalse(manager.isActiveSession(hostId, "session-1"));
+    assertFalse(manager.isFresh(hostId));
+    assertTrue(manager.getVersion() > completeVersion);
+  }
+}
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolderTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolderTest.java
index 63c733f892..6d6fd87e46 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolderTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolderTest.java
@@ -108,4 +108,21 @@ public class HostLevelParamsHolderTest {
     assertTrue(result.getHostLevelParamsClusters().containsKey("1"));
     assertTrue(result.getHostLevelParamsClusters().containsKey("2"));
   }
+
+  @Test
+  public void testHandleUpdateDoesNotRegressRecoveryTopology() {
+    RecoveryConfig currentRecoveryConfig = new RecoveryConfig(null, 
"server-1", 2, true);
+    HostLevelParamsCluster currentCluster = new 
HostLevelParamsCluster(currentRecoveryConfig, Collections.emptyMap());
+    HostLevelParamsUpdateEvent current = new 
HostLevelParamsUpdateEvent(HOST_ID,
+        Collections.singletonMap("1", currentCluster));
+
+    RecoveryConfig olderRecoveryConfig = new RecoveryConfig(null, "server-1", 
1, false);
+    HostLevelParamsCluster olderCluster = new 
HostLevelParamsCluster(olderRecoveryConfig, Collections.emptyMap());
+    HostLevelParamsUpdateEvent update = new HostLevelParamsUpdateEvent(HOST_ID,
+        Collections.singletonMap("1", olderCluster));
+
+    HostLevelParamsHolder levelParamsHolder = new 
HostLevelParamsHolder(createNiceMock(AmbariEventPublisher.class));
+
+    assertEquals(null, levelParamsHolder.handleUpdate(current, update));
+  }
 }
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/configuration/RecoveryConfigHelperTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/configuration/RecoveryConfigHelperTest.java
index db1155b391..a9b5d09186 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/configuration/RecoveryConfigHelperTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/configuration/RecoveryConfigHelperTest.java
@@ -35,7 +35,9 @@ import org.apache.ambari.server.H2DatabaseCleaner;
 import org.apache.ambari.server.agent.HeartbeatTestHelper;
 import org.apache.ambari.server.agent.RecoveryConfig;
 import org.apache.ambari.server.agent.RecoveryConfigComponent;
+import org.apache.ambari.server.agent.RecoveryConfigDependency;
 import org.apache.ambari.server.agent.RecoveryConfigHelper;
+import org.apache.ambari.server.agent.RecoveryTopologyManager;
 import 
org.apache.ambari.server.controller.internal.DeleteHostComponentStatusMetaData;
 import org.apache.ambari.server.orm.GuiceJpaInitializer;
 import org.apache.ambari.server.orm.InMemoryDefaultTestModule;
@@ -72,6 +74,9 @@ public class RecoveryConfigHelperTest {
   @Inject
   private RecoveryConfigHelper recoveryConfigHelper;
 
+  @Inject
+  private RecoveryTopologyManager recoveryTopologyManager;
+
   @Inject
   private RepositoryVersionDAO repositoryVersionDAO;
 
@@ -278,6 +283,52 @@ public class RecoveryConfigHelperTest {
     assertTrue(isConfigStale);
   }
 
+  @Test
+  public void testStartTopologyContainsStatefulDependencies() throws Exception 
{
+    Cluster cluster = heartbeatTestHelper.getDummyCluster();
+    RepositoryVersionEntity repositoryVersion = 
helper.getOrCreateRepositoryVersion(cluster);
+    Service hdfs = cluster.addService(HDFS, repositoryVersion);
+    hdfs.addServiceComponent(NAMENODE).addServiceComponentHost(DummyHostname1);
+    hdfs.addServiceComponent("SECONDARY_NAMENODE").setRecoveryEnabled(true);
+    
hdfs.getServiceComponent("SECONDARY_NAMENODE").addServiceComponentHost(DummyHostname1);
+
+    
hdfs.getServiceComponent(NAMENODE).getServiceComponentHost(DummyHostname1).setState(State.STARTED);
+    
hdfs.getServiceComponent(NAMENODE).getServiceComponentHost(DummyHostname1).setDesiredState(State.STARTED);
+
+    Long hostId = cluster.getHost(DummyHostname1).getHostId();
+    recoveryTopologyManager.beginAgentSession(hostId, "session-1");
+    recoveryTopologyManager.markSnapshotComplete(hostId, "session-1");
+
+    RecoveryConfig recoveryConfig = 
recoveryConfigHelper.getRecoveryConfig(cluster.getClusterName(), 
DummyHostname1);
+    assertTrue(recoveryConfig.isTopologyComplete());
+    assertEquals(1, recoveryConfig.getEnabledComponents().size());
+
+    RecoveryConfigComponent secondaryNameNode = 
recoveryConfig.getEnabledComponents().get(0);
+    assertEquals("SECONDARY_NAMENODE", secondaryNameNode.getComponentName());
+    assertEquals(1, secondaryNameNode.getDependencies().size());
+
+    Map<String, RecoveryConfigDependency> dependencies = new HashMap<>();
+    for (RecoveryConfigDependency dependency : 
secondaryNameNode.getDependencies()) {
+      dependencies.put(dependency.getComponentName(), dependency);
+      assertEquals(State.STARTED, dependency.getRequiredState());
+      assertEquals(State.STARTED, dependency.getDesiredState());
+      assertTrue(dependency.isFresh());
+    }
+    assertEquals(State.STARTED, dependencies.get(NAMENODE).getCurrentState());
+  }
+
+  @Test
+  public void testRecoveryConfigWhileServiceRemovalIsInProgress() throws 
Exception {
+    Cluster cluster = heartbeatTestHelper.getDummyCluster();
+    RepositoryVersionEntity repositoryVersion = 
helper.getOrCreateRepositoryVersion(cluster);
+    Service hdfs = cluster.addService(HDFS, repositoryVersion);
+
+    hdfs.delete(new DeleteHostComponentStatusMetaData());
+
+    RecoveryConfig recoveryConfig = 
recoveryConfigHelper.getRecoveryConfig(cluster.getClusterName(), 
DummyHostname1);
+    assertTrue(recoveryConfig.getEnabledComponents().isEmpty());
+  }
+
   private Cluster getDummyCluster(Set<String> hostNames)
       throws Exception {
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to