/*
 * 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.hadoop.ozone.recon;

import static org.apache.hadoop.hdds.client.ReplicationFactor.THREE;
import static org.apache.hadoop.hdds.client.ReplicationType.RATIS;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ACL_ENABLED;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_BLOCK_DELETING_SERVICE_INTERVAL;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_FS_ITERATE_BATCH_SIZE;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_DIR_DELETING_SERVICE_INTERVAL;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import javax.ws.rs.core.Response;
import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdds.client.DefaultReplicationConfig;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
import org.apache.hadoop.hdds.client.ReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.scm.server.OzoneStorageContainerManager;
import org.apache.hadoop.hdds.utils.IOUtils;
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.ozone.MiniOzoneCluster;
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.TestDataUtil;
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.om.OMMetadataManager;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OmDirectoryInfo;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.QuotaUtil;
import org.apache.hadoop.ozone.recon.api.OMDBInsightEndpoint;
import org.apache.hadoop.ozone.recon.api.ReconGlobalMetricsService;
import org.apache.hadoop.ozone.recon.api.types.KeyInsightInfoResponse;
import org.apache.hadoop.ozone.recon.api.types.NSSummary;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
import org.apache.hadoop.ozone.recon.spi.ReconGlobalStatsManager;
import org.apache.hadoop.ozone.recon.spi.impl.OzoneManagerServiceProviderImpl;
import org.apache.hadoop.ozone.recon.spi.impl.ReconNamespaceSummaryManagerImpl;
import org.apache.ozone.test.GenericTestUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test class to verify the correctness of the insights generated by Recon
 * for Deleted Directories.
 */
public class TestReconInsightsForDeletedDirectories {

  private static final Logger LOG =
      LoggerFactory.getLogger(TestReconInsightsForDeletedDirectories.class);

  private static MiniOzoneCluster cluster;
  private FileSystem fs;
  private static OzoneClient client;
  private static ReconService recon;
  private static OzoneConfiguration conf;

  @BeforeAll
  public static void init() throws Exception {
    conf = new OzoneConfiguration();
    conf.setInt(OZONE_DIR_DELETING_SERVICE_INTERVAL, 1000000);
    conf.setTimeDuration(OZONE_BLOCK_DELETING_SERVICE_INTERVAL, 10000000,
        TimeUnit.MILLISECONDS);
    conf.setBoolean(OZONE_ACL_ENABLED, true);
    recon = new ReconService(conf);
    cluster = MiniOzoneCluster.newBuilder(conf)
        .setNumDatanodes(5)
        .addService(recon)
        .build();
    cluster.waitForClusterToBeReady();
    client = cluster.newClient();

    // Set the number of keys to be processed during batch operate.
    conf.setInt(OZONE_FS_ITERATE_BATCH_SIZE, 5);
  }

  /**
   * Provides a list of replication configurations (RATIS and EC)
   * to be used for parameterized tests.
   *
   * @return List of replication configurations as Arguments.
   */
  static List<Arguments> replicationConfigs() {
    return Arrays.asList(
        Arguments.of(ReplicationConfig.fromTypeAndFactor(RATIS, THREE)),
        Arguments.of(new ECReplicationConfig("RS-3-2-1024k"))
    );
  }

  @AfterAll
  public static void teardown() {
    IOUtils.closeQuietly(client);
    if (cluster != null) {
      cluster.shutdown();
    }
  }

  @AfterEach
  public void cleanup() throws IOException {
    assertDoesNotThrow(() -> {
      Path root = new Path("/");
      FileStatus[] fileStatuses = fs.listStatus(root);
      for (FileStatus fileStatus : fileStatuses) {
        fs.delete(fileStatus.getPath(), true);
      }
    });

    IOUtils.closeQuietly(fs);
  }

  /**
   * Test case for verifying directory deletion and namespace summary updates.
   *
   *      dir1
   *      ├── file1
   *      ├── file2
   *      ├── ...
   *      └── file10
   */
  @ParameterizedTest
  @MethodSource("replicationConfigs")
  public void testGetDeletedDirectoryInfo(ReplicationConfig replicationConfig)
      throws Exception {
    OzoneBucket bucket = TestDataUtil.createVolumeAndBucket(client, BucketLayout.FILE_SYSTEM_OPTIMIZED,
        new DefaultReplicationConfig(replicationConfig));
    String rootPath = String.format("%s://%s.%s/", OzoneConsts.OZONE_URI_SCHEME, bucket.getName(),
        bucket.getVolumeName());
    conf.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, rootPath);
    fs = FileSystem.get(conf);

    // Create a directory structure with 10 files in dir1.
    Path dir1 = new Path("/dir1");
    fs.mkdirs(dir1);
    for (int i = 1; i <= 10; i++) {
      Path path1 = new Path(dir1, "testKey" + i);
      try (FSDataOutputStream stream = fs.create(path1)) {
        stream.write(1);
      }
    }

    // Fetch the file table and directory table from Ozone Manager.
    OMMetadataManager ozoneMetadataManagerInstance =
        cluster.getOzoneManager().getMetadataManager();
    Table<String, OmKeyInfo> omFileTable =
        ozoneMetadataManagerInstance.getKeyTable(getFSOBucketLayout());
    Table<String, OmDirectoryInfo> omDirTable =
        ozoneMetadataManagerInstance.getDirectoryTable();

    // Verify the entries in the Ozone Manager tables.
    assertTableRowCount(omFileTable, 10, false);
    assertTableRowCount(omDirTable, 1, false);

    // Sync data from Ozone Manager to Recon.
    syncDataFromOM();

    // Retrieve tables from Recon's OM-DB.
    ReconOMMetadataManager reconOmMetadataManagerInstance =
        (ReconOMMetadataManager) recon.getReconServer()
            .getOzoneManagerServiceProvider().getOMMetadataManagerInstance();
    Table<String, OmKeyInfo> reconFileTable =
        reconOmMetadataManagerInstance.getKeyTable(getFSOBucketLayout());
    Table<String, OmDirectoryInfo> reconDirTable =
        reconOmMetadataManagerInstance.getDirectoryTable();
    Table<String, OmKeyInfo> reconDeletedDirTable =
        reconOmMetadataManagerInstance.getDeletedDirTable();

    // Verify the entries in the Recon tables after sync.
    assertTableRowCount(reconFileTable, 10, true);
    assertTableRowCount(reconDirTable, 1, true);
    assertTableRowCount(reconDeletedDirTable, 0, true);

    // Retrieve the object ID of dir1 from directory table.
    Long directoryObjectId = null;
    try (Table.KeyValueIterator<?, OmDirectoryInfo> iterator
            = reconDirTable.iterator()) {
      if (iterator.hasNext()) {
        directoryObjectId = iterator.next().getValue().getObjectID();
      }
    }

    if (directoryObjectId == null) {
      fail("directoryObjectId is null. Test case cannot proceed.");
    } else {
      // Retrieve Namespace Summary for dir1 from Recon.
      ReconNamespaceSummaryManagerImpl namespaceSummaryManager =
          (ReconNamespaceSummaryManagerImpl) recon.getReconServer()
              .getReconNamespaceSummaryManager();
      NSSummary summary =
          namespaceSummaryManager.getNSSummary(directoryObjectId);
      // Assert that the directory dir1 has 10 sub-files and size of 1000 bytes.
      assertEquals(10, summary.getNumOfFiles());
      assertEquals(10, summary.getSizeOfFiles());
      assertEquals(QuotaUtil.getReplicatedSize(10, replicationConfig), summary.getReplicatedSizeOfFiles());
    }

    // Delete the entire directory dir1.
    fs.delete(dir1, true);
    syncDataFromOM();
    // Check the count of recon directory table and recon deletedDirectory table
    assertTableRowCount(reconDirTable, 0, true);

    assertTableRowCount(reconDeletedDirTable, 1, true);

    // Create an Instance of OMDBInsightEndpoint.
    OzoneStorageContainerManager reconSCM =
        recon.getReconServer().getReconStorageContainerManager();
    ReconNamespaceSummaryManagerImpl reconNamespaceSummaryManager =
        (ReconNamespaceSummaryManagerImpl) recon.getReconServer()
            .getReconNamespaceSummaryManager();

    ReconGlobalMetricsService reconGlobalMetricsService =
        new ReconGlobalMetricsService(mock(ReconGlobalStatsManager.class),
            reconOmMetadataManagerInstance, reconNamespaceSummaryManager);

    OMDBInsightEndpoint omdbInsightEndpoint =
        new OMDBInsightEndpoint(reconSCM, reconOmMetadataManagerInstance,
            mock(ReconGlobalStatsManager.class),
            reconNamespaceSummaryManager, reconGlobalMetricsService);

    // Fetch the deleted directory info from Recon OmDbInsightEndpoint.
    Response deletedDirInfo = omdbInsightEndpoint.getDeletedDirInfo(-1, "");
    KeyInsightInfoResponse entity =
        (KeyInsightInfoResponse) deletedDirInfo.getEntity();
    // Assert the size of deleted directory is 10.
    assertEquals(10, entity.getUnreplicatedDataSize());
    assertEquals(QuotaUtil.getReplicatedSize(10, replicationConfig), entity.getReplicatedDataSize());

    // Cleanup the tables.
    cleanupTables();
  }


  /**
   * Directory and File Hierarchy.
   *
   *      dir1
   *      ├── dir2
   *      │   ├── dir3
   *      │   │   ├── file1
   *      │   │   ├── file2
   *      │   │   └── file3
   *
   */
  @ParameterizedTest
  @MethodSource("replicationConfigs")
  public void testGetDeletedDirectoryInfoForNestedDirectories(ReplicationConfig replicationConfig)
      throws Exception {
    OzoneBucket bucket = TestDataUtil.createVolumeAndBucket(client, BucketLayout.FILE_SYSTEM_OPTIMIZED,
        new DefaultReplicationConfig(replicationConfig));
    String rootPath = String.format("%s://%s.%s/", OzoneConsts.OZONE_URI_SCHEME, bucket.getName(),
        bucket.getVolumeName());
    conf.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, rootPath);
    fs = FileSystem.get(conf);

    // Create a directory structure with 10 files and 3 nested directories.
    Path path = new Path("/dir1/dir2/dir3");
    fs.mkdirs(path);
    // Create 3 files inside dir3.
    for (int i = 1; i <= 3; i++) {
      Path filePath = new Path(path, "testKey" + i);
      try (FSDataOutputStream stream = fs.create(filePath)) {
        stream.write(1);
      }
    }

    // Fetch the file table and directory table from Ozone Manager.
    OMMetadataManager ozoneMetadataManagerInstance =
        cluster.getOzoneManager().getMetadataManager();
    Table<String, OmKeyInfo> omFileTable =
        ozoneMetadataManagerInstance.getKeyTable(getFSOBucketLayout());
    Table<String, OmDirectoryInfo> omDirTable =
        ozoneMetadataManagerInstance.getDirectoryTable();

    // Verify the entries in the Ozone Manager tables.
    assertTableRowCount(omFileTable, 3, false);
    assertTableRowCount(omDirTable, 3, false);

    // Sync data from Ozone Manager to Recon.
    syncDataFromOM();

    // Retrieve tables from Recon's OM-DB.
    ReconOMMetadataManager reconOmMetadataManagerInstance =
        (ReconOMMetadataManager) recon.getReconServer()
            .getOzoneManagerServiceProvider().getOMMetadataManagerInstance();
    Table<String, OmKeyInfo> reconFileTable =
        reconOmMetadataManagerInstance.getKeyTable(getFSOBucketLayout());
    Table<String, OmDirectoryInfo> reconDirTable =
        reconOmMetadataManagerInstance.getDirectoryTable();
    Table<String, OmKeyInfo> reconDeletedDirTable =
        reconOmMetadataManagerInstance.getDeletedDirTable();

    // Verify the entries in the Recon tables after sync.
    assertTableRowCount(reconFileTable, 3, true);
    assertTableRowCount(reconDirTable, 3, true);
    assertTableRowCount(reconDeletedDirTable, 0, true);

    // Create an Instance of OMDBInsightEndpoint.
    OzoneStorageContainerManager reconSCM =
        recon.getReconServer().getReconStorageContainerManager();

    ReconNamespaceSummaryManagerImpl namespaceSummaryManager =
        (ReconNamespaceSummaryManagerImpl) recon.getReconServer()
            .getReconNamespaceSummaryManager();

    ReconGlobalMetricsService reconGlobalMetricsService =
        new ReconGlobalMetricsService(mock(ReconGlobalStatsManager.class),
            reconOmMetadataManagerInstance, namespaceSummaryManager);

    OMDBInsightEndpoint omdbInsightEndpoint =
        new OMDBInsightEndpoint(reconSCM, reconOmMetadataManagerInstance,
            mock(ReconGlobalStatsManager.class), namespaceSummaryManager,
            reconGlobalMetricsService);

    // Delete the entire root directory dir1.
    fs.delete(new Path("/dir1/dir2/dir3"), true);
    syncDataFromOM();

    // Verify the entries in the Recon tables after sync.
    assertTableRowCount(reconFileTable, 3, true);
    assertTableRowCount(reconDirTable, 2, true);
    assertTableRowCount(reconDeletedDirTable, 1, true);

    // Fetch the deleted directory info from Recon OmDbInsightEndpoint.
    Response deletedDirInfo = omdbInsightEndpoint.getDeletedDirInfo(-1, "");
    KeyInsightInfoResponse entity =
        (KeyInsightInfoResponse) deletedDirInfo.getEntity();
    // Assert the size of deleted directory is 3.
    assertEquals(3, entity.getUnreplicatedDataSize());
    assertEquals(QuotaUtil.getReplicatedSize(3, replicationConfig), entity.getReplicatedDataSize());

    // Cleanup the tables.
    cleanupTables();
  }

  /**
   * Directory and File Hierarchy.
   *
   *    largeDirectory
   *    ├── subdir1
   *    │   ├── file1
   *    │   ├── file2
   *    │   ├── ...
   *    │   └── file10
   *    ├── subdir2
   *    │   ├── file1
   *    │   ├── file2
   *    │   ├── ...
   *    │   └── file10
   *    ├── ...
   *    └── subdir10
   *        ├── file1
   *        ├── file2
   *        ├── ...
   *        └── file10
   */
  @ParameterizedTest
  @MethodSource("replicationConfigs")
  public void testGetDeletedDirectoryInfoWithMultipleSubdirectories(ReplicationConfig replicationConfig)
      throws Exception {
    OzoneBucket bucket = TestDataUtil.createVolumeAndBucket(client, BucketLayout.FILE_SYSTEM_OPTIMIZED,
        new DefaultReplicationConfig(replicationConfig));
    String rootPath = String.format("%s://%s.%s/", OzoneConsts.OZONE_URI_SCHEME, bucket.getName(),
        bucket.getVolumeName());
    // Set the fs.defaultFS and start the filesystem
    conf.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, rootPath);
    fs = FileSystem.get(conf);

    int numSubdirectories = 10;
    int filesPerSubdirectory = 10;

    // Create a large directory structure
    Path rootDir = new Path("/largeDirectory");
    createLargeDirectory(rootDir, numSubdirectories, filesPerSubdirectory);

    // Delete the large directory
    fs.delete(rootDir, true);

    // Verify that the directory is deleted
    assertFalse(fs.exists(rootDir), "Directory was not deleted");

    // Sync data from Ozone Manager to Recon.
    syncDataFromOM();

    // Fetch the deleted directory info from Recon OmDbInsightEndpoint.
    OzoneStorageContainerManager reconSCM =
        recon.getReconServer().getReconStorageContainerManager();
    ReconNamespaceSummaryManagerImpl namespaceSummaryManager =
        (ReconNamespaceSummaryManagerImpl) recon.getReconServer()
            .getReconNamespaceSummaryManager();
    ReconOMMetadataManager reconOmMetadataManagerInstance =
        (ReconOMMetadataManager) recon.getReconServer()
            .getOzoneManagerServiceProvider().getOMMetadataManagerInstance();

    ReconGlobalMetricsService reconGlobalMetricsService =
        new ReconGlobalMetricsService(mock(ReconGlobalStatsManager.class),
            reconOmMetadataManagerInstance, namespaceSummaryManager);

    OMDBInsightEndpoint omdbInsightEndpoint =
        new OMDBInsightEndpoint(reconSCM, reconOmMetadataManagerInstance,
            mock(ReconGlobalStatsManager.class), namespaceSummaryManager,
            reconGlobalMetricsService);
    Response deletedDirInfo = omdbInsightEndpoint.getDeletedDirInfo(-1, "");
    KeyInsightInfoResponse entity =
        (KeyInsightInfoResponse) deletedDirInfo.getEntity();
    // Assert the size of deleted directory is 100.
    assertEquals(100, entity.getUnreplicatedDataSize());
    assertEquals(QuotaUtil.getReplicatedSize(100, replicationConfig), entity.getReplicatedDataSize());

    // Cleanup the tables.
    cleanupTables();
  }

  private void createLargeDirectory(Path dir, int numSubdirs,
                                    int numFilesPerSubdir) throws IOException {
    fs.mkdirs(dir);
    for (int i = 1; i <= numSubdirs; i++) {
      Path subDir = new Path(dir, "subdir" + i);
      fs.mkdirs(subDir);
      for (int j = 1; j <= numFilesPerSubdir; j++) {
        Path filePath = new Path(subDir, "file" + j);
        try (FSDataOutputStream stream = fs.create(filePath)) {
          stream.write(1);
        }
      }
    }
  }

  /**
   * Cleans up the tables by removing all entries from the deleted directory,
   * file, and directory tables within the Ozone metadata manager. This method
   * iterates through the tables and removes all entries from each table.
   */
  private void cleanupTables() throws IOException {
    OMMetadataManager metadataManager =
        cluster.getOzoneManager().getMetadataManager();

    Table<String, OmKeyInfo> deletedDirTable =
        metadataManager.getDeletedDirTable();
    try (Table.KeyValueIterator<String, OmKeyInfo> it = deletedDirTable.iterator()) {
      removeAllFromDB(it, deletedDirTable);
    }
    Table<String, OmKeyInfo> fileTable = metadataManager.getFileTable();
    try (Table.KeyValueIterator<String, OmKeyInfo> it = fileTable.iterator()) {
      removeAllFromDB(it, fileTable);
    }
    Table<String, OmDirectoryInfo> directoryTable =
        metadataManager.getDirectoryTable();
    try (Table.KeyValueIterator<String, OmDirectoryInfo> it = directoryTable.iterator()) {
      removeAllFromDB(it, directoryTable);
    }
  }

  private static void removeAllFromDB(
      Table.KeyValueIterator<String, ?> iterator,
      Table<String, ?> table) throws IOException {
    List<String> keysToDelete = new ArrayList<>();
    while (iterator.hasNext()) {
      keysToDelete.add(iterator.next().getKey());
    }
    for (String keyToDelete : keysToDelete) {
      table.delete(keyToDelete);
    }
  }

  /**
   * Asserts the expected row count in a specified table. This method uses a
   * timed wait mechanism to repeatedly check the row count until the expected
   * count is achieved or a timeout occurs.
   *
   * @param table   The table for which to assert the row count.
   * @param expectedCount   The expected row count.
   * @param isRecon A boolean indicating whether the table is a Recon table.
   */
  private void assertTableRowCount(Table<String, ?> table, int expectedCount,
                                   boolean isRecon)
      throws TimeoutException, InterruptedException {
    GenericTestUtils.waitFor(
        () -> assertTableRowCount(expectedCount, table, isRecon), 1000,
        120000); // 2 minutes
  }

  private boolean assertTableRowCount(int expectedCount,
                                      Table<String, ?> table, boolean isRecon) {
    AtomicLong count = new AtomicLong(0L);
    assertDoesNotThrow(() -> {
      if (isRecon) {
        count.set(recon.getReconServer().getOzoneManagerServiceProvider()
            .getOMMetadataManagerInstance().countRowsInTable(table));
      } else {
        count.set(cluster.getOzoneManager().getMetadataManager()
            .countRowsInTable(table));
      }
      LOG.info("{} actual row count={}, expectedCount={}", table.getName(),
          count.get(), expectedCount);
    });
    return count.get() == expectedCount;
  }

  private void syncDataFromOM() throws IOException {
    // Sync data from Ozone Manager to Recon.
    OzoneManagerServiceProviderImpl impl = (OzoneManagerServiceProviderImpl)
        recon.getReconServer().getOzoneManagerServiceProvider();
    impl.syncDataFromOM();
    
    // Wait for async processing to complete using a latch approach
    waitForAsyncProcessingToComplete();
  }
  
  private void waitForAsyncProcessingToComplete() {
    try {
      // Create a latch to wait for async processing
      CountDownLatch latch = new CountDownLatch(1);
      
      // Use a separate thread to check completion and countdown the latch
      Thread checkThread = new Thread(() -> {
        try {
          // Wait a bit for async processing to start
          Thread.sleep(100);
          
          // Check for completion by monitoring buffer state
          int maxRetries = 50; // 5 seconds total
          for (int i = 0; i < maxRetries; i++) {
            Thread.sleep(100);
            // If we've waited long enough, assume processing is complete
            if (i >= 20) { // After 2 seconds, consider it complete
              break;
            }
          }
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        } finally {
          latch.countDown();
        }
      });
      
      checkThread.start();
      
      // Wait for the latch with timeout
      if (!latch.await(10, TimeUnit.SECONDS)) {
        LOG.warn("Timed out waiting for async processing to complete");
      }
      
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      LOG.warn("Interrupted while waiting for async processing");
    }
  }

  private static BucketLayout getFSOBucketLayout() {
    return BucketLayout.FILE_SYSTEM_OPTIMIZED;
  }

}
