FileReftableStack.java

  1. /*
  2.  * Copyright (C) 2019 Google LLC and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.internal.storage.file;

  11. import static java.nio.charset.StandardCharsets.UTF_8;

  12. import java.io.BufferedReader;
  13. import java.io.File;
  14. import java.io.FileInputStream;
  15. import java.io.FileNotFoundException;
  16. import java.io.FileOutputStream;
  17. import java.io.IOException;
  18. import java.io.InputStreamReader;
  19. import java.nio.file.Files;
  20. import java.nio.file.StandardCopyOption;
  21. import java.security.SecureRandom;
  22. import java.util.ArrayList;
  23. import java.util.Comparator;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Optional;
  27. import java.util.function.Supplier;
  28. import java.util.stream.Collectors;

  29. import org.eclipse.jgit.annotations.Nullable;
  30. import org.eclipse.jgit.errors.LockFailedException;
  31. import org.eclipse.jgit.internal.storage.io.BlockSource;
  32. import org.eclipse.jgit.internal.storage.reftable.MergedReftable;
  33. import org.eclipse.jgit.internal.storage.reftable.ReftableCompactor;
  34. import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
  35. import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
  36. import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
  37. import org.eclipse.jgit.lib.Config;
  38. import org.eclipse.jgit.util.FileUtils;
  39. import org.eclipse.jgit.util.SystemReader;

  40. /**
  41.  * A mutable stack of reftables on local filesystem storage. Not thread-safe.
  42.  * This is an AutoCloseable because this object owns the file handles to the
  43.  * open reftables.
  44.  */
  45. public class FileReftableStack implements AutoCloseable {
  46.     private static class StackEntry {

  47.         String name;

  48.         ReftableReader reftableReader;
  49.     }

  50.     private MergedReftable mergedReftable;

  51.     private List<StackEntry> stack;

  52.     private long lastNextUpdateIndex;

  53.     private final File stackPath;

  54.     private final File reftableDir;

  55.     private final Runnable onChange;

  56.     private final SecureRandom random = new SecureRandom();

  57.     private final Supplier<Config> configSupplier;

  58.     // Used for stats & testing.
  59.     static class CompactionStats {

  60.         long tables;

  61.         long bytes;

  62.         int attempted;

  63.         int failed;

  64.         long refCount;

  65.         long logCount;

  66.         CompactionStats() {
  67.             tables = 0;
  68.             bytes = 0;
  69.             attempted = 0;
  70.             failed = 0;
  71.             logCount = 0;
  72.             refCount = 0;
  73.         }
  74.     }

  75.     private final CompactionStats stats;

  76.     /**
  77.      * Creates a stack corresponding to the list of reftables in the argument
  78.      *
  79.      * @param stackPath
  80.      *            the filename for the stack.
  81.      * @param reftableDir
  82.      *            the dir holding the tables.
  83.      * @param onChange
  84.      *            hook to call if we notice a new write
  85.      * @param configSupplier
  86.      *            Config supplier
  87.      * @throws IOException
  88.      *             on I/O problems
  89.      */
  90.     public FileReftableStack(File stackPath, File reftableDir,
  91.             @Nullable Runnable onChange, Supplier<Config> configSupplier)
  92.             throws IOException {
  93.         this.stackPath = stackPath;
  94.         this.reftableDir = reftableDir;
  95.         this.stack = new ArrayList<>();
  96.         this.configSupplier = configSupplier;
  97.         this.onChange = onChange;

  98.         // skip event notification
  99.         lastNextUpdateIndex = 0;
  100.         reload();

  101.         stats = new CompactionStats();
  102.     }

  103.     CompactionStats getStats() {
  104.         return stats;
  105.     }

  106.     /**
  107.      * Reloads the stack, potentially reusing opened reftableReaders.
  108.      *
  109.      * @param names
  110.      *            holds the names of the tables to load.
  111.      * @throws FileNotFoundException
  112.      *             load must be retried.
  113.      * @throws IOException
  114.      *             on other IO errors.
  115.      */
  116.     private void reloadOnce(List<String> names)
  117.             throws IOException, FileNotFoundException {
  118.         Map<String, ReftableReader> current = stack.stream()
  119.                 .collect(Collectors.toMap(e -> e.name, e -> e.reftableReader));

  120.         List<ReftableReader> newTables = new ArrayList<>();
  121.         List<StackEntry> newStack = new ArrayList<>(stack.size() + 1);
  122.         try {
  123.             for (String name : names) {
  124.                 StackEntry entry = new StackEntry();
  125.                 entry.name = name;

  126.                 ReftableReader t = null;
  127.                 if (current.containsKey(name)) {
  128.                     t = current.remove(name);
  129.                 } else {
  130.                     File subtable = new File(reftableDir, name);
  131.                     FileInputStream is;

  132.                     is = new FileInputStream(subtable);

  133.                     t = new ReftableReader(BlockSource.from(is));
  134.                     newTables.add(t);
  135.                 }

  136.                 entry.reftableReader = t;
  137.                 newStack.add(entry);
  138.             }
  139.             // survived without exceptions: swap in new stack, and close
  140.             // dangling tables.
  141.             stack = newStack;
  142.             newTables.clear();

  143.             current.values().forEach(r -> {
  144.                 try {
  145.                     r.close();
  146.                 } catch (IOException e) {
  147.                     throw new AssertionError(e);
  148.                 }
  149.             });
  150.         } finally {
  151.             newTables.forEach(t -> {
  152.                 try {
  153.                     t.close();
  154.                 } catch (IOException ioe) {
  155.                     // reader close should not generate errors.
  156.                     throw new AssertionError(ioe);
  157.                 }
  158.             });
  159.         }
  160.     }

  161.     void reload() throws IOException {
  162.         // Try for 2.5 seconds.
  163.         long deadline = System.currentTimeMillis() + 2500;
  164.         // A successful reftable transaction is 2 atomic file writes
  165.         // (open, write, close, rename), which a fast Linux system should be
  166.         // able to do in about ~200us. So 1 ms should be ample time.
  167.         long min = 1;
  168.         long max = 1000;
  169.         long delay = 0;
  170.         boolean success = false;

  171.         // Don't check deadline for the first 3 retries, so we can step with a
  172.         // debugger without worrying about deadlines.
  173.         int tries = 0;
  174.         while (tries < 3 || System.currentTimeMillis() < deadline) {
  175.             List<String> names = readTableNames();
  176.             tries++;
  177.             try {
  178.                 reloadOnce(names);
  179.                 success = true;
  180.                 break;
  181.             } catch (FileNotFoundException e) {
  182.                 List<String> changed = readTableNames();
  183.                 if (changed.equals(names)) {
  184.                     throw e;
  185.                 }
  186.             }

  187.             delay = FileUtils.delay(delay, min, max);
  188.             try {
  189.                 Thread.sleep(delay);
  190.             } catch (InterruptedException e) {
  191.                 Thread.currentThread().interrupt();
  192.                 throw new RuntimeException(e);
  193.             }
  194.         }

  195.         if (!success) {
  196.             throw new LockFailedException(stackPath);
  197.         }

  198.         mergedReftable = new MergedReftable(stack.stream()
  199.                 .map(x -> x.reftableReader).collect(Collectors.toList()));
  200.         long curr = nextUpdateIndex();
  201.         if (lastNextUpdateIndex > 0 && lastNextUpdateIndex != curr
  202.                 && onChange != null) {
  203.             onChange.run();
  204.         }
  205.         lastNextUpdateIndex = curr;
  206.     }

  207.     /**
  208.      * @return the merged reftable
  209.      */
  210.     public MergedReftable getMergedReftable() {
  211.         return mergedReftable;
  212.     }

  213.     /**
  214.      * Writer is a callable that writes data to a reftable under construction.
  215.      * It should set the min/max update index, and then write refs and/or logs.
  216.      * It should not call finish() on the writer.
  217.      */
  218.     public interface Writer {
  219.         /**
  220.          * Write data to reftable
  221.          *
  222.          * @param w
  223.          *            writer to use
  224.          * @throws IOException
  225.          */
  226.         void call(ReftableWriter w) throws IOException;
  227.     }

  228.     private List<String> readTableNames() throws IOException {
  229.         List<String> names = new ArrayList<>(stack.size() + 1);

  230.         try (BufferedReader br = new BufferedReader(
  231.                 new InputStreamReader(new FileInputStream(stackPath), UTF_8))) {
  232.             String line;
  233.             while ((line = br.readLine()) != null) {
  234.                 if (!line.isEmpty()) {
  235.                     names.add(line);
  236.                 }
  237.             }
  238.         } catch (FileNotFoundException e) {
  239.             // file isn't there: empty repository.
  240.         }
  241.         return names;
  242.     }

  243.     /**
  244.      * @return true if the on-disk file corresponds to the in-memory data.
  245.      * @throws IOException
  246.      *             on IO problem
  247.      */
  248.     boolean isUpToDate() throws IOException {
  249.         // We could use FileSnapshot to avoid reading the file, but the file is
  250.         // small so it's probably a minor optimization.
  251.         try {
  252.             List<String> names = readTableNames();
  253.             if (names.size() != stack.size()) {
  254.                 return false;
  255.             }
  256.             for (int i = 0; i < names.size(); i++) {
  257.                 if (!names.get(i).equals(stack.get(i).name)) {
  258.                     return false;
  259.                 }
  260.             }
  261.         } catch (FileNotFoundException e) {
  262.             return stack.isEmpty();
  263.         }
  264.         return true;
  265.     }

  266.     /**
  267.      * {@inheritDoc}
  268.      */
  269.     @Override
  270.     public void close() {
  271.         for (StackEntry entry : stack) {
  272.             try {
  273.                 entry.reftableReader.close();
  274.             } catch (Exception e) {
  275.                 // we are reading; this should never fail.
  276.                 throw new AssertionError(e);
  277.             }
  278.         }
  279.     }

  280.     private long nextUpdateIndex() throws IOException {
  281.         return stack.size() > 0
  282.                 ? stack.get(stack.size() - 1).reftableReader.maxUpdateIndex()
  283.                         + 1
  284.                 : 1;
  285.     }

  286.     private String filename(long low, long high) {
  287.         return String.format("%012x-%012x-%08x", //$NON-NLS-1$
  288.                 Long.valueOf(low), Long.valueOf(high),
  289.                 Integer.valueOf(random.nextInt()));
  290.     }

  291.     /**
  292.      * Tries to add a new reftable to the stack. Returns true if it succeeded,
  293.      * or false if there was a lock failure, due to races with other processes.
  294.      * This is package private so FileReftableDatabase can call into here.
  295.      *
  296.      * @param w
  297.      *            writer to write data to a reftable under construction
  298.      * @return true if the transaction was successful.
  299.      * @throws IOException
  300.      *             on I/O problems
  301.      */
  302.     @SuppressWarnings("nls")
  303.     public boolean addReftable(Writer w) throws IOException {
  304.         LockFile lock = new LockFile(stackPath);
  305.         try {
  306.             if (!lock.lockForAppend()) {
  307.                 return false;
  308.             }
  309.             if (!isUpToDate()) {
  310.                 return false;
  311.             }

  312.             String fn = filename(nextUpdateIndex(), nextUpdateIndex());

  313.             File tmpTable = File.createTempFile(fn + "_", ".ref",
  314.                     stackPath.getParentFile());

  315.             ReftableWriter.Stats s;
  316.             try (FileOutputStream fos = new FileOutputStream(tmpTable)) {
  317.                 ReftableWriter rw = new ReftableWriter(reftableConfig(), fos);
  318.                 w.call(rw);
  319.                 rw.finish();
  320.                 s = rw.getStats();
  321.             }

  322.             if (s.minUpdateIndex() < nextUpdateIndex()) {
  323.                 return false;
  324.             }

  325.             // The spec says to name log-only files with .log, which is somewhat
  326.             // pointless given compaction, but we do so anyway.
  327.             fn += s.refCount() > 0 ? ".ref" : ".log";
  328.             File dest = new File(reftableDir, fn);

  329.             FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE);
  330.             lock.write((fn + "\n").getBytes(UTF_8));
  331.             if (!lock.commit()) {
  332.                 FileUtils.delete(dest);
  333.                 return false;
  334.             }

  335.             reload();

  336.             autoCompact();
  337.         } finally {
  338.             lock.unlock();
  339.         }
  340.         return true;
  341.     }

  342.     private ReftableConfig reftableConfig() {
  343.         return new ReftableConfig(configSupplier.get());
  344.     }

  345.     /**
  346.      * Write the reftable for the given range into a temp file.
  347.      *
  348.      * @param first
  349.      *            index of first stack entry to be written
  350.      * @param last
  351.      *            index of last stack entry to be written
  352.      * @return the file holding the replacement table.
  353.      * @throws IOException
  354.      *             on I/O problem
  355.      */
  356.     private File compactLocked(int first, int last) throws IOException {
  357.         String fn = filename(first, last);

  358.         File tmpTable = File.createTempFile(fn + "_", ".ref", //$NON-NLS-1$//$NON-NLS-2$
  359.                 stackPath.getParentFile());
  360.         try (FileOutputStream fos = new FileOutputStream(tmpTable)) {
  361.             ReftableCompactor c = new ReftableCompactor(fos)
  362.                     .setConfig(reftableConfig())
  363.                     .setIncludeDeletes(first > 0);

  364.             List<ReftableReader> compactMe = new ArrayList<>();
  365.             long totalBytes = 0;
  366.             for (int i = first; i <= last; i++) {
  367.                 compactMe.add(stack.get(i).reftableReader);
  368.                 totalBytes += stack.get(i).reftableReader.size();
  369.             }
  370.             c.addAll(compactMe);

  371.             c.compact();

  372.             // Even though the compaction did not definitely succeed, we keep
  373.             // tally here as we've expended the effort.
  374.             stats.bytes += totalBytes;
  375.             stats.tables += first - last + 1;
  376.             stats.attempted++;
  377.             stats.refCount += c.getStats().refCount();
  378.             stats.logCount += c.getStats().logCount();
  379.         }

  380.         return tmpTable;
  381.     }

  382.     /**
  383.      * Compacts a range of the stack, following the file locking protocol
  384.      * documented in the spec.
  385.      *
  386.      * @param first
  387.      *            index of first stack entry to be considered in compaction
  388.      * @param last
  389.      *            index of last stack entry to be considered in compaction
  390.      * @return true if a compaction was successfully applied.
  391.      * @throws IOException
  392.      *             on I/O problem
  393.      */
  394.     boolean compactRange(int first, int last) throws IOException {
  395.         if (first >= last) {
  396.             return true;
  397.         }
  398.         LockFile lock = new LockFile(stackPath);

  399.         File tmpTable = null;
  400.         List<LockFile> subtableLocks = new ArrayList<>();

  401.         try {
  402.             if (!lock.lock()) {
  403.                 return false;
  404.             }
  405.             if (!isUpToDate()) {
  406.                 return false;
  407.             }

  408.             List<File> deleteOnSuccess = new ArrayList<>();
  409.             for (int i = first; i <= last; i++) {
  410.                 File f = new File(reftableDir, stack.get(i).name);
  411.                 LockFile lf = new LockFile(f);
  412.                 if (!lf.lock()) {
  413.                     return false;
  414.                 }
  415.                 subtableLocks.add(lf);
  416.                 deleteOnSuccess.add(f);
  417.             }

  418.             lock.unlock();
  419.             lock = null;

  420.             tmpTable = compactLocked(first, last);

  421.             lock = new LockFile(stackPath);
  422.             if (!lock.lock()) {
  423.                 return false;
  424.             }
  425.             if (!isUpToDate()) {
  426.                 return false;
  427.             }

  428.             String fn = filename(
  429.                     stack.get(first).reftableReader.minUpdateIndex(),
  430.                     stack.get(last).reftableReader.maxUpdateIndex());

  431.             // The spec suggests to use .log for log-only tables, and collect
  432.             // all log entries in a single file at the bottom of the stack. That would
  433.             // require supporting overlapping ranges for the different tables. For the
  434.             // sake of simplicity, we simply ignore this and always produce a log +
  435.             // ref combined table.
  436.             fn += ".ref"; //$NON-NLS-1$
  437.             File dest = new File(reftableDir, fn);

  438.             FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE);
  439.             tmpTable = null;

  440.             StringBuilder sb = new StringBuilder();

  441.             for (int i = 0; i < first; i++) {
  442.                 sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  443.             }
  444.             sb.append(fn + "\n"); //$NON-NLS-1$
  445.             for (int i = last + 1; i < stack.size(); i++) {
  446.                 sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  447.             }

  448.             lock.write(sb.toString().getBytes(UTF_8));
  449.             if (!lock.commit()) {
  450.                 dest.delete();
  451.                 return false;
  452.             }

  453.             reload();
  454.             for (File f : deleteOnSuccess) {
  455.                 try {
  456.                     Files.delete(f.toPath());
  457.                 } catch (IOException e) {
  458.                     // Ignore: this can happen on Windows in case of concurrent processes.
  459.                     // leave the garbage and continue.
  460.                     if (!SystemReader.getInstance().isWindows()) {
  461.                         throw e;
  462.                     }
  463.                 }
  464.             }

  465.             return true;
  466.         } finally {
  467.             if (tmpTable != null) {
  468.                 tmpTable.delete();
  469.             }
  470.             for (LockFile lf : subtableLocks) {
  471.                 lf.unlock();
  472.             }
  473.             if (lock != null) {
  474.                 lock.unlock();
  475.             }
  476.         }
  477.     }

  478.     /**
  479.      * Calculate an approximate log2.
  480.      *
  481.      * @param sz
  482.      * @return log2
  483.      */
  484.     static int log(long sz) {
  485.         long base = 2;
  486.         if (sz <= 0) {
  487.             throw new IllegalArgumentException("log2 negative"); //$NON-NLS-1$
  488.         }
  489.         int l = 0;
  490.         while (sz > 0) {
  491.             l++;
  492.             sz /= base;
  493.         }

  494.         return l - 1;
  495.     }

  496.     /**
  497.      * A segment is a consecutive list of reftables of the same approximate
  498.      * size.
  499.      */
  500.     static class Segment {
  501.         // the approximate log_2 of the size.
  502.         int log;

  503.         // The total bytes in this segment
  504.         long bytes;

  505.         int start;

  506.         int end; // exclusive.

  507.         int size() {
  508.             return end - start;
  509.         }

  510.         Segment(int start, int end, int log, long bytes) {
  511.             this.log = log;
  512.             this.start = start;
  513.             this.end = end;
  514.             this.bytes = bytes;
  515.         }

  516.         Segment() {
  517.             this(0, 0, 0, 0);
  518.         }

  519.         @Override
  520.         public int hashCode() {
  521.             return 0; // appease error-prone
  522.         }

  523.         @Override
  524.         public boolean equals(Object other) {
  525.             if (other == null) {
  526.                 return false;
  527.             }
  528.             Segment o = (Segment) other;
  529.             return o.bytes == bytes && o.log == log && o.start == start
  530.                     && o.end == end;
  531.         }

  532.         @SuppressWarnings("boxing")
  533.         @Override
  534.         public String toString() {
  535.             return String.format("{ [%d,%d) l=%d sz=%d }", start, end, log, //$NON-NLS-1$
  536.                     bytes);
  537.         }
  538.     }

  539.     static List<Segment> segmentSizes(long[] sizes) {
  540.         List<Segment> segments = new ArrayList<>();
  541.         Segment cur = new Segment();
  542.         for (int i = 0; i < sizes.length; i++) {
  543.             int l = log(sizes[i]);
  544.             if (l != cur.log && cur.bytes > 0) {
  545.                 segments.add(cur);
  546.                 cur = new Segment();
  547.                 cur.start = i;
  548.                 cur.log = l;
  549.             }

  550.             cur.log = l;
  551.             cur.end = i + 1;
  552.             cur.bytes += sizes[i];
  553.         }
  554.         segments.add(cur);
  555.         return segments;
  556.     }

  557.     private static Optional<Segment> autoCompactCandidate(long[] sizes) {
  558.         if (sizes.length == 0) {
  559.             return Optional.empty();
  560.         }

  561.         // The cost of compaction is proportional to the size, and we want to
  562.         // avoid frequent large compactions. We do this by playing the game 2048
  563.         // here: first compact together the smallest tables if there are more
  564.         // than one. Then try to see if the result will be big enough to match
  565.         // up with next up.

  566.         List<Segment> segments = segmentSizes(sizes);
  567.         segments = segments.stream().filter(s -> s.size() > 1)
  568.                 .collect(Collectors.toList());
  569.         if (segments.isEmpty()) {
  570.             return Optional.empty();
  571.         }

  572.         Optional<Segment> optMinSeg = segments.stream()
  573.                 .min(Comparator.comparing(s -> Integer.valueOf(s.log)));
  574.         // Input is non-empty, so always present.
  575.         Segment smallCollected = optMinSeg.get();
  576.         while (smallCollected.start > 0) {
  577.             int prev = smallCollected.start - 1;
  578.             long prevSize = sizes[prev];
  579.             if (log(smallCollected.bytes) < log(prevSize)) {
  580.                 break;
  581.             }
  582.             smallCollected.start = prev;
  583.             smallCollected.bytes += prevSize;
  584.         }

  585.         return Optional.of(smallCollected);
  586.     }

  587.     /**
  588.      * Heuristically tries to compact the stack if the stack has a suitable
  589.      * shape.
  590.      *
  591.      * @throws IOException
  592.      */
  593.     private void autoCompact() throws IOException {
  594.         Optional<Segment> cand = autoCompactCandidate(tableSizes());
  595.         if (cand.isPresent()) {
  596.             if (!compactRange(cand.get().start, cand.get().end - 1)) {
  597.                 stats.failed++;
  598.             }
  599.         }
  600.     }

  601.     // 68b footer, 24b header = 92.
  602.     private static long OVERHEAD = 91;

  603.     private long[] tableSizes() throws IOException {
  604.         long[] sizes = new long[stack.size()];
  605.         for (int i = 0; i < stack.size(); i++) {
  606.             // If we don't subtract the overhead, the file size isn't
  607.             // proportional to the number of entries. This will cause us to
  608.             // compact too often, which is expensive.
  609.             sizes[i] = stack.get(i).reftableReader.size() - OVERHEAD;
  610.         }
  611.         return sizes;
  612.     }

  613.     void compactFully() throws IOException {
  614.         if (!compactRange(0, stack.size() - 1)) {
  615.             stats.failed++;
  616.         }
  617.     }
  618. }