View Javadoc
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  
44  package org.eclipse.jgit.internal.storage.file;
45  
46  import static java.util.stream.Collectors.toList;
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.REJECTED_NONFASTFORWARD;
50  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
51  
52  import java.io.IOException;
53  import java.text.MessageFormat;
54  import java.util.Collections;
55  import java.util.Comparator;
56  import java.util.HashMap;
57  import java.util.HashSet;
58  import java.util.List;
59  import java.util.Map;
60  import java.util.Set;
61  
62  import org.eclipse.jgit.annotations.Nullable;
63  import org.eclipse.jgit.errors.LockFailedException;
64  import org.eclipse.jgit.errors.MissingObjectException;
65  import org.eclipse.jgit.internal.JGitText;
66  import org.eclipse.jgit.internal.storage.file.RefDirectory.PackedRefList;
67  import org.eclipse.jgit.lib.BatchRefUpdate;
68  import org.eclipse.jgit.lib.ObjectId;
69  import org.eclipse.jgit.lib.ObjectIdRef;
70  import org.eclipse.jgit.lib.PersonIdent;
71  import org.eclipse.jgit.lib.ProgressMonitor;
72  import org.eclipse.jgit.lib.Ref;
73  import org.eclipse.jgit.lib.RefDatabase;
74  import org.eclipse.jgit.lib.ReflogEntry;
75  import org.eclipse.jgit.revwalk.RevObject;
76  import org.eclipse.jgit.revwalk.RevTag;
77  import org.eclipse.jgit.revwalk.RevWalk;
78  import org.eclipse.jgit.transport.ReceiveCommand;
79  import org.eclipse.jgit.util.RefList;
80  
81  /**
82   * Implementation of {@link BatchRefUpdate} that uses the {@code packed-refs}
83   * file to support atomically updating multiple refs.
84   * <p>
85   * The algorithm is designed to be compatible with traditional single ref
86   * updates operating on single refs only. Regardless of success or failure, the
87   * results are atomic: from the perspective of any reader, either all updates in
88   * the batch will be visible, or none will. In the case of process failure
89   * during any of the following steps, removal of stale lock files is always
90   * safe, and will never result in an inconsistent state, although the update may
91   * or may not have been applied.
92   * <p>
93   * The algorithm is:
94   * <ol>
95   * <li>Pack loose refs involved in the transaction using the normal pack-refs
96   * operation. This ensures that creating lock files in the following step
97   * succeeds even if a batch contains both a delete of {@code refs/x} (loose) and
98   * a create of {@code refs/x/y}.</li>
99   * <li>Create locks for all loose refs involved in the transaction, even if they
100  * are not currently loose.</li>
101  * <li>Pack loose refs again, this time while holding all lock files (see {@link
102  * RefDirectory#pack(Map)}), without deleting them afterwards. This covers a
103  * potential race where new loose refs were created after the initial packing
104  * step. If no new loose refs were created during this race, this step does not
105  * modify any files on disk. Keep the merged state in memory.</li>
106  * <li>Update the in-memory packed refs with the commands in the batch, possibly
107  * failing the whole batch if any old ref values do not match.</li>
108  * <li>If the update succeeds, lock {@code packed-refs} and commit by atomically
109  * renaming the lock file.</li>
110  * <li>Delete loose ref lock files.</li>
111  * </ol>
112  *
113  * Because the packed-refs file format is a sorted list, this algorithm is
114  * linear in the total number of refs, regardless of the batch size. This can be
115  * a significant slowdown on repositories with large numbers of refs; callers
116  * that prefer speed over atomicity should use {@code setAtomic(false)}. As an
117  * optimization, an update containing a single ref update does not use the
118  * packed-refs protocol.
119  */
120 class PackedBatchRefUpdate extends BatchRefUpdate {
121 	private RefDirectory refdb;
122 
123 	PackedBatchRefUpdate(RefDirectory refdb) {
124 		super(refdb);
125 		this.refdb = refdb;
126 	}
127 
128 	/** {@inheritDoc} */
129 	@Override
130 	public void execute(RevWalk walk, ProgressMonitor monitor,
131 			List<String> options) throws IOException {
132 		if (!isAtomic()) {
133 			// Use default one-by-one implementation.
134 			super.execute(walk, monitor, options);
135 			return;
136 		}
137 		List<ReceiveCommand> pending =
138 				ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED);
139 		if (pending.isEmpty()) {
140 			return;
141 		}
142 		if (pending.size() == 1) {
143 			// Single-ref updates are always atomic, no need for packed-refs.
144 			super.execute(walk, monitor, options);
145 			return;
146 		}
147 		if (containsSymrefs(pending)) {
148 			// packed-refs file cannot store symrefs
149 			reject(pending.get(0), REJECTED_OTHER_REASON,
150 					JGitText.get().atomicSymRefNotSupported, pending);
151 			return;
152 		}
153 
154 		// Required implementation details copied from super.execute.
155 		if (!blockUntilTimestamps(MAX_WAIT)) {
156 			return;
157 		}
158 		if (options != null) {
159 			setPushOptions(options);
160 		}
161 		// End required implementation details.
162 
163 		// Check for conflicting names before attempting to acquire locks, since
164 		// lockfile creation may fail on file/directory conflicts.
165 		if (!checkConflictingNames(pending)) {
166 			return;
167 		}
168 
169 		if (!checkObjectExistence(walk, pending)) {
170 			return;
171 		}
172 
173 		if (!checkNonFastForwards(walk, pending)) {
174 			return;
175 		}
176 
177 		// Pack refs normally, so we can create lock files even in the case where
178 		// refs/x is deleted and refs/x/y is created in this batch.
179 		try {
180 			refdb.pack(
181 					pending.stream().map(ReceiveCommand::getRefName).collect(toList()));
182 		} catch (LockFailedException e) {
183 			lockFailure(pending.get(0), pending);
184 			return;
185 		}
186 
187 		Map<String, LockFile> locks = null;
188 		refdb.inProcessPackedRefsLock.lock();
189 		try {
190 			PackedRefList oldPackedList;
191 			if (!refdb.isInClone()) {
192 				locks = lockLooseRefs(pending);
193 				if (locks == null) {
194 					return;
195 				}
196 				oldPackedList = refdb.pack(locks);
197 			} else {
198 				// During clone locking isn't needed since no refs exist yet.
199 				// This also helps to avoid problems with refs only differing in
200 				// case on a case insensitive filesystem (bug 528497)
201 				oldPackedList = refdb.getPackedRefs();
202 			}
203 			RefList<Ref> newRefs = applyUpdates(walk, oldPackedList, pending);
204 			if (newRefs == null) {
205 				return;
206 			}
207 			LockFile packedRefsLock = refdb.lockPackedRefs();
208 			if (packedRefsLock == null) {
209 				lockFailure(pending.get(0), pending);
210 				return;
211 			}
212 			// commitPackedRefs removes lock file (by renaming over real file).
213 			refdb.commitPackedRefs(packedRefsLock, newRefs, oldPackedList,
214 					true);
215 		} finally {
216 			try {
217 				unlockAll(locks);
218 			} finally {
219 				refdb.inProcessPackedRefsLock.unlock();
220 			}
221 		}
222 
223 		refdb.fireRefsChanged();
224 		pending.forEach(c -> c.setResult(ReceiveCommand.Result.OK));
225 		writeReflog(pending);
226 	}
227 
228 	private static boolean containsSymrefs(List<ReceiveCommand> commands) {
229 		for (ReceiveCommand cmd : commands) {
230 			if (cmd.getOldSymref() != null || cmd.getNewSymref() != null) {
231 				return true;
232 			}
233 		}
234 		return false;
235 	}
236 
237 	private boolean checkConflictingNames(List<ReceiveCommand> commands)
238 			throws IOException {
239 		Set<String> takenNames = new HashSet<>();
240 		Set<String> takenPrefixes = new HashSet<>();
241 		Set<String> deletes = new HashSet<>();
242 		for (ReceiveCommand cmd : commands) {
243 			if (cmd.getType() != ReceiveCommand.Type.DELETE) {
244 				takenNames.add(cmd.getRefName());
245 				addPrefixesTo(cmd.getRefName(), takenPrefixes);
246 			} else {
247 				deletes.add(cmd.getRefName());
248 			}
249 		}
250 		Set<String> initialRefs = refdb.getRefs(RefDatabase.ALL).keySet();
251 		for (String name : initialRefs) {
252 			if (!deletes.contains(name)) {
253 				takenNames.add(name);
254 				addPrefixesTo(name, takenPrefixes);
255 			}
256 		}
257 
258 		for (ReceiveCommand cmd : commands) {
259 			if (cmd.getType() != ReceiveCommand.Type.DELETE &&
260 					takenPrefixes.contains(cmd.getRefName())) {
261 				// This ref is a prefix of some other ref. This check doesn't apply when
262 				// this command is a delete, because if the ref is deleted nobody will
263 				// ever be creating a loose ref with that name.
264 				lockFailure(cmd, commands);
265 				return false;
266 			}
267 			for (String prefix : getPrefixes(cmd.getRefName())) {
268 				if (takenNames.contains(prefix)) {
269 					// A prefix of this ref is already a refname. This check does apply
270 					// when this command is a delete, because we would need to create the
271 					// refname as a directory in order to create a lockfile for the
272 					// to-be-deleted ref.
273 					lockFailure(cmd, commands);
274 					return false;
275 				}
276 			}
277 		}
278 		return true;
279 	}
280 
281 	private boolean checkObjectExistence(RevWalk walk,
282 			List<ReceiveCommand> commands) throws IOException {
283 		for (ReceiveCommand cmd : commands) {
284 			try {
285 				if (!cmd.getNewId().equals(ObjectId.zeroId())) {
286 					walk.parseAny(cmd.getNewId());
287 				}
288 			} catch (MissingObjectException e) {
289 				// ReceiveCommand#setResult(Result) converts REJECTED to
290 				// REJECTED_NONFASTFORWARD, even though that result is also used for a
291 				// missing object. Eagerly handle this case so we can set the right
292 				// result.
293 				reject(cmd, ReceiveCommand.Result.REJECTED_MISSING_OBJECT, commands);
294 				return false;
295 			}
296 		}
297 		return true;
298 	}
299 
300 	private boolean checkNonFastForwards(RevWalk walk,
301 			List<ReceiveCommand> commands) throws IOException {
302 		if (isAllowNonFastForwards()) {
303 			return true;
304 		}
305 		for (ReceiveCommand cmd : commands) {
306 			cmd.updateType(walk);
307 			if (cmd.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) {
308 				reject(cmd, REJECTED_NONFASTFORWARD, commands);
309 				return false;
310 			}
311 		}
312 		return true;
313 	}
314 
315 	/**
316 	 * Lock loose refs corresponding to a list of commands.
317 	 *
318 	 * @param commands
319 	 *            commands that we intend to execute.
320 	 * @return map of ref name in the input commands to lock file. Always contains
321 	 *         one entry for each ref in the input list. All locks are acquired
322 	 *         before returning. If any lock was not able to be acquired: the
323 	 *         return value is null; no locks are held; and all commands that were
324 	 *         pending are set to fail with {@code LOCK_FAILURE}.
325 	 * @throws IOException
326 	 *             an error occurred other than a failure to acquire; no locks are
327 	 *             held if this exception is thrown.
328 	 */
329 	@Nullable
330 	private Map<String, LockFile> lockLooseRefs(List<ReceiveCommand> commands)
331 			throws IOException {
332 		ReceiveCommand failed = null;
333 		Map<String, LockFile> locks = new HashMap<>();
334 		try {
335 			RETRY: for (int ms : refdb.getRetrySleepMs()) {
336 				failed = null;
337 				// Release all locks before trying again, to prevent deadlock.
338 				unlockAll(locks);
339 				locks.clear();
340 				RefDirectory.sleep(ms);
341 
342 				for (ReceiveCommand c : commands) {
343 					String name = c.getRefName();
344 					LockFile lock = new LockFile(refdb.fileFor(name));
345 					if (locks.put(name, lock) != null) {
346 						throw new IOException(
347 								MessageFormat.format(JGitText.get().duplicateRef, name));
348 					}
349 					if (!lock.lock()) {
350 						failed = c;
351 						continue RETRY;
352 					}
353 				}
354 				Map<String, LockFile> result = locks;
355 				locks = null;
356 				return result;
357 			}
358 		} finally {
359 			unlockAll(locks);
360 		}
361 		lockFailure(failed != null ? failed : commands.get(0), commands);
362 		return null;
363 	}
364 
365 	private static RefList<Ref> applyUpdates(RevWalk walk, RefList<Ref> refs,
366 			List<ReceiveCommand> commands) throws IOException {
367 		// Construct a new RefList by merging the old list with the updates.
368 		// This assumes that each ref occurs at most once as a ReceiveCommand.
369 		Collections.sort(commands, new Comparator<ReceiveCommand>() {
370 			@Override
371 			public int compare(ReceiveCommand./../../../../../org/eclipse/jgit/transport/ReceiveCommand.html#ReceiveCommand">ReceiveCommand a, ReceiveCommand b) {
372 				return a.getRefName().compareTo(b.getRefName());
373 			}
374 		});
375 
376 		int delta = 0;
377 		for (ReceiveCommand c : commands) {
378 			switch (c.getType()) {
379 			case DELETE:
380 				delta--;
381 				break;
382 			case CREATE:
383 				delta++;
384 				break;
385 			default:
386 			}
387 		}
388 
389 		RefList.Builder<Ref> b = new RefList.Builder<>(refs.size() + delta);
390 		int refIdx = 0;
391 		int cmdIdx = 0;
392 		while (refIdx < refs.size() || cmdIdx < commands.size()) {
393 			Ref ref = (refIdx < refs.size()) ? refs.get(refIdx) : null;
394 			ReceiveCommand cmd = (cmdIdx < commands.size())
395 					? commands.get(cmdIdx)
396 					: null;
397 			int cmp = 0;
398 			if (ref != null && cmd != null) {
399 				cmp = ref.getName().compareTo(cmd.getRefName());
400 			} else if (ref == null) {
401 				cmp = 1;
402 			} else if (cmd == null) {
403 				cmp = -1;
404 			}
405 
406 			if (cmp < 0) {
407 				b.add(ref);
408 				refIdx++;
409 			} else if (cmp > 0) {
410 				assert cmd != null;
411 				if (cmd.getType() != ReceiveCommand.Type.CREATE) {
412 					lockFailure(cmd, commands);
413 					return null;
414 				}
415 
416 				b.add(peeledRef(walk, cmd));
417 				cmdIdx++;
418 			} else {
419 				assert cmd != null;
420 				assert ref != null;
421 				if (!cmd.getOldId().equals(ref.getObjectId())) {
422 					lockFailure(cmd, commands);
423 					return null;
424 				}
425 
426 				if (cmd.getType() != ReceiveCommand.Type.DELETE) {
427 					b.add(peeledRef(walk, cmd));
428 				}
429 				cmdIdx++;
430 				refIdx++;
431 			}
432 		}
433 		return b.toRefList();
434 	}
435 
436 	private void writeReflog(List<ReceiveCommand> commands) {
437 		PersonIdent ident = getRefLogIdent();
438 		if (ident == null) {
439 			ident = new PersonIdent(refdb.getRepository());
440 		}
441 		for (ReceiveCommand cmd : commands) {
442 			// Assume any pending commands have already been executed atomically.
443 			if (cmd.getResult() != ReceiveCommand.Result.OK) {
444 				continue;
445 			}
446 			String name = cmd.getRefName();
447 
448 			if (cmd.getType() == ReceiveCommand.Type.DELETE) {
449 				try {
450 					RefDirectory.delete(refdb.logFor(name), RefDirectory.levelsIn(name));
451 				} catch (IOException e) {
452 					// Ignore failures, see below.
453 				}
454 				continue;
455 			}
456 
457 			if (isRefLogDisabled(cmd)) {
458 				continue;
459 			}
460 
461 			String msg = getRefLogMessage(cmd);
462 			if (isRefLogIncludingResult(cmd)) {
463 				String strResult = toResultString(cmd);
464 				if (strResult != null) {
465 					msg = msg.isEmpty()
466 							? strResult : msg + ": " + strResult; //$NON-NLS-1$
467 				}
468 			}
469 			try {
470 				new ReflogWriter(refdb, isForceRefLog(cmd))
471 						.log(name, cmd.getOldId(), cmd.getNewId(), ident, msg);
472 			} catch (IOException e) {
473 				// Ignore failures, but continue attempting to write more reflogs.
474 				//
475 				// In this storage format, it is impossible to atomically write the
476 				// reflog with the ref updates, so we have to choose between:
477 				// a. Propagating this exception and claiming failure, even though the
478 				//    actual ref updates succeeded.
479 				// b. Ignoring failures writing the reflog, so we claim success if and
480 				//    only if the ref updates succeeded.
481 				// We choose (b) in order to surprise callers the least.
482 				//
483 				// Possible future improvements:
484 				// * Log a warning to a logger.
485 				// * Retry a fixed number of times in case the error was transient.
486 			}
487 		}
488 	}
489 
490 	private String toResultString(ReceiveCommand cmd) {
491 		switch (cmd.getType()) {
492 		case CREATE:
493 			return ReflogEntry.PREFIX_CREATED;
494 		case UPDATE:
495 			// Match the behavior of a single RefUpdate. In that case, setting the
496 			// force bit completely bypasses the potentially expensive isMergedInto
497 			// check, by design, so the reflog message may be inaccurate.
498 			//
499 			// Similarly, this class bypasses the isMergedInto checks when the force
500 			// bit is set, meaning we can't actually distinguish between UPDATE and
501 			// UPDATE_NONFASTFORWARD when isAllowNonFastForwards() returns true.
502 			return isAllowNonFastForwards()
503 					? ReflogEntry.PREFIX_FORCED_UPDATE : ReflogEntry.PREFIX_FAST_FORWARD;
504 		case UPDATE_NONFASTFORWARD:
505 			return ReflogEntry.PREFIX_FORCED_UPDATE;
506 		default:
507 			return null;
508 		}
509 	}
510 
511 	private static Ref peeledRef(RevWalk walk, ReceiveCommand cmd)
512 			throws IOException {
513 		ObjectId newId = cmd.getNewId().copy();
514 		RevObject obj = walk.parseAny(newId);
515 		if (obj instanceof RevTag) {
516 			return new ObjectIdRef.PeeledTag(
517 					Ref.Storage.PACKED, cmd.getRefName(), newId, walk.peel(obj).copy());
518 		}
519 		return new ObjectIdRef.PeeledNonTag(
520 				Ref.Storage.PACKED, cmd.getRefName(), newId);
521 	}
522 
523 	private static void unlockAll(@Nullable Map<?, LockFile> locks) {
524 		if (locks != null) {
525 			locks.values().forEach(LockFile::unlock);
526 		}
527 	}
528 
529 	private static void lockFailure(ReceiveCommand cmd,
530 			List<ReceiveCommand> commands) {
531 		reject(cmd, LOCK_FAILURE, commands);
532 	}
533 
534 	private static void reject(ReceiveCommand cmd, ReceiveCommand.Result result,
535 			List<ReceiveCommand> commands) {
536 		reject(cmd, result, null, commands);
537 	}
538 
539 	private static void reject(ReceiveCommand cmd, ReceiveCommand.Result result,
540 			String why, List<ReceiveCommand> commands) {
541 		cmd.setResult(result, why);
542 		for (ReceiveCommand c2 : commands) {
543 			if (c2.getResult() == ReceiveCommand.Result.OK) {
544 				// Undo OK status so ReceiveCommand#abort aborts it. Assumes this method
545 				// is always called before committing any updates to disk.
546 				c2.setResult(ReceiveCommand.Result.NOT_ATTEMPTED);
547 			}
548 		}
549 		ReceiveCommand.abort(commands);
550 	}
551 }