您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

FileReftableStack.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  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. }