PackedBatchRefUpdate.java

  1. /*
  2.  * Copyright (C) 2017, Google Inc. 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.util.stream.Collectors.toList;
  12. import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
  13. import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
  14. import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
  15. import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;

  16. import java.io.IOException;
  17. import java.text.MessageFormat;
  18. import java.util.Collections;
  19. import java.util.Comparator;
  20. import java.util.HashMap;
  21. import java.util.HashSet;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Set;

  25. import org.eclipse.jgit.annotations.Nullable;
  26. import org.eclipse.jgit.errors.LockFailedException;
  27. import org.eclipse.jgit.errors.MissingObjectException;
  28. import org.eclipse.jgit.internal.JGitText;
  29. import org.eclipse.jgit.internal.storage.file.RefDirectory.PackedRefList;
  30. import org.eclipse.jgit.lib.BatchRefUpdate;
  31. import org.eclipse.jgit.lib.ObjectId;
  32. import org.eclipse.jgit.lib.ObjectIdRef;
  33. import org.eclipse.jgit.lib.PersonIdent;
  34. import org.eclipse.jgit.lib.ProgressMonitor;
  35. import org.eclipse.jgit.lib.Ref;
  36. import org.eclipse.jgit.lib.RefDatabase;
  37. import org.eclipse.jgit.lib.ReflogEntry;
  38. import org.eclipse.jgit.revwalk.RevObject;
  39. import org.eclipse.jgit.revwalk.RevTag;
  40. import org.eclipse.jgit.revwalk.RevWalk;
  41. import org.eclipse.jgit.transport.ReceiveCommand;
  42. import org.eclipse.jgit.util.RefList;

  43. /**
  44.  * Implementation of {@link BatchRefUpdate} that uses the {@code packed-refs}
  45.  * file to support atomically updating multiple refs.
  46.  * <p>
  47.  * The algorithm is designed to be compatible with traditional single ref
  48.  * updates operating on single refs only. Regardless of success or failure, the
  49.  * results are atomic: from the perspective of any reader, either all updates in
  50.  * the batch will be visible, or none will. In the case of process failure
  51.  * during any of the following steps, removal of stale lock files is always
  52.  * safe, and will never result in an inconsistent state, although the update may
  53.  * or may not have been applied.
  54.  * <p>
  55.  * The algorithm is:
  56.  * <ol>
  57.  * <li>Pack loose refs involved in the transaction using the normal pack-refs
  58.  * operation. This ensures that creating lock files in the following step
  59.  * succeeds even if a batch contains both a delete of {@code refs/x} (loose) and
  60.  * a create of {@code refs/x/y}.</li>
  61.  * <li>Create locks for all loose refs involved in the transaction, even if they
  62.  * are not currently loose.</li>
  63.  * <li>Pack loose refs again, this time while holding all lock files (see {@link
  64.  * RefDirectory#pack(Map)}), without deleting them afterwards. This covers a
  65.  * potential race where new loose refs were created after the initial packing
  66.  * step. If no new loose refs were created during this race, this step does not
  67.  * modify any files on disk. Keep the merged state in memory.</li>
  68.  * <li>Update the in-memory packed refs with the commands in the batch, possibly
  69.  * failing the whole batch if any old ref values do not match.</li>
  70.  * <li>If the update succeeds, lock {@code packed-refs} and commit by atomically
  71.  * renaming the lock file.</li>
  72.  * <li>Delete loose ref lock files.</li>
  73.  * </ol>
  74.  *
  75.  * Because the packed-refs file format is a sorted list, this algorithm is
  76.  * linear in the total number of refs, regardless of the batch size. This can be
  77.  * a significant slowdown on repositories with large numbers of refs; callers
  78.  * that prefer speed over atomicity should use {@code setAtomic(false)}. As an
  79.  * optimization, an update containing a single ref update does not use the
  80.  * packed-refs protocol.
  81.  */
  82. class PackedBatchRefUpdate extends BatchRefUpdate {
  83.     private RefDirectory refdb;
  84.     private boolean shouldLockLooseRefs;

  85.     PackedBatchRefUpdate(RefDirectory refdb) {
  86.         this(refdb, true);
  87.     }

  88.     PackedBatchRefUpdate(RefDirectory refdb, boolean shouldLockLooseRefs) {
  89.       super(refdb);
  90.       this.refdb = refdb;
  91.       this.shouldLockLooseRefs = shouldLockLooseRefs;
  92.     }

  93.     /** {@inheritDoc} */
  94.     @Override
  95.     public void execute(RevWalk walk, ProgressMonitor monitor,
  96.             List<String> options) throws IOException {
  97.         if (!isAtomic()) {
  98.             // Use default one-by-one implementation.
  99.             super.execute(walk, monitor, options);
  100.             return;
  101.         }
  102.         List<ReceiveCommand> pending =
  103.                 ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED);
  104.         if (pending.isEmpty()) {
  105.             return;
  106.         }
  107.         if (pending.size() == 1) {
  108.             // Single-ref updates are always atomic, no need for packed-refs.
  109.             super.execute(walk, monitor, options);
  110.             return;
  111.         }
  112.         if (containsSymrefs(pending)) {
  113.             // packed-refs file cannot store symrefs
  114.             reject(pending.get(0), REJECTED_OTHER_REASON,
  115.                     JGitText.get().atomicSymRefNotSupported, pending);
  116.             return;
  117.         }

  118.         // Required implementation details copied from super.execute.
  119.         if (!blockUntilTimestamps(MAX_WAIT)) {
  120.             return;
  121.         }
  122.         if (options != null) {
  123.             setPushOptions(options);
  124.         }
  125.         // End required implementation details.

  126.         // Check for conflicting names before attempting to acquire locks, since
  127.         // lockfile creation may fail on file/directory conflicts.
  128.         if (!checkConflictingNames(pending)) {
  129.             return;
  130.         }

  131.         if (!checkObjectExistence(walk, pending)) {
  132.             return;
  133.         }

  134.         if (!checkNonFastForwards(walk, pending)) {
  135.             return;
  136.         }

  137.         // Pack refs normally, so we can create lock files even in the case where
  138.         // refs/x is deleted and refs/x/y is created in this batch.
  139.         try {
  140.             refdb.pack(
  141.                     pending.stream().map(ReceiveCommand::getRefName).collect(toList()));
  142.         } catch (LockFailedException e) {
  143.             lockFailure(pending.get(0), pending);
  144.             return;
  145.         }

  146.         Map<String, LockFile> locks = null;
  147.         refdb.inProcessPackedRefsLock.lock();
  148.         try {
  149.             PackedRefList oldPackedList;
  150.             if (!refdb.isInClone() && shouldLockLooseRefs) {
  151.                 locks = lockLooseRefs(pending);
  152.                 if (locks == null) {
  153.                     return;
  154.                 }
  155.                 oldPackedList = refdb.pack(locks);
  156.             } else {
  157.                 // During clone locking isn't needed since no refs exist yet.
  158.                 // This also helps to avoid problems with refs only differing in
  159.                 // case on a case insensitive filesystem (bug 528497)
  160.                 oldPackedList = refdb.getPackedRefs();
  161.             }
  162.             RefList<Ref> newRefs = applyUpdates(walk, oldPackedList, pending);
  163.             if (newRefs == null) {
  164.                 return;
  165.             }
  166.             LockFile packedRefsLock = refdb.lockPackedRefs();
  167.             if (packedRefsLock == null) {
  168.                 lockFailure(pending.get(0), pending);
  169.                 return;
  170.             }
  171.             // commitPackedRefs removes lock file (by renaming over real file).
  172.             refdb.commitPackedRefs(packedRefsLock, newRefs, oldPackedList,
  173.                     true);
  174.         } finally {
  175.             try {
  176.                 unlockAll(locks);
  177.             } finally {
  178.                 refdb.inProcessPackedRefsLock.unlock();
  179.             }
  180.         }

  181.         refdb.fireRefsChanged();
  182.         pending.forEach(c -> c.setResult(ReceiveCommand.Result.OK));
  183.         writeReflog(pending);
  184.     }

  185.     private static boolean containsSymrefs(List<ReceiveCommand> commands) {
  186.         for (ReceiveCommand cmd : commands) {
  187.             if (cmd.getOldSymref() != null || cmd.getNewSymref() != null) {
  188.                 return true;
  189.             }
  190.         }
  191.         return false;
  192.     }

  193.     private boolean checkConflictingNames(List<ReceiveCommand> commands)
  194.             throws IOException {
  195.         Set<String> takenNames = new HashSet<>();
  196.         Set<String> takenPrefixes = new HashSet<>();
  197.         Set<String> deletes = new HashSet<>();
  198.         for (ReceiveCommand cmd : commands) {
  199.             if (cmd.getType() != ReceiveCommand.Type.DELETE) {
  200.                 takenNames.add(cmd.getRefName());
  201.                 addPrefixesTo(cmd.getRefName(), takenPrefixes);
  202.             } else {
  203.                 deletes.add(cmd.getRefName());
  204.             }
  205.         }
  206.         Set<String> initialRefs = refdb.getRefs(RefDatabase.ALL).keySet();
  207.         for (String name : initialRefs) {
  208.             if (!deletes.contains(name)) {
  209.                 takenNames.add(name);
  210.                 addPrefixesTo(name, takenPrefixes);
  211.             }
  212.         }

  213.         for (ReceiveCommand cmd : commands) {
  214.             if (cmd.getType() != ReceiveCommand.Type.DELETE &&
  215.                     takenPrefixes.contains(cmd.getRefName())) {
  216.                 // This ref is a prefix of some other ref. This check doesn't apply when
  217.                 // this command is a delete, because if the ref is deleted nobody will
  218.                 // ever be creating a loose ref with that name.
  219.                 lockFailure(cmd, commands);
  220.                 return false;
  221.             }
  222.             for (String prefix : getPrefixes(cmd.getRefName())) {
  223.                 if (takenNames.contains(prefix)) {
  224.                     // A prefix of this ref is already a refname. This check does apply
  225.                     // when this command is a delete, because we would need to create the
  226.                     // refname as a directory in order to create a lockfile for the
  227.                     // to-be-deleted ref.
  228.                     lockFailure(cmd, commands);
  229.                     return false;
  230.                 }
  231.             }
  232.         }
  233.         return true;
  234.     }

  235.     private boolean checkObjectExistence(RevWalk walk,
  236.             List<ReceiveCommand> commands) throws IOException {
  237.         for (ReceiveCommand cmd : commands) {
  238.             try {
  239.                 if (!cmd.getNewId().equals(ObjectId.zeroId())) {
  240.                     walk.parseAny(cmd.getNewId());
  241.                 }
  242.             } catch (MissingObjectException e) {
  243.                 // ReceiveCommand#setResult(Result) converts REJECTED to
  244.                 // REJECTED_NONFASTFORWARD, even though that result is also used for a
  245.                 // missing object. Eagerly handle this case so we can set the right
  246.                 // result.
  247.                 reject(cmd, ReceiveCommand.Result.REJECTED_MISSING_OBJECT, commands);
  248.                 return false;
  249.             }
  250.         }
  251.         return true;
  252.     }

  253.     private boolean checkNonFastForwards(RevWalk walk,
  254.             List<ReceiveCommand> commands) throws IOException {
  255.         if (isAllowNonFastForwards()) {
  256.             return true;
  257.         }
  258.         for (ReceiveCommand cmd : commands) {
  259.             cmd.updateType(walk);
  260.             if (cmd.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) {
  261.                 reject(cmd, REJECTED_NONFASTFORWARD, commands);
  262.                 return false;
  263.             }
  264.         }
  265.         return true;
  266.     }

  267.     /**
  268.      * Lock loose refs corresponding to a list of commands.
  269.      *
  270.      * @param commands
  271.      *            commands that we intend to execute.
  272.      * @return map of ref name in the input commands to lock file. Always contains
  273.      *         one entry for each ref in the input list. All locks are acquired
  274.      *         before returning. If any lock was not able to be acquired: the
  275.      *         return value is null; no locks are held; and all commands that were
  276.      *         pending are set to fail with {@code LOCK_FAILURE}.
  277.      * @throws IOException
  278.      *             an error occurred other than a failure to acquire; no locks are
  279.      *             held if this exception is thrown.
  280.      */
  281.     @Nullable
  282.     private Map<String, LockFile> lockLooseRefs(List<ReceiveCommand> commands)
  283.             throws IOException {
  284.         ReceiveCommand failed = null;
  285.         Map<String, LockFile> locks = new HashMap<>();
  286.         try {
  287.             RETRY: for (int ms : refdb.getRetrySleepMs()) {
  288.                 failed = null;
  289.                 // Release all locks before trying again, to prevent deadlock.
  290.                 unlockAll(locks);
  291.                 locks.clear();
  292.                 RefDirectory.sleep(ms);

  293.                 for (ReceiveCommand c : commands) {
  294.                     String name = c.getRefName();
  295.                     LockFile lock = new LockFile(refdb.fileFor(name));
  296.                     if (locks.put(name, lock) != null) {
  297.                         throw new IOException(
  298.                                 MessageFormat.format(JGitText.get().duplicateRef, name));
  299.                     }
  300.                     if (!lock.lock()) {
  301.                         failed = c;
  302.                         continue RETRY;
  303.                     }
  304.                 }
  305.                 Map<String, LockFile> result = locks;
  306.                 locks = null;
  307.                 return result;
  308.             }
  309.         } finally {
  310.             unlockAll(locks);
  311.         }
  312.         lockFailure(failed != null ? failed : commands.get(0), commands);
  313.         return null;
  314.     }

  315.     private static RefList<Ref> applyUpdates(RevWalk walk, RefList<Ref> refs,
  316.             List<ReceiveCommand> commands) throws IOException {
  317.         // Construct a new RefList by merging the old list with the updates.
  318.         // This assumes that each ref occurs at most once as a ReceiveCommand.
  319.         Collections.sort(commands,
  320.                 Comparator.comparing(ReceiveCommand::getRefName));

  321.         int delta = 0;
  322.         for (ReceiveCommand c : commands) {
  323.             switch (c.getType()) {
  324.             case DELETE:
  325.                 delta--;
  326.                 break;
  327.             case CREATE:
  328.                 delta++;
  329.                 break;
  330.             default:
  331.             }
  332.         }

  333.         RefList.Builder<Ref> b = new RefList.Builder<>(refs.size() + delta);
  334.         int refIdx = 0;
  335.         int cmdIdx = 0;
  336.         while (refIdx < refs.size() || cmdIdx < commands.size()) {
  337.             Ref ref = (refIdx < refs.size()) ? refs.get(refIdx) : null;
  338.             ReceiveCommand cmd = (cmdIdx < commands.size())
  339.                     ? commands.get(cmdIdx)
  340.                     : null;
  341.             int cmp = 0;
  342.             if (ref != null && cmd != null) {
  343.                 cmp = ref.getName().compareTo(cmd.getRefName());
  344.             } else if (ref == null) {
  345.                 cmp = 1;
  346.             } else if (cmd == null) {
  347.                 cmp = -1;
  348.             }

  349.             if (cmp < 0) {
  350.                 b.add(ref);
  351.                 refIdx++;
  352.             } else if (cmp > 0) {
  353.                 assert cmd != null;
  354.                 if (cmd.getType() != ReceiveCommand.Type.CREATE) {
  355.                     lockFailure(cmd, commands);
  356.                     return null;
  357.                 }

  358.                 b.add(peeledRef(walk, cmd));
  359.                 cmdIdx++;
  360.             } else {
  361.                 assert cmd != null;
  362.                 assert ref != null;
  363.                 if (!cmd.getOldId().equals(ref.getObjectId())) {
  364.                     lockFailure(cmd, commands);
  365.                     return null;
  366.                 }

  367.                 if (cmd.getType() != ReceiveCommand.Type.DELETE) {
  368.                     b.add(peeledRef(walk, cmd));
  369.                 }
  370.                 cmdIdx++;
  371.                 refIdx++;
  372.             }
  373.         }
  374.         return b.toRefList();
  375.     }

  376.     private void writeReflog(List<ReceiveCommand> commands) {
  377.         PersonIdent ident = getRefLogIdent();
  378.         if (ident == null) {
  379.             ident = new PersonIdent(refdb.getRepository());
  380.         }
  381.         for (ReceiveCommand cmd : commands) {
  382.             // Assume any pending commands have already been executed atomically.
  383.             if (cmd.getResult() != ReceiveCommand.Result.OK) {
  384.                 continue;
  385.             }
  386.             String name = cmd.getRefName();

  387.             if (cmd.getType() == ReceiveCommand.Type.DELETE) {
  388.                 try {
  389.                     RefDirectory.delete(refdb.logFor(name), RefDirectory.levelsIn(name));
  390.                 } catch (IOException e) {
  391.                     // Ignore failures, see below.
  392.                 }
  393.                 continue;
  394.             }

  395.             if (isRefLogDisabled(cmd)) {
  396.                 continue;
  397.             }

  398.             String msg = getRefLogMessage(cmd);
  399.             if (isRefLogIncludingResult(cmd)) {
  400.                 String strResult = toResultString(cmd);
  401.                 if (strResult != null) {
  402.                     msg = msg.isEmpty()
  403.                             ? strResult : msg + ": " + strResult; //$NON-NLS-1$
  404.                 }
  405.             }
  406.             try {
  407.                 new ReflogWriter(refdb, isForceRefLog(cmd))
  408.                         .log(name, cmd.getOldId(), cmd.getNewId(), ident, msg);
  409.             } catch (IOException e) {
  410.                 // Ignore failures, but continue attempting to write more reflogs.
  411.                 //
  412.                 // In this storage format, it is impossible to atomically write the
  413.                 // reflog with the ref updates, so we have to choose between:
  414.                 // a. Propagating this exception and claiming failure, even though the
  415.                 //    actual ref updates succeeded.
  416.                 // b. Ignoring failures writing the reflog, so we claim success if and
  417.                 //    only if the ref updates succeeded.
  418.                 // We choose (b) in order to surprise callers the least.
  419.                 //
  420.                 // Possible future improvements:
  421.                 // * Log a warning to a logger.
  422.                 // * Retry a fixed number of times in case the error was transient.
  423.             }
  424.         }
  425.     }

  426.     private String toResultString(ReceiveCommand cmd) {
  427.         switch (cmd.getType()) {
  428.         case CREATE:
  429.             return ReflogEntry.PREFIX_CREATED;
  430.         case UPDATE:
  431.             // Match the behavior of a single RefUpdate. In that case, setting the
  432.             // force bit completely bypasses the potentially expensive isMergedInto
  433.             // check, by design, so the reflog message may be inaccurate.
  434.             //
  435.             // Similarly, this class bypasses the isMergedInto checks when the force
  436.             // bit is set, meaning we can't actually distinguish between UPDATE and
  437.             // UPDATE_NONFASTFORWARD when isAllowNonFastForwards() returns true.
  438.             return isAllowNonFastForwards()
  439.                     ? ReflogEntry.PREFIX_FORCED_UPDATE : ReflogEntry.PREFIX_FAST_FORWARD;
  440.         case UPDATE_NONFASTFORWARD:
  441.             return ReflogEntry.PREFIX_FORCED_UPDATE;
  442.         default:
  443.             return null;
  444.         }
  445.     }

  446.     private static Ref peeledRef(RevWalk walk, ReceiveCommand cmd)
  447.             throws IOException {
  448.         ObjectId newId = cmd.getNewId().copy();
  449.         RevObject obj = walk.parseAny(newId);
  450.         if (obj instanceof RevTag) {
  451.             return new ObjectIdRef.PeeledTag(
  452.                     Ref.Storage.PACKED, cmd.getRefName(), newId, walk.peel(obj).copy());
  453.         }
  454.         return new ObjectIdRef.PeeledNonTag(
  455.                 Ref.Storage.PACKED, cmd.getRefName(), newId);
  456.     }

  457.     private static void unlockAll(@Nullable Map<?, LockFile> locks) {
  458.         if (locks != null) {
  459.             locks.values().forEach(LockFile::unlock);
  460.         }
  461.     }

  462.     private static void lockFailure(ReceiveCommand cmd,
  463.             List<ReceiveCommand> commands) {
  464.         reject(cmd, LOCK_FAILURE, commands);
  465.     }

  466.     private static void reject(ReceiveCommand cmd, ReceiveCommand.Result result,
  467.             List<ReceiveCommand> commands) {
  468.         reject(cmd, result, null, commands);
  469.     }

  470.     private static void reject(ReceiveCommand cmd, ReceiveCommand.Result result,
  471.             String why, List<ReceiveCommand> commands) {
  472.         cmd.setResult(result, why);
  473.         for (ReceiveCommand c2 : commands) {
  474.             if (c2.getResult() == ReceiveCommand.Result.OK) {
  475.                 // Undo OK status so ReceiveCommand#abort aborts it. Assumes this method
  476.                 // is always called before committing any updates to disk.
  477.                 c2.setResult(ReceiveCommand.Result.NOT_ATTEMPTED);
  478.             }
  479.         }
  480.         ReceiveCommand.abort(commands);
  481.     }
  482. }