content: /*
   *
   * 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.bookkeeper.bookie;
  
  import com.google.common.annotations.VisibleForTesting;
  import com.google.common.cache.Cache;
  import com.google.common.cache.CacheBuilder;
  import com.google.common.cache.RemovalListener;
  import com.google.common.cache.RemovalNotification;
  import com.google.common.util.concurrent.UncheckedExecutionException;
  import io.netty.buffer.ByteBuf;
  import java.io.File;
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.List;
  import java.util.concurrent.Callable;
  import java.util.concurrent.ExecutionException;
  import java.util.concurrent.TimeUnit;
  import org.apache.bookkeeper.bookie.FileInfoBackingCache.CachedFileInfo;
  import org.apache.bookkeeper.bookie.LedgerDirsManager.NoWritableLedgerDirException;
  import org.apache.bookkeeper.bookie.stats.IndexPersistenceMgrStats;
  import org.apache.bookkeeper.common.util.Watcher;
  import org.apache.bookkeeper.conf.ServerConfiguration;
  import org.apache.bookkeeper.stats.StatsLogger;
  import org.apache.bookkeeper.util.SnapshotMap;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * A {@code IndexPersistenceMgr} is responsible for managing the persistence state for the index in a bookie.
   */
  public class IndexPersistenceMgr {
      private static final Logger LOG = LoggerFactory.getLogger(IndexPersistenceMgr.class);
  
      private static final String IDX = ".idx";
      static final String RLOC = ".rloc";
  
      @VisibleForTesting
      public static final String getLedgerName(long ledgerId) {
          int parent = (int) (ledgerId & 0xff);
          int grandParent = (int) ((ledgerId & 0xff00) >> 8);
          StringBuilder sb = new StringBuilder();
          sb.append(Integer.toHexString(grandParent));
          sb.append('/');
          sb.append(Integer.toHexString(parent));
          sb.append('/');
          sb.append(Long.toHexString(ledgerId));
          sb.append(IDX);
          return sb.toString();
      }
  
      // use two separate cache for write and read
      final Cache<Long, CachedFileInfo> writeFileInfoCache;
      final Cache<Long, CachedFileInfo> readFileInfoCache;
      final FileInfoBackingCache fileInfoBackingCache;
  
      final int openFileLimit;
      final int pageSize;
      final int entriesPerPage;
  
      // Manage all active ledgers in LedgerManager
      // so LedgerManager has knowledge to garbage collect inactive/deleted ledgers
      final SnapshotMap<Long, Boolean> activeLedgers;
      final LedgerDirsManager ledgerDirsManager;
  
      private final IndexPersistenceMgrStats persistenceMgrStats;
  
      public IndexPersistenceMgr(int pageSize,
                                 int entriesPerPage,
                                 ServerConfiguration conf,
                                 SnapshotMap<Long, Boolean> activeLedgers,
                                 LedgerDirsManager ledgerDirsManager,
                                 StatsLogger statsLogger) throws IOException {
          this.openFileLimit = conf.getOpenFileLimit();
          this.activeLedgers = activeLedgers;
          this.ledgerDirsManager = ledgerDirsManager;
          this.pageSize = pageSize;
          this.entriesPerPage = entriesPerPage;
          LOG.info("openFileLimit = {}", openFileLimit);
          // Retrieve all of the active ledgers.
          getActiveLedgers();
  
          // build the file info cache
          int concurrencyLevel = Math.max(1, Math.max(conf.getNumAddWorkerThreads(), conf.getNumReadWorkerThreads()));
          fileInfoBackingCache = new FileInfoBackingCache(this::createFileInfoBackingFile,
                  conf.getFileInfoFormatVersionToWrite());
          RemovalListener<Long, CachedFileInfo> fileInfoEvictionListener = this::handleLedgerEviction;
          writeFileInfoCache = buildCache(
              concurrencyLevel,
              conf.getFileInfoCacheInitialCapacity(),
              openFileLimit,
              conf.getFileInfoMaxIdleTime(),
              fileInfoEvictionListener);
          readFileInfoCache = buildCache(
              concurrencyLevel,
              2 * conf.getFileInfoCacheInitialCapacity(),
              2 * openFileLimit,
              conf.getFileInfoMaxIdleTime(),
              fileInfoEvictionListener);
  
          // Expose Stats
          persistenceMgrStats = new IndexPersistenceMgrStats(
              statsLogger,
              () -> writeFileInfoCache.size(),
              () -> readFileInfoCache.size()
          );
      }
  
      private static Cache<Long, CachedFileInfo> buildCache(int concurrencyLevel,
                                                            int initialCapacity,
                                                            int maximumSize,
                                                            long expireAfterAccessSeconds,
                                                            RemovalListener<Long, CachedFileInfo> removalListener) {
          CacheBuilder<Long, CachedFileInfo> builder = CacheBuilder.newBuilder()
              .concurrencyLevel(concurrencyLevel)
              .initialCapacity(initialCapacity)
              .maximumSize(maximumSize)
              .removalListener(removalListener);
          if (expireAfterAccessSeconds > 0) {
              builder.expireAfterAccess(expireAfterAccessSeconds, TimeUnit.SECONDS);
          }
          return builder.build();
      }
  
      private File createFileInfoBackingFile(long ledger, boolean createIfMissing) throws IOException {
          File lf = findIndexFile(ledger);
          if (null == lf) {
              if (!createIfMissing) {
                  throw new Bookie.NoLedgerException(ledger);
              }
              // We don't have a ledger index file on disk or in cache, so create it.
              lf = getNewLedgerIndexFile(ledger, null);
          }
          return lf;
      }
  
      /**
       * When a ledger is evicted, we need to make sure there's no other thread
       * trying to get FileInfo for that ledger at the same time when we close
       * the FileInfo.
       */
      private void handleLedgerEviction(RemovalNotification<Long, CachedFileInfo> notification) {
          CachedFileInfo fileInfo = notification.getValue();
          if (null == fileInfo || null == notification.getKey()) {
              return;
          }
          if (notification.wasEvicted()) {
              persistenceMgrStats.getEvictedLedgersCounter().inc();
          }
          fileInfo.release();
      }
  
      /**
       * Get the FileInfo and increase reference count.
       * When we get FileInfo from cache, we need to make sure it is synchronized
       * with eviction, otherwise there might be a race condition as we get
       * the FileInfo from cache, that FileInfo is then evicted and closed before we
       * could even increase the reference counter.
       */
      CachedFileInfo getFileInfo(final Long ledger, final byte[] masterKey) throws IOException {
          try {
              CachedFileInfo fi;
              persistenceMgrStats.getPendingGetFileInfoCounter().inc();
              Callable<CachedFileInfo> loader = () -> {
                  CachedFileInfo fileInfo = fileInfoBackingCache.loadFileInfo(ledger, masterKey);
                  activeLedgers.put(ledger, true);
                  return fileInfo;
              };
              do {
                  if (null != masterKey) {
                      fi = writeFileInfoCache.get(ledger, loader);
                  } else {
                      fi = readFileInfoCache.get(ledger, loader);
                  }
                  if (!fi.tryRetain()) {
                      // defensively ensure that dead fileinfo objects don't exist in the
                      // cache. They shouldn't if refcounting is correct, but if someone
                      // does a double release, the fileinfo will be cleaned up, while
                      // remaining in the cache, which could cause a tight loop in this method.
                      boolean inWriteMap = writeFileInfoCache.asMap().remove(ledger, fi);
                      boolean inReadMap = readFileInfoCache.asMap().remove(ledger, fi);
                      if (inWriteMap || inReadMap) {
                          LOG.error("Dead fileinfo({}) forced out of cache (write:{}, read:{}). "
                                    + "It must have been double-released somewhere.",
                                    fi, inWriteMap, inReadMap);
                      }
                      fi = null;
                  }
              } while (fi == null);
  
              return fi;
          } catch (ExecutionException | UncheckedExecutionException ee) {
              if (ee.getCause() instanceof IOException) {
                  throw (IOException) ee.getCause();
              } else {
                  throw new LedgerCache.NoIndexForLedgerException("Failed to load file info for ledger " + ledger, ee);
              }
          } finally {
              persistenceMgrStats.getPendingGetFileInfoCounter().dec();
          }
      }
  
      /**
       * Get a new index file for ledger excluding directory <code>excludedDir</code>.
       *
       * @param ledger
       *          Ledger id.
       * @param excludedDir
       *          The ledger directory to exclude.
       * @return new index file object.
       * @throws NoWritableLedgerDirException if there is no writable dir available.
       */
      private File getNewLedgerIndexFile(Long ledger, File excludedDir)
                      throws NoWritableLedgerDirException {
          File dir = ledgerDirsManager.pickRandomWritableDirForNewIndexFile(excludedDir);
          String ledgerName = getLedgerName(ledger);
          return new File(dir, ledgerName);
      }
  
      /**
       * This method will look within the ledger directories for the ledger index
       * files. That will comprise the set of active ledgers this particular
       * BookieServer knows about that have not yet been deleted by the BookKeeper
       * Client. This is called only once during initialization.
       */
      private void getActiveLedgers() throws IOException {
          // Ledger index files are stored in a file hierarchy with a parent and
          // grandParent directory. We'll have to go two levels deep into these
          // directories to find the index files.
          for (File ledgerDirectory : ledgerDirsManager.getAllLedgerDirs()) {
              File[] grandParents = ledgerDirectory.listFiles();
              if (grandParents == null) {
                  continue;
              }
              for (File grandParent : grandParents) {
                  if (grandParent.isDirectory()) {
                      File[] parents = grandParent.listFiles();
                      if (parents == null) {
                          continue;
                      }
                      for (File parent : parents) {
                          if (parent.isDirectory()) {
                              File[] indexFiles = parent.listFiles();
                              if (indexFiles == null) {
                                  continue;
                              }
                              for (File index : indexFiles) {
                                  if (!index.isFile()
                                          || (!index.getName().endsWith(IDX) && !index.getName().endsWith(RLOC))) {
                                      continue;
                                  }
  
                                  // We've found a ledger index file. The file
                                  // name is the HexString representation of the
                                  // ledgerId.
                                  String ledgerIdInHex = index.getName().replace(RLOC, "").replace(IDX, "");
                                  long ledgerId = Long.parseLong(ledgerIdInHex, 16);
                                  if (index.getName().endsWith(RLOC)) {
                                      if (findIndexFile(ledgerId) != null) {
                                          if (!index.delete()) {
                                              LOG.warn("Deleting the rloc file " + index + " failed");
                                          }
                                          continue;
                                      } else {
                                          File dest = new File(index.getParentFile(), ledgerIdInHex + IDX);
                                          if (!index.renameTo(dest)) {
                                              throw new IOException("Renaming rloc file " + index
                                                      + " to index file has failed");
                                          }
                                      }
                                  }
                                  activeLedgers.put(ledgerId, true);
                              }
                          }
                      }
                  }
              }
          }
      }
  
      /**
       * This method is called whenever a ledger is deleted by the BookKeeper Client
       * and we want to remove all relevant data for it stored in the LedgerCache.
       */
      void removeLedger(Long ledgerId) throws IOException {
          // Delete the ledger's index file and close the FileInfo
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              // Don't force flush. There's no need since we're deleting the ledger
              // anyway, and recreating the file at this point, although safe, will
              // force the garbage collector to do more work later.
              fi.close(false);
              fi.delete();
          } finally {
              if (fi != null) {
                  // should release use count
                  fi.release();
                  // Remove it from the active ledger manager
                  activeLedgers.remove(ledgerId);
                  // Now remove it from cache
                  writeFileInfoCache.invalidate(ledgerId);
                  readFileInfoCache.invalidate(ledgerId);
              }
          }
      }
  
      private File findIndexFile(long ledgerId) throws IOException {
          String ledgerName = getLedgerName(ledgerId);
          for (File d : ledgerDirsManager.getAllLedgerDirs()) {
              File lf = new File(d, ledgerName);
              if (lf.exists()) {
                  return lf;
              }
          }
          return null;
      }
  
      boolean ledgerExists(long ledgerId) throws IOException {
          return activeLedgers.containsKey(ledgerId);
      }
  
      void close() throws IOException {
          // Don't force create the file. We may have many dirty ledgers and file create/flush
          // can be quite expensive as a result. We can use this optimization in this case
          // because metadata will be recovered from the journal when we restart anyway.
          fileInfoBackingCache.closeAllWithoutFlushing();
          writeFileInfoCache.invalidateAll();
          readFileInfoCache.invalidateAll();
      }
  
      Long getLastAddConfirmed(long ledgerId) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return fi.getLastAddConfirmed();
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      boolean waitForLastAddConfirmedUpdate(long ledgerId,
                                            long previousLAC,
                                            Watcher<LastAddConfirmedUpdateNotification> watcher) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return fi.waitForLastAddConfirmedUpdate(previousLAC, watcher);
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      void cancelWaitForLastAddConfirmedUpdate(long ledgerId,
                                            Watcher<LastAddConfirmedUpdateNotification> watcher) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              fi.cancelWaitForLastAddConfirmedUpdate(watcher);
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      long updateLastAddConfirmed(long ledgerId, long lac) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return fi.setLastAddConfirmed(lac);
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      byte[] readMasterKey(long ledgerId) throws IOException, BookieException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return fi.getMasterKey();
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      void setMasterKey(long ledgerId, byte[] masterKey) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, masterKey);
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      boolean setFenced(long ledgerId) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return fi.setFenced();
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      boolean isFenced(long ledgerId) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return fi.isFenced();
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      void setExplicitLac(long ledgerId, ByteBuf lac) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              fi.setExplicitLac(lac);
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      public ByteBuf getExplicitLac(long ledgerId) {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return fi.getExplicitLac();
          } catch (IOException e) {
              LOG.error("Exception during getLastAddConfirmed", e);
              return null;
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      int getOpenFileLimit() {
          return openFileLimit;
      }
  
      private void relocateIndexFileAndFlushHeader(long ledger, FileInfo fi) throws IOException {
          File currentDir = getLedgerDirForLedger(fi);
          if (ledgerDirsManager.isDirFull(currentDir)) {
              try {
                  moveLedgerIndexFile(ledger, fi);
              } catch (NoWritableLedgerDirException nwe) {
                  /*
                   * if there is no other indexDir, which could accommodate new
                   * indexFile but the current indexDir has enough space
                   * (minUsableSizeForIndexFileCreation) for this flushHeader
                   * operation, then it is ok to proceed without moving
                   * LedgerIndexFile.
                   */
                  if (!ledgerDirsManager.isDirWritableForNewIndexFile(currentDir)) {
                      throw nwe;
                  }
              }
          }
          fi.flushHeader();
      }
  
      /**
       * Get the ledger directory that the ledger index belongs to.
       *
       * @param fi File info of a ledger
       * @return ledger directory that the ledger belongs to.
       */
      private File getLedgerDirForLedger(FileInfo fi) {
          return fi.getLf().getParentFile().getParentFile().getParentFile();
      }
  
      private void moveLedgerIndexFile(Long l, FileInfo fi) throws NoWritableLedgerDirException, IOException {
          File newLedgerIndexFile = getNewLedgerIndexFile(l, getLedgerDirForLedger(fi));
          try {
              fi.moveToNewLocation(newLedgerIndexFile, fi.getSizeSinceLastWrite());
          } catch (FileInfo.FileInfoDeletedException fileInfoDeleted) {
              // File concurrently deleted
              throw new Bookie.NoLedgerException(l);
          }
      }
  
      void flushLedgerHeader(long ledger) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledger, null);
              relocateIndexFileAndFlushHeader(ledger, fi);
          } catch (Bookie.NoLedgerException nle) {
              // ledger has been deleted
              LOG.info("No ledger {} found when flushing header.", ledger);
              return;
          } finally {
              if (null != fi) {
                  fi.release();
              }
          }
      }
  
      void flushLedgerEntries(long l, List<LedgerEntryPage> entries) throws IOException {
          CachedFileInfo fi = null;
          try {
              Collections.sort(entries, new Comparator<LedgerEntryPage>() {
                  @Override
                  public int compare(LedgerEntryPage o1, LedgerEntryPage o2) {
                      return (int) (o1.getFirstEntry() - o2.getFirstEntry());
                  }
              });
              int[] versions = new int[entries.size()];
              try {
                  fi = getFileInfo(l, null);
              } catch (Bookie.NoLedgerException nle) {
                  // ledger has been deleted
                  LOG.info("No ledger {} found when flushing entries.", l);
                  return;
              }
  
              // flush the header if necessary
              relocateIndexFileAndFlushHeader(l, fi);
              int start = 0;
              long lastOffset = -1;
              for (int i = 0; i < entries.size(); i++) {
                  versions[i] = entries.get(i).getVersion();
                  if (lastOffset != -1 && (entries.get(i).getFirstEntry() - lastOffset) != entriesPerPage) {
                      // send up a sequential list
                      int count = i - start;
                      if (count == 0) {
                          LOG.warn("Count cannot possibly be zero!");
                      }
                      writeBuffers(l, entries, fi, start, count);
                      start = i;
                  }
                  lastOffset = entries.get(i).getFirstEntry();
              }
              if (entries.size() - start == 0 && entries.size() != 0) {
                  LOG.warn("Nothing to write, but there were entries!");
              }
              writeBuffers(l, entries, fi, start, entries.size() - start);
              for (int i = 0; i < entries.size(); i++) {
                  LedgerEntryPage lep = entries.get(i);
                  lep.setClean(versions[i]);
              }
              if (LOG.isDebugEnabled()) {
                  LOG.debug("Flushed ledger {} with {} pages.", l, entries.size());
              }
          } finally {
              if (fi != null) {
                  fi.release();
              }
          }
      }
  
      private void writeBuffers(Long ledger,
                                List<LedgerEntryPage> entries, FileInfo fi,
                                int start, int count) throws IOException, Bookie.NoLedgerException {
          if (LOG.isTraceEnabled()) {
              LOG.trace("Writing {} buffers of {}", count, Long.toHexString(ledger));
          }
          if (count == 0) {
              return;
          }
          ByteBuffer[] buffs = new ByteBuffer[count];
          for (int j = 0; j < count; j++) {
              buffs[j] = entries.get(start + j).getPageToWrite();
              if (entries.get(start + j).getLedger() != ledger) {
                  throw new IOException("Writing to " + ledger + " but page belongs to "
                                  + entries.get(start + j).getLedger());
              }
          }
          long totalWritten = 0;
          while (buffs[buffs.length - 1].remaining() > 0) {
              long rc = 0;
              try {
                  rc = fi.write(buffs, entries.get(start + 0).getFirstEntryPosition());
              } catch (FileInfo.FileInfoDeletedException e) {
                  throw new Bookie.NoLedgerException(ledger);
              }
              if (rc <= 0) {
                  throw new IOException("Short write to ledger " + ledger + " rc = " + rc);
              }
              totalWritten += rc;
          }
          if (totalWritten != (long) count * (long) pageSize) {
              throw new IOException("Short write to ledger " + ledger + " wrote " + totalWritten
                              + " expected " + count * pageSize);
          }
      }
  
      /**
       * Update the ledger entry page.
       *
       * @param lep
       *          ledger entry page
       * @return true if it is a new page, otherwise false.
       * @throws IOException
       */
      boolean updatePage(LedgerEntryPage lep) throws IOException {
          if (!lep.isClean()) {
              throw new IOException("Trying to update a dirty page");
          }
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(lep.getLedger(), null);
              long pos = lep.getFirstEntryPosition();
              if (pos >= fi.size()) {
                  lep.zeroPage();
                  return true;
              } else {
                  lep.readPage(fi);
                  return false;
              }
          } finally {
              if (fi != null) {
                  fi.release();
              }
          }
      }
  
      long getPersistEntryBeyondInMem(long ledgerId, long lastEntryInMem) throws IOException {
          CachedFileInfo fi = null;
          long lastEntry = lastEntryInMem;
          try {
              fi = getFileInfo(ledgerId, null);
              long size = fi.size();
              // make sure the file size is aligned with index entry size
              // otherwise we may read incorrect data
              if (0 != size % LedgerEntryPage.getIndexEntrySize()) {
                  LOG.warn("Index file of ledger {} is not aligned with index entry size.", ledgerId);
                  size = size - size % LedgerEntryPage.getIndexEntrySize();
              }
              // we may not have the last entry in the cache
              if (size > lastEntry * LedgerEntryPage.getIndexEntrySize()) {
                  ByteBuffer bb = ByteBuffer.allocate(pageSize);
                  long position = size - pageSize;
                  if (position < 0) {
                      position = 0;
                  }
                  // we read the last page from file size minus page size, so it should not encounter short read
                  // exception. if it does, it is an unexpected situation, then throw the exception and fail it
                  // immediately.
                  try {
                      fi.read(bb, position, false);
                  } catch (ShortReadException sre) {
                      // throw a more meaningful exception with ledger id
                      throw new ShortReadException("Short read on ledger " + ledgerId + " : ", sre);
                  }
                  bb.flip();
                  long startingEntryId = position / LedgerEntryPage.getIndexEntrySize();
                  for (int i = entriesPerPage - 1; i >= 0; i--) {
                      if (bb.getLong(i * LedgerEntryPage.getIndexEntrySize()) != 0) {
                          if (lastEntry < startingEntryId + i) {
                              lastEntry = startingEntryId + i;
                          }
                          break;
                      }
                  }
              }
          } finally {
              if (fi != null) {
                  fi.release();
              }
          }
          return lastEntry;
      }
  
      /**
       * Read ledger meta.
       * @param ledgerId Ledger Id
       */
      public LedgerCache.LedgerIndexMetadata readLedgerIndexMetadata(long ledgerId) throws IOException {
          CachedFileInfo fi = null;
          try {
              fi = getFileInfo(ledgerId, null);
              return new LedgerCache.LedgerIndexMetadata(
                      fi.getMasterKey(),
                      fi.size(),
                      fi.isFenced());
          } finally {
              if (fi != null) {
                  fi.release();
              }
          }
      }
  }
  