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

  46.         String name;

  47.         ReftableReader reftableReader;
  48.     }

  49.     private MergedReftable mergedReftable;

  50.     private List<StackEntry> stack;

  51.     private long lastNextUpdateIndex;

  52.     private final File stackPath;

  53.     private final File reftableDir;

  54.     private final Runnable onChange;

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

  56.     private final Supplier<Config> configSupplier;

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

  59.         long tables;

  60.         long bytes;

  61.         int attempted;

  62.         int failed;

  63.         long refCount;

  64.         long logCount;

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

  74.     private final CompactionStats stats;

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

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

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

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

  105.     /** Thrown if the update indices in the stack are not monotonic */
  106.     public static class ReftableNumbersNotIncreasingException
  107.             extends RuntimeException {
  108.         private static final long serialVersionUID = 1L;

  109.         String name;

  110.         long lastMax;

  111.         long min;

  112.         ReftableNumbersNotIncreasingException(String name, long lastMax,
  113.                 long min) {
  114.             this.name = name;
  115.             this.lastMax = lastMax;
  116.             this.min = min;
  117.         }

  118.         @SuppressWarnings({ "nls", "boxing" })
  119.         @Override
  120.         public String toString() {
  121.             return String.format(
  122.                     "ReftableNumbersNotIncreasingException %s: min %d, lastMax %d",
  123.                     name, min, lastMax);
  124.         }
  125.     }

  126.     /**
  127.      * Reloads the stack, potentially reusing opened reftableReaders.
  128.      *
  129.      * @param names
  130.      *            holds the names of the tables to load.
  131.      * @throws FileNotFoundException
  132.      *             load must be retried.
  133.      * @throws IOException
  134.      *             on other IO errors.
  135.      */
  136.     private void reloadOnce(List<String> names)
  137.             throws IOException, FileNotFoundException {
  138.         Map<String, ReftableReader> current = stack.stream()
  139.                 .collect(Collectors.toMap(e -> e.name, e -> e.reftableReader));

  140.         List<ReftableReader> newTables = new ArrayList<>();
  141.         List<StackEntry> newStack = new ArrayList<>(stack.size() + 1);
  142.         try {
  143.             ReftableReader last = null;
  144.             for (String name : names) {
  145.                 StackEntry entry = new StackEntry();
  146.                 entry.name = name;

  147.                 ReftableReader t = null;
  148.                 if (current.containsKey(name)) {
  149.                     t = current.remove(name);
  150.                 } else {
  151.                     File subtable = new File(reftableDir, name);
  152.                     FileInputStream is;

  153.                     is = new FileInputStream(subtable);

  154.                     t = new ReftableReader(BlockSource.from(is));
  155.                     newTables.add(t);
  156.                 }

  157.                 if (last != null) {
  158.                     // TODO: move this to MergedReftable
  159.                     if (last.maxUpdateIndex() >= t.minUpdateIndex()) {
  160.                         throw new ReftableNumbersNotIncreasingException(name,
  161.                                 last.maxUpdateIndex(), t.minUpdateIndex());
  162.                     }
  163.                 }
  164.                 last = t;

  165.                 entry.reftableReader = t;
  166.                 newStack.add(entry);
  167.             }
  168.             // survived without exceptions: swap in new stack, and close
  169.             // dangling tables.
  170.             stack = newStack;
  171.             newTables.clear();

  172.             current.values().forEach(r -> {
  173.                 try {
  174.                     r.close();
  175.                 } catch (IOException e) {
  176.                     throw new AssertionError(e);
  177.                 }
  178.             });
  179.         } finally {
  180.             newTables.forEach(t -> {
  181.                 try {
  182.                     t.close();
  183.                 } catch (IOException ioe) {
  184.                     // reader close should not generate errors.
  185.                     throw new AssertionError(ioe);
  186.                 }
  187.             });
  188.         }
  189.     }

  190.     void reload() throws IOException {
  191.         // Try for 2.5 seconds.
  192.         long deadline = System.currentTimeMillis() + 2500;
  193.         // A successful reftable transaction is 2 atomic file writes
  194.         // (open, write, close, rename), which a fast Linux system should be
  195.         // able to do in about ~200us. So 1 ms should be ample time.
  196.         long min = 1;
  197.         long max = 1000;
  198.         long delay = 0;
  199.         boolean success = false;

  200.         // Don't check deadline for the first 3 retries, so we can step with a
  201.         // debugger without worrying about deadlines.
  202.         int tries = 0;
  203.         while (tries < 3 || System.currentTimeMillis() < deadline) {
  204.             List<String> names = readTableNames();
  205.             tries++;
  206.             try {
  207.                 reloadOnce(names);
  208.                 success = true;
  209.                 break;
  210.             } catch (FileNotFoundException e) {
  211.                 List<String> changed = readTableNames();
  212.                 if (changed.equals(names)) {
  213.                     throw e;
  214.                 }
  215.             }

  216.             delay = FileUtils.delay(delay, min, max);
  217.             try {
  218.                 Thread.sleep(delay);
  219.             } catch (InterruptedException e) {
  220.                 Thread.currentThread().interrupt();
  221.                 throw new RuntimeException(e);
  222.             }
  223.         }

  224.         if (!success) {
  225.             throw new LockFailedException(stackPath);
  226.         }

  227.         mergedReftable = new MergedReftable(stack.stream()
  228.                 .map(x -> x.reftableReader).collect(Collectors.toList()));
  229.         long curr = nextUpdateIndex();
  230.         if (lastNextUpdateIndex > 0 && lastNextUpdateIndex != curr
  231.                 && onChange != null) {
  232.             onChange.run();
  233.         }
  234.         lastNextUpdateIndex = curr;
  235.     }

  236.     /**
  237.      * @return the merged reftable
  238.      */
  239.     public MergedReftable getMergedReftable() {
  240.         return mergedReftable;
  241.     }

  242.     /**
  243.      * Writer is a callable that writes data to a reftable under construction.
  244.      * It should set the min/max update index, and then write refs and/or logs.
  245.      * It should not call finish() on the writer.
  246.      */
  247.     public interface Writer {
  248.         /**
  249.          * Write data to reftable
  250.          *
  251.          * @param w
  252.          *            writer to use
  253.          * @throws IOException
  254.          */
  255.         void call(ReftableWriter w) throws IOException;
  256.     }

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

  259.         try (BufferedReader br = new BufferedReader(
  260.                 new InputStreamReader(new FileInputStream(stackPath), UTF_8))) {
  261.             String line;
  262.             while ((line = br.readLine()) != null) {
  263.                 if (!line.isEmpty()) {
  264.                     names.add(line);
  265.                 }
  266.             }
  267.         } catch (FileNotFoundException e) {
  268.             // file isn't there: empty repository.
  269.         }
  270.         return names;
  271.     }

  272.     /**
  273.      * @return true if the on-disk file corresponds to the in-memory data.
  274.      * @throws IOException
  275.      *             on IO problem
  276.      */
  277.     boolean isUpToDate() throws IOException {
  278.         // We could use FileSnapshot to avoid reading the file, but the file is
  279.         // small so it's probably a minor optimization.
  280.         try {
  281.             List<String> names = readTableNames();
  282.             if (names.size() != stack.size()) {
  283.                 return false;
  284.             }
  285.             for (int i = 0; i < names.size(); i++) {
  286.                 if (!names.get(i).equals(stack.get(i).name)) {
  287.                     return false;
  288.                 }
  289.             }
  290.         } catch (FileNotFoundException e) {
  291.             return stack.isEmpty();
  292.         }
  293.         return true;
  294.     }

  295.     /**
  296.      * {@inheritDoc}
  297.      */
  298.     @Override
  299.     public void close() {
  300.         for (StackEntry entry : stack) {
  301.             try {
  302.                 entry.reftableReader.close();
  303.             } catch (Exception e) {
  304.                 // we are reading; this should never fail.
  305.                 throw new AssertionError(e);
  306.             }
  307.         }
  308.     }

  309.     private long nextUpdateIndex() throws IOException {
  310.         return stack.size() > 0
  311.                 ? stack.get(stack.size() - 1).reftableReader.maxUpdateIndex()
  312.                         + 1
  313.                 : 1;
  314.     }

  315.     private String filename(long low, long high) {
  316.         return String.format("%012x-%012x-%08x", //$NON-NLS-1$
  317.                 Long.valueOf(low), Long.valueOf(high),
  318.                 Integer.valueOf(random.nextInt()));
  319.     }

  320.     /**
  321.      * Tries to add a new reftable to the stack. Returns true if it succeeded,
  322.      * or false if there was a lock failure, due to races with other processes.
  323.      * This is package private so FileReftableDatabase can call into here.
  324.      *
  325.      * @param w
  326.      *            writer to write data to a reftable under construction
  327.      * @return true if the transaction was successful.
  328.      * @throws IOException
  329.      *             on I/O problems
  330.      */
  331.     @SuppressWarnings("nls")
  332.     public boolean addReftable(Writer w) throws IOException {
  333.         LockFile lock = new LockFile(stackPath);
  334.         try {
  335.             if (!lock.lockForAppend()) {
  336.                 return false;
  337.             }
  338.             if (!isUpToDate()) {
  339.                 return false;
  340.             }

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

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

  344.             ReftableWriter.Stats s;
  345.             try (FileOutputStream fos = new FileOutputStream(tmpTable)) {
  346.                 ReftableWriter rw = new ReftableWriter(reftableConfig(), fos);
  347.                 w.call(rw);
  348.                 rw.finish();
  349.                 s = rw.getStats();
  350.             }

  351.             if (s.minUpdateIndex() < nextUpdateIndex()) {
  352.                 return false;
  353.             }

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

  358.             FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE);
  359.             lock.write((fn + "\n").getBytes(UTF_8));
  360.             if (!lock.commit()) {
  361.                 FileUtils.delete(dest);
  362.                 return false;
  363.             }

  364.             reload();

  365.             autoCompact();
  366.         } finally {
  367.             lock.unlock();
  368.         }
  369.         return true;
  370.     }

  371.     private ReftableConfig reftableConfig() {
  372.         return new ReftableConfig(configSupplier.get());
  373.     }

  374.     /**
  375.      * Write the reftable for the given range into a temp file.
  376.      *
  377.      * @param first
  378.      *            index of first stack entry to be written
  379.      * @param last
  380.      *            index of last stack entry to be written
  381.      * @return the file holding the replacement table.
  382.      * @throws IOException
  383.      *             on I/O problem
  384.      */
  385.     private File compactLocked(int first, int last) throws IOException {
  386.         String fn = filename(first, last);

  387.         File tmpTable = File.createTempFile(fn + "_", ".ref", //$NON-NLS-1$//$NON-NLS-2$
  388.                 stackPath.getParentFile());
  389.         try (FileOutputStream fos = new FileOutputStream(tmpTable)) {
  390.             ReftableCompactor c = new ReftableCompactor(fos)
  391.                     .setConfig(reftableConfig())
  392.                     .setIncludeDeletes(first > 0);

  393.             List<ReftableReader> compactMe = new ArrayList<>();
  394.             long totalBytes = 0;
  395.             for (int i = first; i <= last; i++) {
  396.                 compactMe.add(stack.get(i).reftableReader);
  397.                 totalBytes += stack.get(i).reftableReader.size();
  398.             }
  399.             c.addAll(compactMe);

  400.             c.compact();

  401.             // Even though the compaction did not definitely succeed, we keep
  402.             // tally here as we've expended the effort.
  403.             stats.bytes += totalBytes;
  404.             stats.tables += first - last + 1;
  405.             stats.attempted++;
  406.             stats.refCount += c.getStats().refCount();
  407.             stats.logCount += c.getStats().logCount();
  408.         }

  409.         return tmpTable;
  410.     }

  411.     /**
  412.      * Compacts a range of the stack, following the file locking protocol
  413.      * documented in the spec.
  414.      *
  415.      * @param first
  416.      *            index of first stack entry to be considered in compaction
  417.      * @param last
  418.      *            index of last stack entry to be considered in compaction
  419.      * @return true if a compaction was successfully applied.
  420.      * @throws IOException
  421.      *             on I/O problem
  422.      */
  423.     boolean compactRange(int first, int last) throws IOException {
  424.         if (first >= last) {
  425.             return true;
  426.         }
  427.         LockFile lock = new LockFile(stackPath);

  428.         File tmpTable = null;
  429.         List<LockFile> subtableLocks = new ArrayList<>();

  430.         try {
  431.             if (!lock.lock()) {
  432.                 return false;
  433.             }
  434.             if (!isUpToDate()) {
  435.                 return false;
  436.             }

  437.             List<File> deleteOnSuccess = new ArrayList<>();
  438.             for (int i = first; i <= last; i++) {
  439.                 File f = new File(reftableDir, stack.get(i).name);
  440.                 LockFile lf = new LockFile(f);
  441.                 if (!lf.lock()) {
  442.                     return false;
  443.                 }
  444.                 subtableLocks.add(lf);
  445.                 deleteOnSuccess.add(f);
  446.             }

  447.             lock.unlock();
  448.             lock = null;

  449.             tmpTable = compactLocked(first, last);

  450.             lock = new LockFile(stackPath);
  451.             if (!lock.lock()) {
  452.                 return false;
  453.             }
  454.             if (!isUpToDate()) {
  455.                 return false;
  456.             }

  457.             String fn = filename(
  458.                     stack.get(first).reftableReader.minUpdateIndex(),
  459.                     stack.get(last).reftableReader.maxUpdateIndex());

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

  467.             FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE);
  468.             tmpTable = null;

  469.             StringBuilder sb = new StringBuilder();

  470.             for (int i = 0; i < first; i++) {
  471.                 sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  472.             }
  473.             sb.append(fn + "\n"); //$NON-NLS-1$
  474.             for (int i = last + 1; i < stack.size(); i++) {
  475.                 sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  476.             }

  477.             lock.write(sb.toString().getBytes(UTF_8));
  478.             if (!lock.commit()) {
  479.                 dest.delete();
  480.                 return false;
  481.             }

  482.             for (File f : deleteOnSuccess) {
  483.                 Files.delete(f.toPath());
  484.             }

  485.             reload();
  486.             return true;
  487.         } finally {
  488.             if (tmpTable != null) {
  489.                 tmpTable.delete();
  490.             }
  491.             for (LockFile lf : subtableLocks) {
  492.                 lf.unlock();
  493.             }
  494.             if (lock != null) {
  495.                 lock.unlock();
  496.             }
  497.         }
  498.     }

  499.     /**
  500.      * Calculate an approximate log2.
  501.      *
  502.      * @param sz
  503.      * @return log2
  504.      */
  505.     static int log(long sz) {
  506.         long base = 2;
  507.         if (sz <= 0) {
  508.             throw new IllegalArgumentException("log2 negative"); //$NON-NLS-1$
  509.         }
  510.         int l = 0;
  511.         while (sz > 0) {
  512.             l++;
  513.             sz /= base;
  514.         }

  515.         return l - 1;
  516.     }

  517.     /**
  518.      * A segment is a consecutive list of reftables of the same approximate
  519.      * size.
  520.      */
  521.     static class Segment {
  522.         // the approximate log_2 of the size.
  523.         int log;

  524.         // The total bytes in this segment
  525.         long bytes;

  526.         int start;

  527.         int end; // exclusive.

  528.         int size() {
  529.             return end - start;
  530.         }

  531.         Segment(int start, int end, int log, long bytes) {
  532.             this.log = log;
  533.             this.start = start;
  534.             this.end = end;
  535.             this.bytes = bytes;
  536.         }

  537.         Segment() {
  538.             this(0, 0, 0, 0);
  539.         }

  540.         @Override
  541.         public int hashCode() {
  542.             return 0; // appease error-prone
  543.         }

  544.         @Override
  545.         public boolean equals(Object other) {
  546.             if (other == null) {
  547.                 return false;
  548.             }
  549.             Segment o = (Segment) other;
  550.             return o.bytes == bytes && o.log == log && o.start == start
  551.                     && o.end == end;
  552.         }

  553.         @SuppressWarnings("boxing")
  554.         @Override
  555.         public String toString() {
  556.             return String.format("{ [%d,%d) l=%d sz=%d }", start, end, log, //$NON-NLS-1$
  557.                     bytes);
  558.         }
  559.     }

  560.     static List<Segment> segmentSizes(long[] sizes) {
  561.         List<Segment> segments = new ArrayList<>();
  562.         Segment cur = new Segment();
  563.         for (int i = 0; i < sizes.length; i++) {
  564.             int l = log(sizes[i]);
  565.             if (l != cur.log && cur.bytes > 0) {
  566.                 segments.add(cur);
  567.                 cur = new Segment();
  568.                 cur.start = i;
  569.                 cur.log = l;
  570.             }

  571.             cur.log = l;
  572.             cur.end = i + 1;
  573.             cur.bytes += sizes[i];
  574.         }
  575.         segments.add(cur);
  576.         return segments;
  577.     }

  578.     private static Optional<Segment> autoCompactCandidate(long[] sizes) {
  579.         if (sizes.length == 0) {
  580.             return Optional.empty();
  581.         }

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

  587.         List<Segment> segments = segmentSizes(sizes);
  588.         segments = segments.stream().filter(s -> s.size() > 1)
  589.                 .collect(Collectors.toList());
  590.         if (segments.isEmpty()) {
  591.             return Optional.empty();
  592.         }

  593.         Optional<Segment> optMinSeg = segments.stream()
  594.                 .min(Comparator.comparing(s -> Integer.valueOf(s.log)));
  595.         // Input is non-empty, so always present.
  596.         Segment smallCollected = optMinSeg.get();
  597.         while (smallCollected.start > 0) {
  598.             int prev = smallCollected.start - 1;
  599.             long prevSize = sizes[prev];
  600.             if (log(smallCollected.bytes) < log(prevSize)) {
  601.                 break;
  602.             }
  603.             smallCollected.start = prev;
  604.             smallCollected.bytes += prevSize;
  605.         }

  606.         return Optional.of(smallCollected);
  607.     }

  608.     /**
  609.      * Heuristically tries to compact the stack if the stack has a suitable
  610.      * shape.
  611.      *
  612.      * @throws IOException
  613.      */
  614.     private void autoCompact() throws IOException {
  615.         Optional<Segment> cand = autoCompactCandidate(tableSizes());
  616.         if (cand.isPresent()) {
  617.             if (!compactRange(cand.get().start, cand.get().end - 1)) {
  618.                 stats.failed++;
  619.             }
  620.         }
  621.     }

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

  624.     private long[] tableSizes() throws IOException {
  625.         long[] sizes = new long[stack.size()];
  626.         for (int i = 0; i < stack.size(); i++) {
  627.             // If we don't subtract the overhead, the file size isn't
  628.             // proportional to the number of entries. This will cause us to
  629.             // compact too often, which is expensive.
  630.             sizes[i] = stack.get(i).reftableReader.size() - OVERHEAD;
  631.         }
  632.         return sizes;
  633.     }

  634.     void compactFully() throws IOException {
  635.         if (!compactRange(0, stack.size() - 1)) {
  636.             stats.failed++;
  637.         }
  638.     }
  639. }