ReftableBatchRefUpdate.java

  1. /*
  2.  * Copyright (C) 2017, Google Inc.
  3.  * and other copyright owners as documented in the project's IP log.
  4.  *
  5.  * This program and the accompanying materials are made available
  6.  * under the terms of the Eclipse Distribution License v1.0 which
  7.  * accompanies this distribution, is reproduced below, and is
  8.  * available at http://www.eclipse.org/org/documents/edl-v10.php
  9.  *
  10.  * All rights reserved.
  11.  *
  12.  * Redistribution and use in source and binary forms, with or
  13.  * without modification, are permitted provided that the following
  14.  * conditions are met:
  15.  *
  16.  * - Redistributions of source code must retain the above copyright
  17.  *   notice, this list of conditions and the following disclaimer.
  18.  *
  19.  * - Redistributions in binary form must reproduce the above
  20.  *   copyright notice, this list of conditions and the following
  21.  *   disclaimer in the documentation and/or other materials provided
  22.  *   with the distribution.
  23.  *
  24.  * - Neither the name of the Eclipse Foundation, Inc. nor the
  25.  *   names of its contributors may be used to endorse or promote
  26.  *   products derived from this software without specific prior
  27.  *   written permission.
  28.  *
  29.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30.  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31.  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32.  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34.  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35.  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36.  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38.  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39.  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41.  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42.  */

  43. package org.eclipse.jgit.internal.storage.dfs;

  44. import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
  45. import static org.eclipse.jgit.lib.Ref.Storage.NEW;
  46. import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
  47. import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
  48. import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
  49. import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
  50. import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
  51. import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
  52. import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;

  53. import java.io.ByteArrayOutputStream;
  54. import java.io.IOException;
  55. import java.io.OutputStream;
  56. import java.util.ArrayList;
  57. import java.util.Collections;
  58. import java.util.HashMap;
  59. import java.util.HashSet;
  60. import java.util.List;
  61. import java.util.Map;
  62. import java.util.Set;
  63. import java.util.concurrent.locks.ReentrantLock;

  64. import org.eclipse.jgit.annotations.Nullable;
  65. import org.eclipse.jgit.errors.MissingObjectException;
  66. import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
  67. import org.eclipse.jgit.internal.storage.io.BlockSource;
  68. import org.eclipse.jgit.internal.storage.pack.PackExt;
  69. import org.eclipse.jgit.internal.storage.reftable.Reftable;
  70. import org.eclipse.jgit.internal.storage.reftable.ReftableCompactor;
  71. import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
  72. import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
  73. import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
  74. import org.eclipse.jgit.lib.AnyObjectId;
  75. import org.eclipse.jgit.lib.BatchRefUpdate;
  76. import org.eclipse.jgit.lib.ObjectId;
  77. import org.eclipse.jgit.lib.ObjectIdRef;
  78. import org.eclipse.jgit.lib.PersonIdent;
  79. import org.eclipse.jgit.lib.ProgressMonitor;
  80. import org.eclipse.jgit.lib.Ref;
  81. import org.eclipse.jgit.lib.ReflogEntry;
  82. import org.eclipse.jgit.lib.SymbolicRef;
  83. import org.eclipse.jgit.revwalk.RevObject;
  84. import org.eclipse.jgit.revwalk.RevTag;
  85. import org.eclipse.jgit.revwalk.RevWalk;
  86. import org.eclipse.jgit.transport.ReceiveCommand;

  87. /**
  88.  * {@link org.eclipse.jgit.lib.BatchRefUpdate} for
  89.  * {@link org.eclipse.jgit.internal.storage.dfs.DfsReftableDatabase}.
  90.  */
  91. public class ReftableBatchRefUpdate extends BatchRefUpdate {
  92.     private static final int AVG_BYTES = 36;

  93.     private final DfsReftableDatabase refdb;

  94.     private final DfsObjDatabase odb;

  95.     private final ReentrantLock lock;

  96.     private final ReftableConfig reftableConfig;

  97.     /**
  98.      * Initialize batch update.
  99.      *
  100.      * @param refdb
  101.      *            database the update will modify.
  102.      * @param odb
  103.      *            object database to store the reftable.
  104.      */
  105.     protected ReftableBatchRefUpdate(DfsReftableDatabase refdb,
  106.             DfsObjDatabase odb) {
  107.         super(refdb);
  108.         this.refdb = refdb;
  109.         this.odb = odb;
  110.         lock = refdb.getLock();
  111.         reftableConfig = refdb.getReftableConfig();
  112.     }

  113.     /** {@inheritDoc} */
  114.     @Override
  115.     public void execute(RevWalk rw, ProgressMonitor pm, List<String> options) {
  116.         List<ReceiveCommand> pending = getPending();
  117.         if (pending.isEmpty()) {
  118.             return;
  119.         }
  120.         if (options != null) {
  121.             setPushOptions(options);
  122.         }
  123.         try {
  124.             if (!checkObjectExistence(rw, pending)) {
  125.                 return;
  126.             }
  127.             if (!checkNonFastForwards(rw, pending)) {
  128.                 return;
  129.             }

  130.             lock.lock();
  131.             try {
  132.                 Reftable table = refdb.reader();
  133.                 if (!checkExpected(table, pending)) {
  134.                     return;
  135.                 }
  136.                 if (!checkConflicting(pending)) {
  137.                     return;
  138.                 }
  139.                 if (!blockUntilTimestamps(MAX_WAIT)) {
  140.                     return;
  141.                 }
  142.                 applyUpdates(rw, pending);
  143.                 for (ReceiveCommand cmd : pending) {
  144.                     cmd.setResult(OK);
  145.                 }
  146.             } finally {
  147.                 lock.unlock();
  148.             }
  149.         } catch (IOException e) {
  150.             pending.get(0).setResult(LOCK_FAILURE, "io error"); //$NON-NLS-1$
  151.             ReceiveCommand.abort(pending);
  152.         }
  153.     }

  154.     private List<ReceiveCommand> getPending() {
  155.         return ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED);
  156.     }

  157.     private boolean checkObjectExistence(RevWalk rw,
  158.             List<ReceiveCommand> pending) throws IOException {
  159.         for (ReceiveCommand cmd : pending) {
  160.             try {
  161.                 if (!cmd.getNewId().equals(ObjectId.zeroId())) {
  162.                     rw.parseAny(cmd.getNewId());
  163.                 }
  164.             } catch (MissingObjectException e) {
  165.                 // ReceiveCommand#setResult(Result) converts REJECTED to
  166.                 // REJECTED_NONFASTFORWARD, even though that result is also
  167.                 // used for a missing object. Eagerly handle this case so we
  168.                 // can set the right result.
  169.                 cmd.setResult(REJECTED_MISSING_OBJECT);
  170.                 ReceiveCommand.abort(pending);
  171.                 return false;
  172.             }
  173.         }
  174.         return true;
  175.     }

  176.     private boolean checkNonFastForwards(RevWalk rw,
  177.             List<ReceiveCommand> pending) throws IOException {
  178.         if (isAllowNonFastForwards()) {
  179.             return true;
  180.         }
  181.         for (ReceiveCommand cmd : pending) {
  182.             cmd.updateType(rw);
  183.             if (cmd.getType() == UPDATE_NONFASTFORWARD) {
  184.                 cmd.setResult(REJECTED_NONFASTFORWARD);
  185.                 ReceiveCommand.abort(pending);
  186.                 return false;
  187.             }
  188.         }
  189.         return true;
  190.     }

  191.     private boolean checkConflicting(List<ReceiveCommand> pending)
  192.             throws IOException {
  193.         Set<String> names = new HashSet<>();
  194.         for (ReceiveCommand cmd : pending) {
  195.             names.add(cmd.getRefName());
  196.         }

  197.         boolean ok = true;
  198.         for (ReceiveCommand cmd : pending) {
  199.             String name = cmd.getRefName();
  200.             if (refdb.isNameConflicting(name)) {
  201.                 cmd.setResult(LOCK_FAILURE);
  202.                 ok = false;
  203.             } else {
  204.                 int s = name.lastIndexOf('/');
  205.                 while (0 < s) {
  206.                     if (names.contains(name.substring(0, s))) {
  207.                         cmd.setResult(LOCK_FAILURE);
  208.                         ok = false;
  209.                         break;
  210.                     }
  211.                     s = name.lastIndexOf('/', s - 1);
  212.                 }
  213.             }
  214.         }
  215.         if (!ok && isAtomic()) {
  216.             ReceiveCommand.abort(pending);
  217.             return false;
  218.         }
  219.         return ok;
  220.     }

  221.     private boolean checkExpected(Reftable table, List<ReceiveCommand> pending)
  222.             throws IOException {
  223.         for (ReceiveCommand cmd : pending) {
  224.             if (!matchOld(cmd, table.exactRef(cmd.getRefName()))) {
  225.                 cmd.setResult(LOCK_FAILURE);
  226.                 if (isAtomic()) {
  227.                     ReceiveCommand.abort(pending);
  228.                     return false;
  229.                 }
  230.             }
  231.         }
  232.         return true;
  233.     }

  234.     private static boolean matchOld(ReceiveCommand cmd, @Nullable Ref ref) {
  235.         if (ref == null) {
  236.             return AnyObjectId.isEqual(ObjectId.zeroId(), cmd.getOldId())
  237.                     && cmd.getOldSymref() == null;
  238.         } else if (ref.isSymbolic()) {
  239.             return ref.getTarget().getName().equals(cmd.getOldSymref());
  240.         }
  241.         ObjectId id = ref.getObjectId();
  242.         if (id == null) {
  243.             id = ObjectId.zeroId();
  244.         }
  245.         return cmd.getOldId().equals(id);
  246.     }

  247.     private void applyUpdates(RevWalk rw, List<ReceiveCommand> pending)
  248.             throws IOException {
  249.         List<Ref> newRefs = toNewRefs(rw, pending);
  250.         long updateIndex = nextUpdateIndex();
  251.         Set<DfsPackDescription> prune = Collections.emptySet();
  252.         DfsPackDescription pack = odb.newPack(PackSource.INSERT);
  253.         try (DfsOutputStream out = odb.writeFile(pack, REFTABLE)) {
  254.             ReftableConfig cfg = DfsPackCompactor
  255.                     .configureReftable(reftableConfig, out);

  256.             ReftableWriter.Stats stats;
  257.             if (refdb.compactDuringCommit()
  258.                     && newRefs.size() * AVG_BYTES <= cfg.getRefBlockSize()
  259.                     && canCompactTopOfStack(cfg)) {
  260.                 ByteArrayOutputStream tmp = new ByteArrayOutputStream();
  261.                 write(tmp, cfg, updateIndex, newRefs, pending);
  262.                 stats = compactTopOfStack(out, cfg, tmp.toByteArray());
  263.                 prune = toPruneTopOfStack();
  264.             } else {
  265.                 stats = write(out, cfg, updateIndex, newRefs, pending);
  266.             }
  267.             pack.addFileExt(REFTABLE);
  268.             pack.setReftableStats(stats);
  269.         }

  270.         odb.commitPack(Collections.singleton(pack), prune);
  271.         odb.addReftable(pack, prune);
  272.         refdb.clearCache();
  273.     }

  274.     private ReftableWriter.Stats write(OutputStream os, ReftableConfig cfg,
  275.             long updateIndex, List<Ref> newRefs, List<ReceiveCommand> pending)
  276.             throws IOException {
  277.         ReftableWriter writer = new ReftableWriter(cfg)
  278.                 .setMinUpdateIndex(updateIndex).setMaxUpdateIndex(updateIndex)
  279.                 .begin(os).sortAndWriteRefs(newRefs);
  280.         if (!isRefLogDisabled()) {
  281.             writeLog(writer, updateIndex, pending);
  282.         }
  283.         writer.finish();
  284.         return writer.getStats();
  285.     }

  286.     private void writeLog(ReftableWriter writer, long updateIndex,
  287.             List<ReceiveCommand> pending) throws IOException {
  288.         Map<String, ReceiveCommand> cmds = new HashMap<>();
  289.         List<String> byName = new ArrayList<>(pending.size());
  290.         for (ReceiveCommand cmd : pending) {
  291.             cmds.put(cmd.getRefName(), cmd);
  292.             byName.add(cmd.getRefName());
  293.         }
  294.         Collections.sort(byName);

  295.         PersonIdent ident = getRefLogIdent();
  296.         if (ident == null) {
  297.             ident = new PersonIdent(refdb.getRepository());
  298.         }
  299.         for (String name : byName) {
  300.             ReceiveCommand cmd = cmds.get(name);
  301.             if (isRefLogDisabled(cmd)) {
  302.                 continue;
  303.             }
  304.             String msg = getRefLogMessage(cmd);
  305.             if (isRefLogIncludingResult(cmd)) {
  306.                 String strResult = toResultString(cmd);
  307.                 if (strResult != null) {
  308.                     msg = msg.isEmpty() ? strResult : msg + ": " + strResult; //$NON-NLS-1$
  309.                 }
  310.             }
  311.             writer.writeLog(name, updateIndex, ident, cmd.getOldId(),
  312.                     cmd.getNewId(), msg);
  313.         }
  314.     }

  315.     private String toResultString(ReceiveCommand cmd) {
  316.         switch (cmd.getType()) {
  317.         case CREATE:
  318.             return ReflogEntry.PREFIX_CREATED;
  319.         case UPDATE:
  320.             // Match the behavior of a single RefUpdate. In that case, setting
  321.             // the force bit completely bypasses the potentially expensive
  322.             // isMergedInto check, by design, so the reflog message may be
  323.             // inaccurate.
  324.             //
  325.             // Similarly, this class bypasses the isMergedInto checks when the
  326.             // force bit is set, meaning we can't actually distinguish between
  327.             // UPDATE and UPDATE_NONFASTFORWARD when isAllowNonFastForwards()
  328.             // returns true.
  329.             return isAllowNonFastForwards() ? ReflogEntry.PREFIX_FORCED_UPDATE
  330.                     : ReflogEntry.PREFIX_FAST_FORWARD;
  331.         case UPDATE_NONFASTFORWARD:
  332.             return ReflogEntry.PREFIX_FORCED_UPDATE;
  333.         default:
  334.             return null;
  335.         }
  336.     }

  337.     private static List<Ref> toNewRefs(RevWalk rw, List<ReceiveCommand> pending)
  338.             throws IOException {
  339.         List<Ref> refs = new ArrayList<>(pending.size());
  340.         for (ReceiveCommand cmd : pending) {
  341.             String name = cmd.getRefName();
  342.             ObjectId newId = cmd.getNewId();
  343.             String newSymref = cmd.getNewSymref();
  344.             if (AnyObjectId.isEqual(ObjectId.zeroId(), newId)
  345.                     && newSymref == null) {
  346.                 refs.add(new ObjectIdRef.Unpeeled(NEW, name, null));
  347.                 continue;
  348.             } else if (newSymref != null) {
  349.                 refs.add(new SymbolicRef(name,
  350.                         new ObjectIdRef.Unpeeled(NEW, newSymref, null)));
  351.                 continue;
  352.             }

  353.             RevObject obj = rw.parseAny(newId);
  354.             RevObject peel = null;
  355.             if (obj instanceof RevTag) {
  356.                 peel = rw.peel(obj);
  357.             }
  358.             if (peel != null) {
  359.                 refs.add(new ObjectIdRef.PeeledTag(PACKED, name, newId,
  360.                         peel.copy()));
  361.             } else {
  362.                 refs.add(new ObjectIdRef.PeeledNonTag(PACKED, name, newId));
  363.             }
  364.         }
  365.         return refs;
  366.     }

  367.     private long nextUpdateIndex() throws IOException {
  368.         long updateIndex = 0;
  369.         for (Reftable r : refdb.stack().readers()) {
  370.             if (r instanceof ReftableReader) {
  371.                 updateIndex = Math.max(updateIndex,
  372.                         ((ReftableReader) r).maxUpdateIndex());
  373.             }
  374.         }
  375.         return updateIndex + 1;
  376.     }

  377.     private boolean canCompactTopOfStack(ReftableConfig cfg)
  378.             throws IOException {
  379.         ReftableStack stack = refdb.stack();
  380.         List<Reftable> readers = stack.readers();
  381.         if (readers.isEmpty()) {
  382.             return false;
  383.         }

  384.         int lastIdx = readers.size() - 1;
  385.         DfsReftable last = stack.files().get(lastIdx);
  386.         DfsPackDescription desc = last.getPackDescription();
  387.         if (desc.getPackSource() != PackSource.INSERT
  388.                 || !packOnlyContainsReftable(desc)) {
  389.             return false;
  390.         }

  391.         Reftable table = readers.get(lastIdx);
  392.         int bs = cfg.getRefBlockSize();
  393.         return table instanceof ReftableReader
  394.                 && ((ReftableReader) table).size() <= 3 * bs;
  395.     }

  396.     private ReftableWriter.Stats compactTopOfStack(OutputStream out,
  397.             ReftableConfig cfg, byte[] newTable) throws IOException {
  398.         List<Reftable> stack = refdb.stack().readers();
  399.         Reftable last = stack.get(stack.size() - 1);

  400.         List<Reftable> tables = new ArrayList<>(2);
  401.         tables.add(last);
  402.         tables.add(new ReftableReader(BlockSource.from(newTable)));

  403.         ReftableCompactor compactor = new ReftableCompactor();
  404.         compactor.setConfig(cfg);
  405.         compactor.setIncludeDeletes(true);
  406.         compactor.addAll(tables);
  407.         compactor.compact(out);
  408.         return compactor.getStats();
  409.     }

  410.     private Set<DfsPackDescription> toPruneTopOfStack() throws IOException {
  411.         List<DfsReftable> stack = refdb.stack().files();
  412.         DfsReftable last = stack.get(stack.size() - 1);
  413.         return Collections.singleton(last.getPackDescription());
  414.     }

  415.     private boolean packOnlyContainsReftable(DfsPackDescription desc) {
  416.         for (PackExt ext : PackExt.values()) {
  417.             if (ext != REFTABLE && desc.hasFileExt(ext)) {
  418.                 return false;
  419.             }
  420.         }
  421.         return true;
  422.     }
  423. }