void processSingleResource(FileStatus resource) {
    Path path = resource.getPath();
    // indicates the processing status of the resource
    ResourceStatus resourceStatus = ResourceStatus.INIT;

    // first, if the path ends with the renamed suffix, it indicates the
    // directory was moved (as stale) but somehow not deleted (probably due to
    // SCM failure); delete the directory
    if (path.toString().endsWith(RENAMED_SUFFIX)) {
      LOG.info("Found a renamed directory that was left undeleted at " +
          path.toString() + ". Deleting.");
      try {
        if (fs.delete(path, true)) {
          resourceStatus = ResourceStatus.DELETED;
        }
      } catch (IOException e) {
        LOG.error("Error while processing a shared cache resource: " + path, e);
      }
    } else {
      // this is the path to the cache resource directory
      // the directory name is the resource key (i.e. a unique identifier)
      String key = path.getName();

      try {
        store.cleanResourceReferences(key);
      } catch (YarnException e) {
        LOG.error("Exception thrown while removing dead appIds.", e);
      }

      if (store.isResourceEvictable(key, resource)) {
        try {
          /*
           * TODO See YARN-2663: There is a race condition between
           * store.removeResource(key) and
           * removeResourceFromCacheFileSystem(path) operations because they do
           * not happen atomically and resources can be uploaded with different
           * file names by the node managers.
           */
          // remove the resource from scm (checks for appIds as well)
          if (store.removeResource(key)) {
            // remove the resource from the file system
            boolean deleted = removeResourceFromCacheFileSystem(path);
            if (deleted) {
              resourceStatus = ResourceStatus.DELETED;
            } else {
              LOG.error("Failed to remove path from the file system."
                  + " Skipping this resource: " + path);
              resourceStatus = ResourceStatus.ERROR;
            }
          } else {
            // we did not delete the resource because it contained application
            // ids
            resourceStatus = ResourceStatus.PROCESSED;
          }
        } catch (IOException e) {
          LOG.error(
              "Failed to remove path from the file system. Skipping this resource: "
                  + path, e);
          resourceStatus = ResourceStatus.ERROR;
        }
      } else {
        resourceStatus = ResourceStatus.PROCESSED;
      }
    }

    // record the processing
    switch (resourceStatus) {
    case DELETED:
      metrics.reportAFileDelete();
      break;
    case PROCESSED:
      metrics.reportAFileProcess();
      break;
    case ERROR:
      metrics.reportAFileError();
      break;
    default:
      LOG.error("Cleaner encountered an invalid status (" + resourceStatus
          + ") while processing resource: " + path.getName());
    }
  }
@Test
  void testProcessFreshResource() throws Exception {
    FileSystem fs = mock(FileSystem.class);
    CleanerMetrics metrics = mock(CleanerMetrics.class);
    SCMStore store = mock(SCMStore.class);

    CleanerTask task =
        createSpiedTask(fs, store, metrics, new ReentrantLock());

    // mock a resource that is not evictable
    when(store.isResourceEvictable(isA(String.class), isA(FileStatus.class)))
        .thenReturn(false);
    FileStatus status = mock(FileStatus.class);
    when(status.getPath()).thenReturn(new Path(ROOT + "/a/b/c/abc"));

    // process the resource
    task.processSingleResource(status);

    // the directory should not be renamed
    verify(fs, never()).rename(eq(status.getPath()), isA(Path.class));
    // metrics should record a processed file (but not delete)
    verify(metrics).reportAFileProcess();
    verify(metrics, never()).reportAFileDelete();
  }