You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by rn...@apache.org on 2017/04/24 14:56:05 UTC

ambari git commit: AMBARI-20819. LogSearch Integration should limit requests to portal for missing components. (rnettleton)

Repository: ambari
Updated Branches:
  refs/heads/trunk e8fd10cf6 -> 8c039bbc2


AMBARI-20819. LogSearch Integration should limit requests to portal for missing components. (rnettleton)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/8c039bbc
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/8c039bbc
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/8c039bbc

Branch: refs/heads/trunk
Commit: 8c039bbc20d7a66fc46f62e5c1d2d690171b667e
Parents: e8fd10c
Author: Bob Nettleton <rn...@hortonworks.com>
Authored: Mon Apr 24 10:55:15 2017 -0400
Committer: Bob Nettleton <rn...@hortonworks.com>
Committed: Mon Apr 24 10:55:15 2017 -0400

----------------------------------------------------------------------
 .../logging/LogSearchDataRetrievalService.java  |  75 ++++--
 .../LogSearchDataRetrievalServiceTest.java      | 249 ++++++++++++++++++-
 2 files changed, 304 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/8c039bbc/ambari-server/src/main/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalService.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalService.java
index 6b484a4..487182e 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalService.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalService.java
@@ -17,10 +17,12 @@
  */
 package org.apache.ambari.server.controller.logging;
 
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.ambari.server.AmbariService;
 import org.apache.ambari.server.configuration.Configuration;
@@ -30,9 +32,9 @@ import org.apache.commons.collections.CollectionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.AbstractService;
 import com.google.inject.Inject;
@@ -69,6 +71,13 @@ public class LogSearchDataRetrievalService extends AbstractService {
   private static Logger LOG = LoggerFactory.getLogger(LogSearchDataRetrievalService.class);
 
   /**
+   * Maximum number of failed attempts that the LogSearch integration code will attempt for
+   *   a given component before treating the component as failed and skipping the request.
+   *
+   */
+  private static int MAX_RETRIES_FOR_FAILED_METADATA_REQUEST = 10;
+
+  /**
    * Factory instance used to handle URL string generation requests on the
    *   main request thread.
    */
@@ -109,6 +118,19 @@ public class LogSearchDataRetrievalService extends AbstractService {
    */
   private final Set<String> currentRequests = Sets.newConcurrentHashSet();
 
+  /**
+   * A map that maintains the set of failure counts for logging
+   * metadata requests on a per-component basis.  This map should
+   * be consulted prior to making a metadata request to the LogSearch
+   * service.  If LogSearch has already returned an empty list for the given
+   * component, or any other error has occurred for a certain number of attempts,
+   * the request should not be attempted further.
+   *
+   */
+  private final Map<String, AtomicInteger> componentRequestFailureCounts =
+    Maps.newConcurrentMap();
+
+
 
   /**
    * Executor instance to be used to run REST queries against
@@ -172,18 +194,20 @@ public class LogSearchDataRetrievalService extends AbstractService {
       LOG.debug("LogFileNames result for key = {} found in cache", key);
       return cacheResult;
     } else {
-      // queue up a thread to create the LogSearch REST request to obtain this information
-      if (currentRequests.contains(key)) {
-        LOG.debug("LogFileNames request has been made for key = {}, but not completed yet", key);
+      if (!componentRequestFailureCounts.containsKey(component) || componentRequestFailureCounts.get(component).get() < MAX_RETRIES_FOR_FAILED_METADATA_REQUEST) {
+        // queue up a thread to create the LogSearch REST request to obtain this information
+        if (currentRequests.contains(key)) {
+          LOG.debug("LogFileNames request has been made for key = {}, but not completed yet", key);
+        } else {
+          LOG.debug("LogFileNames result for key = {} not in cache, queueing up remote request", key);
+          // add request key to queue, to keep multiple copies of the same request from
+          // being submitted
+          currentRequests.add(key);
+          startLogSearchFileNameRequest(host, component, cluster);
+        }
       } else {
-        LOG.debug("LogFileNames result for key = {} not in cache, queueing up remote request", key);
-        // add request key to queue, to keep multiple copies of the same request from
-        // being submitted
-        currentRequests.add(key);
-        startLogSearchFileNameRequest(host, component, cluster);
+        LOG.debug("Too many failures occurred while attempting to obtain log file metadata for component = {}, Ambari will ignore this component for LogSearch Integration", component);
       }
-
-
     }
 
     return null;
@@ -260,6 +284,15 @@ public class LogSearchDataRetrievalService extends AbstractService {
     return currentRequests;
   }
 
+  /**
+   * This protected method allows for simpler unit tests.
+   *
+   * @return the Map of failure counts on a per-component basis
+   */
+  protected Map<String, AtomicInteger> getComponentRequestFailureCounts() {
+    return componentRequestFailureCounts;
+  }
+
   private void startLogSearchFileNameRequest(String host, String component, String cluster) {
     // Create a separate instance of LoggingRequestHelperFactory for
     // each task launched, since these tasks will occur on a separate thread
@@ -268,7 +301,7 @@ public class LogSearchDataRetrievalService extends AbstractService {
     // TODO: the LoggingRequestHelperFactory implementation thread-safe, so that
     // TODO: a single factory instance can be shared across multiple threads safely
     executor.execute(new LogSearchFileNameRequestRunnable(host, component, cluster, logFileNameCache, currentRequests,
-                                                          injector.getInstance(LoggingRequestHelperFactory.class)));
+                                                          injector.getInstance(LoggingRequestHelperFactory.class), componentRequestFailureCounts));
   }
 
   private AmbariManagementController getController() {
@@ -304,20 +337,24 @@ public class LogSearchDataRetrievalService extends AbstractService {
 
     private LoggingRequestHelperFactory loggingRequestHelperFactory;
 
+    private final Map<String, AtomicInteger> componentRequestFailureCounts;
+
     private AmbariManagementController controller;
 
-    LogSearchFileNameRequestRunnable(String host, String component, String cluster, Cache<String, Set<String>> logFileNameCache, Set<String> currentRequests, LoggingRequestHelperFactory loggingRequestHelperFactory) {
-      this(host, component, cluster, logFileNameCache, currentRequests, loggingRequestHelperFactory, AmbariServer.getController());
+    LogSearchFileNameRequestRunnable(String host, String component, String cluster, Cache<String, Set<String>> logFileNameCache, Set<String> currentRequests, LoggingRequestHelperFactory loggingRequestHelperFactory,
+                                     Map<String, AtomicInteger> componentRequestFailureCounts) {
+      this(host, component, cluster, logFileNameCache, currentRequests, loggingRequestHelperFactory, componentRequestFailureCounts, AmbariServer.getController());
     }
 
     LogSearchFileNameRequestRunnable(String host, String component, String cluster, Cache<String, Set<String>> logFileNameCache, Set<String> currentRequests,
-                                               LoggingRequestHelperFactory loggingRequestHelperFactory, AmbariManagementController controller) {
+                                               LoggingRequestHelperFactory loggingRequestHelperFactory, Map<String, AtomicInteger> componentRequestFailureCounts, AmbariManagementController controller) {
       this.host  = host;
       this.component = component;
       this.cluster = cluster;
       this.logFileNameCache = logFileNameCache;
       this.currentRequests = currentRequests;
       this.loggingRequestHelperFactory = loggingRequestHelperFactory;
+      this.componentRequestFailureCounts = componentRequestFailureCounts;
       this.controller = controller;
     }
 
@@ -340,7 +377,13 @@ public class LogSearchDataRetrievalService extends AbstractService {
             // update cache with returned result
             logFileNameCache.put(key, logFileNamesResult);
           } else {
-            LOG.debug("LogSearchFileNameRequestRunnable: remote request was not successful");
+            LOG.debug("LogSearchFileNameRequestRunnable: remote request was not successful for component = {} on host ={}", component, host);
+            if (!componentRequestFailureCounts.containsKey(component)) {
+              componentRequestFailureCounts.put(component, new AtomicInteger());
+            }
+
+            // increment the failure count for this component
+            componentRequestFailureCounts.get(component).incrementAndGet();
           }
         } else {
           LOG.debug("LogSearchFileNameRequestRunnable: request helper was null.  This may mean that LogSearch is not available, or could be a potential connection problem.");

http://git-wip-us.apache.org/repos/asf/ambari/blob/8c039bbc/ambari-server/src/test/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalServiceTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalServiceTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalServiceTest.java
index d60596b..1bf0204 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalServiceTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/logging/LogSearchDataRetrievalServiceTest.java
@@ -17,19 +17,26 @@
  */
 package org.apache.ambari.server.controller.logging;
 
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.isA;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.controller.AmbariManagementController;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
 import org.easymock.EasyMockSupport;
 import org.junit.Test;
 
@@ -166,6 +173,161 @@ public class LogSearchDataRetrievalServiceTest {
 
     assertTrue("Incorrect HostComponent set on request set",
                 retrievalService.getCurrentRequests().contains(expectedComponentName + "+" + expectedHostName));
+    assertEquals("Incorrect size for failure counts for components, should be 0",
+                 0, retrievalService.getComponentRequestFailureCounts().size());
+
+    mockSupport.verifyAll();
+  }
+
+  @Test
+  public void testGetLogFileNamesExistingFailuresLessThanThreshold() throws Exception {
+    final String expectedHostName = "c6401.ambari.apache.org";
+    final String expectedComponentName = "DATANODE";
+    final String expectedClusterName = "clusterone";
+
+    EasyMockSupport mockSupport = new EasyMockSupport();
+
+    LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class);
+
+    Executor executorMock = mockSupport.createMock(Executor.class);
+
+    Injector injectorMock =
+      mockSupport.createMock(Injector.class);
+
+    Configuration configurationMock =
+      mockSupport.createMock(Configuration.class);
+
+    // expect the executor to be called to execute the LogSearch request
+    executorMock.execute(isA(LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable.class));
+    // executor should only be called once
+    expectLastCall().once();
+
+    expect(injectorMock.getInstance(LoggingRequestHelperFactory.class)).andReturn(helperFactoryMock);
+
+    expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce();
+
+    mockSupport.replayAll();
+
+    LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService();
+    retrievalService.setLoggingRequestHelperFactory(helperFactoryMock);
+    retrievalService.setInjector(injectorMock);
+    retrievalService.setConfiguration(configurationMock);
+    // call the initialization routine called by the Google framework
+    retrievalService.doStart();
+    retrievalService.setExecutor(executorMock);
+    // initialize the comopnent-based failure count to have a count > 0, but less than threshold (10)
+    retrievalService.getComponentRequestFailureCounts().put(expectedComponentName, new AtomicInteger(5));
+
+
+    assertEquals("Default request set should be empty", 0, retrievalService.getCurrentRequests().size());
+
+    Set<String> resultSet = retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName);
+
+    assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet);
+    assertEquals("Incorrect number of entries in the current request set", 1, retrievalService.getCurrentRequests().size());
+
+    assertTrue("Incorrect HostComponent set on request set",
+      retrievalService.getCurrentRequests().contains(expectedComponentName + "+" + expectedHostName));
+    assertEquals("Incorrect size for failure counts for components, should be 0",
+      1, retrievalService.getComponentRequestFailureCounts().size());
+    assertEquals("Incorrect failure count for component",
+      5, retrievalService.getComponentRequestFailureCounts().get(expectedComponentName).get());
+
+    mockSupport.verifyAll();
+  }
+
+  @Test
+  public void testGetLogFileNamesExistingFailuresAtThreshold() throws Exception {
+    final String expectedHostName = "c6401.ambari.apache.org";
+    final String expectedComponentName = "DATANODE";
+    final String expectedClusterName = "clusterone";
+
+    EasyMockSupport mockSupport = new EasyMockSupport();
+
+    LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class);
+
+    Executor executorMock = mockSupport.createMock(Executor.class);
+
+    Injector injectorMock =
+      mockSupport.createMock(Injector.class);
+
+    Configuration configurationMock =
+      mockSupport.createMock(Configuration.class);
+
+    expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce();
+
+    mockSupport.replayAll();
+
+    LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService();
+    retrievalService.setLoggingRequestHelperFactory(helperFactoryMock);
+    retrievalService.setInjector(injectorMock);
+    retrievalService.setConfiguration(configurationMock);
+    // call the initialization routine called by the Google framework
+    retrievalService.doStart();
+    retrievalService.setExecutor(executorMock);
+    // initialize the comopnent-based failure count to have a count at the threshold
+    retrievalService.getComponentRequestFailureCounts().put(expectedComponentName, new AtomicInteger(10));
+
+    assertEquals("Default request set should be empty", 0, retrievalService.getCurrentRequests().size());
+
+    Set<String> resultSet =
+      retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName);
+
+    assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet);
+    assertEquals("Incorrect number of entries in the current request set", 0, retrievalService.getCurrentRequests().size());
+
+    assertEquals("Incorrect size for failure counts for components, should be 0",
+      1, retrievalService.getComponentRequestFailureCounts().size());
+    assertEquals("Incorrect failure count for component",
+      10, retrievalService.getComponentRequestFailureCounts().get(expectedComponentName).get());
+
+    mockSupport.verifyAll();
+  }
+
+  @Test
+  public void testGetLogFileNamesExistingFailuresOverThreshold() throws Exception {
+    final String expectedHostName = "c6401.ambari.apache.org";
+    final String expectedComponentName = "DATANODE";
+    final String expectedClusterName = "clusterone";
+
+    EasyMockSupport mockSupport = new EasyMockSupport();
+
+    LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class);
+
+    Executor executorMock = mockSupport.createMock(Executor.class);
+
+    Injector injectorMock =
+      mockSupport.createMock(Injector.class);
+
+    Configuration configurationMock =
+      mockSupport.createMock(Configuration.class);
+
+    expect(configurationMock.getLogSearchMetadataCacheExpireTimeout()).andReturn(1).atLeastOnce();
+
+    mockSupport.replayAll();
+
+    LogSearchDataRetrievalService retrievalService = new LogSearchDataRetrievalService();
+    retrievalService.setLoggingRequestHelperFactory(helperFactoryMock);
+    retrievalService.setInjector(injectorMock);
+    retrievalService.setConfiguration(configurationMock);
+    // call the initialization routine called by the Google framework
+    retrievalService.doStart();
+    retrievalService.setExecutor(executorMock);
+    // initialize the comopnent-based failure count to have a count over the threshold
+    retrievalService.getComponentRequestFailureCounts().put(expectedComponentName, new AtomicInteger(20));
+
+    assertEquals("Default request set should be empty", 0, retrievalService.getCurrentRequests().size());
+
+    Set<String> resultSet =
+      retrievalService.getLogFileNames(expectedComponentName, expectedHostName, expectedClusterName);
+
+    assertNull("Inital query on the retrieval service should be null, since cache is empty by default", resultSet);
+    assertEquals("Incorrect number of entries in the current request set", 0, retrievalService.getCurrentRequests().size());
+
+    assertEquals("Incorrect size for failure counts for components, should be 0",
+      1, retrievalService.getComponentRequestFailureCounts().size());
+    assertEquals("Incorrect failure count for component",
+      20, retrievalService.getComponentRequestFailureCounts().get(expectedComponentName).get());
 
     mockSupport.verifyAll();
   }
@@ -225,6 +387,7 @@ public class LogSearchDataRetrievalServiceTest {
 
     Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class);
     Set<String> currentRequestsMock = mockSupport.createMock(Set.class);
+    Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class);
 
     expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock);
     expect(helperMock.sendGetLogFileNamesRequest(expectedComponentName, expectedHostName)).andReturn(Collections.singleton("/this/is/just/a/test/directory"));
@@ -237,7 +400,7 @@ public class LogSearchDataRetrievalServiceTest {
 
     LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable =
       new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName,
-          cacheMock, currentRequestsMock, helperFactoryMock, controllerMock);
+          cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock);
     loggingRunnable.run();
 
     mockSupport.verifyAll();
@@ -258,6 +421,7 @@ public class LogSearchDataRetrievalServiceTest {
 
     Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class);
     Set<String> currentRequestsMock = mockSupport.createMock(Set.class);
+    Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class);
 
     // return null to simulate an error during helper instance creation
     expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(null);
@@ -269,7 +433,7 @@ public class LogSearchDataRetrievalServiceTest {
 
     LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable =
       new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName,
-          cacheMock, currentRequestsMock, helperFactoryMock, controllerMock);
+          cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock);
     loggingRunnable.run();
 
     mockSupport.verifyAll();
@@ -283,6 +447,7 @@ public class LogSearchDataRetrievalServiceTest {
     final String expectedComponentName = "DATANODE";
     final String expectedClusterName = "clusterone";
     final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName;
+    final AtomicInteger testInteger = new AtomicInteger(0);
 
     EasyMockSupport mockSupport = new EasyMockSupport();
 
@@ -292,6 +457,9 @@ public class LogSearchDataRetrievalServiceTest {
 
     Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class);
     Set<String> currentRequestsMock = mockSupport.createMock(Set.class);
+    Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class);
+
+    Capture<AtomicInteger> captureFailureCount = EasyMock.newCapture();
 
     expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock);
     // return null to simulate an error occurring during the LogSearch data request
@@ -299,14 +467,72 @@ public class LogSearchDataRetrievalServiceTest {
     // expect that the completed request is removed from the current request set,
     // even in the event of a failure to obtain the LogSearch data
     expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once();
+    // expect that the component failure map is initially empty
+    expect(componentFailureCounts.containsKey(expectedComponentName)).andReturn(false);
+    // expect that the component map is updated with a new count
+    expect(componentFailureCounts.put(eq(expectedComponentName), capture(captureFailureCount))).andReturn(new AtomicInteger(0));
+    // expect that the runnable will obtain an increment the failure count
+    expect(componentFailureCounts.get(expectedComponentName)).andReturn(testInteger);
+
 
     mockSupport.replayAll();
 
     LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable =
       new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName,
-          cacheMock, currentRequestsMock, helperFactoryMock, controllerMock);
+          cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock);
     loggingRunnable.run();
 
+    assertEquals("Initial count set by Runnable should be 0",
+                 0, captureFailureCount.getValue().get());
+    assertEquals("Failure count should have been incremented",
+                 1, testInteger.get());
+
+    mockSupport.verifyAll();
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testRunnableWithFailedCallNullResultExistingFailureCount() throws Exception {
+    final String expectedHostName = "c6401.ambari.apache.org";
+    final String expectedComponentName = "DATANODE";
+    final String expectedClusterName = "clusterone";
+    final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName;
+    final AtomicInteger testFailureCount = new AtomicInteger(2);
+
+    EasyMockSupport mockSupport = new EasyMockSupport();
+
+    LoggingRequestHelperFactory helperFactoryMock = mockSupport.createMock(LoggingRequestHelperFactory.class);
+    AmbariManagementController controllerMock = mockSupport.createMock(AmbariManagementController.class);
+    LoggingRequestHelper helperMock = mockSupport.createMock(LoggingRequestHelper.class);
+
+    Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class);
+    Set<String> currentRequestsMock = mockSupport.createMock(Set.class);
+    Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class);
+
+    expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock);
+    // return null to simulate an error occurring during the LogSearch data request
+    expect(helperMock.sendGetLogFileNamesRequest(expectedComponentName, expectedHostName)).andReturn(null);
+    // expect that the completed request is removed from the current request set,
+    // even in the event of a failure to obtain the LogSearch data
+    expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once();
+    // expect that the component failure map is initially empty
+    expect(componentFailureCounts.containsKey(expectedComponentName)).andReturn(true);
+    // expect that the runnable will obtain an increment the existing failure count
+    expect(componentFailureCounts.get(expectedComponentName)).andReturn(testFailureCount);
+
+    mockSupport.replayAll();
+
+    assertEquals("Initial count should be 2",
+                 2, testFailureCount.get());
+
+    LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable =
+      new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName,
+        cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock);
+    loggingRunnable.run();
+
+    assertEquals("Failure count should have been incremented",
+                 3, testFailureCount.get());
+
     mockSupport.verifyAll();
   }
 
@@ -317,6 +543,7 @@ public class LogSearchDataRetrievalServiceTest {
     final String expectedComponentName = "DATANODE";
     final String expectedClusterName = "clusterone";
     final String expectedComponentAndHostName = expectedComponentName + "+" + expectedHostName;
+    final AtomicInteger testInteger = new AtomicInteger(0);
 
     EasyMockSupport mockSupport = new EasyMockSupport();
 
@@ -326,6 +553,9 @@ public class LogSearchDataRetrievalServiceTest {
 
     Cache<String, Set<String>> cacheMock = mockSupport.createMock(Cache.class);
     Set<String> currentRequestsMock = mockSupport.createMock(Set.class);
+    Map<String, AtomicInteger> componentFailureCounts = mockSupport.createMock(Map.class);
+
+    Capture<AtomicInteger> captureFailureCount = EasyMock.newCapture();
 
     expect(helperFactoryMock.getHelper(controllerMock, expectedClusterName)).andReturn(helperMock);
     // return null to simulate an error occurring during the LogSearch data request
@@ -333,14 +563,25 @@ public class LogSearchDataRetrievalServiceTest {
     // expect that the completed request is removed from the current request set,
     // even in the event of a failure to obtain the LogSearch data
     expect(currentRequestsMock.remove(expectedComponentAndHostName)).andReturn(true).once();
+    // expect that the component failure map is initially empty
+    expect(componentFailureCounts.containsKey(expectedComponentName)).andReturn(false);
+    // expect that the component map is updated with a new count
+    expect(componentFailureCounts.put(eq(expectedComponentName), capture(captureFailureCount))).andReturn(new AtomicInteger(0));
+    // expect that the runnable will obtain an increment the failure count
+    expect(componentFailureCounts.get(expectedComponentName)).andReturn(testInteger);
 
     mockSupport.replayAll();
 
     LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable loggingRunnable =
       new LogSearchDataRetrievalService.LogSearchFileNameRequestRunnable(expectedHostName, expectedComponentName, expectedClusterName,
-          cacheMock, currentRequestsMock, helperFactoryMock, controllerMock);
+          cacheMock, currentRequestsMock, helperFactoryMock, componentFailureCounts, controllerMock);
     loggingRunnable.run();
 
+    assertEquals("Initial count set by Runnable should be 0",
+      0, captureFailureCount.getValue().get());
+    assertEquals("Failure count should have been incremented",
+      1, testInteger.get());
+
     mockSupport.verifyAll();
   }
 }