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