You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FileReftableStack.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  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.util.ArrayList;
  22. import java.util.Comparator;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Optional;
  26. import java.util.function.Supplier;
  27. import java.util.stream.Collectors;
  28. import org.eclipse.jgit.annotations.Nullable;
  29. import org.eclipse.jgit.errors.LockFailedException;
  30. import org.eclipse.jgit.internal.storage.io.BlockSource;
  31. import org.eclipse.jgit.internal.storage.reftable.MergedReftable;
  32. import org.eclipse.jgit.internal.storage.reftable.ReftableCompactor;
  33. import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
  34. import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
  35. import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
  36. import org.eclipse.jgit.lib.Config;
  37. import org.eclipse.jgit.util.FileUtils;
  38. /**
  39. * A mutable stack of reftables on local filesystem storage. Not thread-safe.
  40. * This is an AutoCloseable because this object owns the file handles to the
  41. * open reftables.
  42. */
  43. public class FileReftableStack implements AutoCloseable {
  44. private static class StackEntry {
  45. String name;
  46. ReftableReader reftableReader;
  47. }
  48. private MergedReftable mergedReftable;
  49. private List<StackEntry> stack;
  50. private long lastNextUpdateIndex;
  51. private final File stackPath;
  52. private final File reftableDir;
  53. private final Runnable onChange;
  54. private final Supplier<Config> configSupplier;
  55. // Used for stats & testing.
  56. static class CompactionStats {
  57. long tables;
  58. long bytes;
  59. int attempted;
  60. int failed;
  61. long refCount;
  62. long logCount;
  63. CompactionStats() {
  64. tables = 0;
  65. bytes = 0;
  66. attempted = 0;
  67. failed = 0;
  68. logCount = 0;
  69. refCount = 0;
  70. }
  71. }
  72. private final CompactionStats stats;
  73. /**
  74. * Creates a stack corresponding to the list of reftables in the argument
  75. *
  76. * @param stackPath
  77. * the filename for the stack.
  78. * @param reftableDir
  79. * the dir holding the tables.
  80. * @param onChange
  81. * hook to call if we notice a new write
  82. * @param configSupplier
  83. * Config supplier
  84. * @throws IOException
  85. * on I/O problems
  86. */
  87. public FileReftableStack(File stackPath, File reftableDir,
  88. @Nullable Runnable onChange, Supplier<Config> configSupplier)
  89. throws IOException {
  90. this.stackPath = stackPath;
  91. this.reftableDir = reftableDir;
  92. this.stack = new ArrayList<>();
  93. this.configSupplier = configSupplier;
  94. this.onChange = onChange;
  95. // skip event notification
  96. lastNextUpdateIndex = 0;
  97. reload();
  98. stats = new CompactionStats();
  99. }
  100. CompactionStats getStats() {
  101. return stats;
  102. }
  103. /** Thrown if the update indices in the stack are not monotonic */
  104. public static class ReftableNumbersNotIncreasingException
  105. extends RuntimeException {
  106. private static final long serialVersionUID = 1L;
  107. String name;
  108. long lastMax;
  109. long min;
  110. ReftableNumbersNotIncreasingException(String name, long lastMax,
  111. long min) {
  112. this.name = name;
  113. this.lastMax = lastMax;
  114. this.min = min;
  115. }
  116. @SuppressWarnings({ "nls", "boxing" })
  117. @Override
  118. public String toString() {
  119. return String.format(
  120. "ReftableNumbersNotIncreasingException %s: min %d, lastMax %d",
  121. name, min, lastMax);
  122. }
  123. }
  124. /**
  125. * Reloads the stack, potentially reusing opened reftableReaders.
  126. *
  127. * @param names
  128. * holds the names of the tables to load.
  129. * @throws FileNotFoundException
  130. * load must be retried.
  131. * @throws IOException
  132. * on other IO errors.
  133. */
  134. private void reloadOnce(List<String> names)
  135. throws IOException, FileNotFoundException {
  136. Map<String, ReftableReader> current = stack.stream()
  137. .collect(Collectors.toMap(e -> e.name, e -> e.reftableReader));
  138. List<ReftableReader> newTables = new ArrayList<>();
  139. List<StackEntry> newStack = new ArrayList<>(stack.size() + 1);
  140. try {
  141. ReftableReader last = null;
  142. for (String name : names) {
  143. StackEntry entry = new StackEntry();
  144. entry.name = name;
  145. ReftableReader t = null;
  146. if (current.containsKey(name)) {
  147. t = current.remove(name);
  148. } else {
  149. File subtable = new File(reftableDir, name);
  150. FileInputStream is;
  151. is = new FileInputStream(subtable);
  152. t = new ReftableReader(BlockSource.from(is));
  153. newTables.add(t);
  154. }
  155. if (last != null) {
  156. // TODO: move this to MergedReftable
  157. if (last.maxUpdateIndex() >= t.minUpdateIndex()) {
  158. throw new ReftableNumbersNotIncreasingException(name,
  159. last.maxUpdateIndex(), t.minUpdateIndex());
  160. }
  161. }
  162. last = t;
  163. entry.reftableReader = t;
  164. newStack.add(entry);
  165. }
  166. // survived without exceptions: swap in new stack, and close
  167. // dangling tables.
  168. stack = newStack;
  169. newTables.clear();
  170. current.values().forEach(r -> {
  171. try {
  172. r.close();
  173. } catch (IOException e) {
  174. throw new AssertionError(e);
  175. }
  176. });
  177. } finally {
  178. newTables.forEach(t -> {
  179. try {
  180. t.close();
  181. } catch (IOException ioe) {
  182. // reader close should not generate errors.
  183. throw new AssertionError(ioe);
  184. }
  185. });
  186. }
  187. }
  188. void reload() throws IOException {
  189. // Try for 2.5 seconds.
  190. long deadline = System.currentTimeMillis() + 2500;
  191. // A successful reftable transaction is 2 atomic file writes
  192. // (open, write, close, rename), which a fast Linux system should be
  193. // able to do in about ~200us. So 1 ms should be ample time.
  194. long min = 1;
  195. long max = 1000;
  196. long delay = 0;
  197. boolean success = false;
  198. // Don't check deadline for the first 3 retries, so we can step with a
  199. // debugger without worrying about deadlines.
  200. int tries = 0;
  201. while (tries < 3 || System.currentTimeMillis() < deadline) {
  202. List<String> names = readTableNames();
  203. tries++;
  204. try {
  205. reloadOnce(names);
  206. success = true;
  207. break;
  208. } catch (FileNotFoundException e) {
  209. List<String> changed = readTableNames();
  210. if (changed.equals(names)) {
  211. throw e;
  212. }
  213. }
  214. delay = FileUtils.delay(delay, min, max);
  215. try {
  216. Thread.sleep(delay);
  217. } catch (InterruptedException e) {
  218. Thread.currentThread().interrupt();
  219. throw new RuntimeException(e);
  220. }
  221. }
  222. if (!success) {
  223. throw new LockFailedException(stackPath);
  224. }
  225. mergedReftable = new MergedReftable(stack.stream()
  226. .map(x -> x.reftableReader).collect(Collectors.toList()));
  227. long curr = nextUpdateIndex();
  228. if (lastNextUpdateIndex > 0 && lastNextUpdateIndex != curr
  229. && onChange != null) {
  230. onChange.run();
  231. }
  232. lastNextUpdateIndex = curr;
  233. }
  234. /**
  235. * @return the merged reftable
  236. */
  237. public MergedReftable getMergedReftable() {
  238. return mergedReftable;
  239. }
  240. /**
  241. * Writer is a callable that writes data to a reftable under construction.
  242. * It should set the min/max update index, and then write refs and/or logs.
  243. * It should not call finish() on the writer.
  244. */
  245. public interface Writer {
  246. /**
  247. * Write data to reftable
  248. *
  249. * @param w
  250. * writer to use
  251. * @throws IOException
  252. */
  253. void call(ReftableWriter w) throws IOException;
  254. }
  255. private List<String> readTableNames() throws IOException {
  256. List<String> names = new ArrayList<>(stack.size() + 1);
  257. try (BufferedReader br = new BufferedReader(
  258. new InputStreamReader(new FileInputStream(stackPath), UTF_8))) {
  259. String line;
  260. while ((line = br.readLine()) != null) {
  261. if (!line.isEmpty()) {
  262. names.add(line);
  263. }
  264. }
  265. } catch (FileNotFoundException e) {
  266. // file isn't there: empty repository.
  267. }
  268. return names;
  269. }
  270. /**
  271. * @return true if the on-disk file corresponds to the in-memory data.
  272. * @throws IOException
  273. * on IO problem
  274. */
  275. boolean isUpToDate() throws IOException {
  276. // We could use FileSnapshot to avoid reading the file, but the file is
  277. // small so it's probably a minor optimization.
  278. try {
  279. List<String> names = readTableNames();
  280. if (names.size() != stack.size()) {
  281. return false;
  282. }
  283. for (int i = 0; i < names.size(); i++) {
  284. if (!names.get(i).equals(stack.get(i).name)) {
  285. return false;
  286. }
  287. }
  288. } catch (FileNotFoundException e) {
  289. return stack.isEmpty();
  290. }
  291. return true;
  292. }
  293. /**
  294. * {@inheritDoc}
  295. */
  296. @Override
  297. public void close() {
  298. for (StackEntry entry : stack) {
  299. try {
  300. entry.reftableReader.close();
  301. } catch (Exception e) {
  302. // we are reading; this should never fail.
  303. throw new AssertionError(e);
  304. }
  305. }
  306. }
  307. private long nextUpdateIndex() throws IOException {
  308. return stack.size() > 0
  309. ? stack.get(stack.size() - 1).reftableReader.maxUpdateIndex()
  310. + 1
  311. : 1;
  312. }
  313. private String filename(long low, long high) {
  314. return String.format("%012x-%012x", //$NON-NLS-1$
  315. Long.valueOf(low), Long.valueOf(high));
  316. }
  317. /**
  318. * Tries to add a new reftable to the stack. Returns true if it succeeded,
  319. * or false if there was a lock failure, due to races with other processes.
  320. * This is package private so FileReftableDatabase can call into here.
  321. *
  322. * @param w
  323. * writer to write data to a reftable under construction
  324. * @return true if the transaction was successful.
  325. * @throws IOException
  326. * on I/O problems
  327. */
  328. @SuppressWarnings("nls")
  329. public boolean addReftable(Writer w) throws IOException {
  330. LockFile lock = new LockFile(stackPath);
  331. try {
  332. if (!lock.lockForAppend()) {
  333. return false;
  334. }
  335. if (!isUpToDate()) {
  336. return false;
  337. }
  338. String fn = filename(nextUpdateIndex(), nextUpdateIndex());
  339. File tmpTable = File.createTempFile(fn + "_", ".ref",
  340. stackPath.getParentFile());
  341. ReftableWriter.Stats s;
  342. try (FileOutputStream fos = new FileOutputStream(tmpTable)) {
  343. ReftableWriter rw = new ReftableWriter(reftableConfig(), fos);
  344. w.call(rw);
  345. rw.finish();
  346. s = rw.getStats();
  347. }
  348. if (s.minUpdateIndex() < nextUpdateIndex()) {
  349. return false;
  350. }
  351. // The spec says to name log-only files with .log, which is somewhat
  352. // pointless given compaction, but we do so anyway.
  353. fn += s.refCount() > 0 ? ".ref" : ".log";
  354. File dest = new File(reftableDir, fn);
  355. FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE);
  356. lock.write((fn + "\n").getBytes(UTF_8));
  357. if (!lock.commit()) {
  358. FileUtils.delete(dest);
  359. return false;
  360. }
  361. reload();
  362. autoCompact();
  363. } finally {
  364. lock.unlock();
  365. }
  366. return true;
  367. }
  368. private ReftableConfig reftableConfig() {
  369. return new ReftableConfig(configSupplier.get());
  370. }
  371. /**
  372. * Write the reftable for the given range into a temp file.
  373. *
  374. * @param first
  375. * index of first stack entry to be written
  376. * @param last
  377. * index of last stack entry to be written
  378. * @return the file holding the replacement table.
  379. * @throws IOException
  380. * on I/O problem
  381. */
  382. private File compactLocked(int first, int last) throws IOException {
  383. String fn = filename(first, last);
  384. File tmpTable = File.createTempFile(fn + "_", ".ref", //$NON-NLS-1$//$NON-NLS-2$
  385. stackPath.getParentFile());
  386. try (FileOutputStream fos = new FileOutputStream(tmpTable)) {
  387. ReftableCompactor c = new ReftableCompactor(fos)
  388. .setConfig(reftableConfig())
  389. .setMinUpdateIndex(
  390. stack.get(first).reftableReader.minUpdateIndex())
  391. .setMaxUpdateIndex(
  392. stack.get(last).reftableReader.maxUpdateIndex())
  393. .setIncludeDeletes(first > 0);
  394. List<ReftableReader> compactMe = new ArrayList<>();
  395. long totalBytes = 0;
  396. for (int i = first; i <= last; i++) {
  397. compactMe.add(stack.get(i).reftableReader);
  398. totalBytes += stack.get(i).reftableReader.size();
  399. }
  400. c.addAll(compactMe);
  401. c.compact();
  402. // Even though the compaction did not definitely succeed, we keep
  403. // tally here as we've expended the effort.
  404. stats.bytes += totalBytes;
  405. stats.tables += first - last + 1;
  406. stats.attempted++;
  407. stats.refCount += c.getStats().refCount();
  408. stats.logCount += c.getStats().logCount();
  409. }
  410. return tmpTable;
  411. }
  412. /**
  413. * Compacts a range of the stack, following the file locking protocol
  414. * documented in the spec.
  415. *
  416. * @param first
  417. * index of first stack entry to be considered in compaction
  418. * @param last
  419. * index of last stack entry to be considered in compaction
  420. * @return true if a compaction was successfully applied.
  421. * @throws IOException
  422. * on I/O problem
  423. */
  424. boolean compactRange(int first, int last) throws IOException {
  425. if (first >= last) {
  426. return true;
  427. }
  428. LockFile lock = new LockFile(stackPath);
  429. File tmpTable = null;
  430. List<LockFile> subtableLocks = new ArrayList<>();
  431. try {
  432. if (!lock.lock()) {
  433. return false;
  434. }
  435. if (!isUpToDate()) {
  436. return false;
  437. }
  438. List<File> deleteOnSuccess = new ArrayList<>();
  439. for (int i = first; i <= last; i++) {
  440. File f = new File(reftableDir, stack.get(i).name);
  441. LockFile lf = new LockFile(f);
  442. if (!lf.lock()) {
  443. return false;
  444. }
  445. subtableLocks.add(lf);
  446. deleteOnSuccess.add(f);
  447. }
  448. lock.unlock();
  449. lock = null;
  450. tmpTable = compactLocked(first, last);
  451. lock = new LockFile(stackPath);
  452. if (!lock.lock()) {
  453. return false;
  454. }
  455. if (!isUpToDate()) {
  456. return false;
  457. }
  458. String fn = filename(
  459. stack.get(first).reftableReader.minUpdateIndex(),
  460. stack.get(last).reftableReader.maxUpdateIndex());
  461. // The spec suggests to use .log for log-only tables, and collect
  462. // all log entries in a single file at the bottom of the stack. That would
  463. // require supporting overlapping ranges for the different tables. For the
  464. // sake of simplicity, we simply ignore this and always produce a log +
  465. // ref combined table.
  466. fn += ".ref"; //$NON-NLS-1$
  467. File dest = new File(reftableDir, fn);
  468. FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE);
  469. tmpTable = null;
  470. StringBuilder sb = new StringBuilder();
  471. for (int i = 0; i < first; i++) {
  472. sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  473. }
  474. sb.append(fn + "\n"); //$NON-NLS-1$
  475. for (int i = last + 1; i < stack.size(); i++) {
  476. sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  477. }
  478. lock.write(sb.toString().getBytes(UTF_8));
  479. if (!lock.commit()) {
  480. dest.delete();
  481. return false;
  482. }
  483. for (File f : deleteOnSuccess) {
  484. Files.delete(f.toPath());
  485. }
  486. reload();
  487. return true;
  488. } finally {
  489. if (tmpTable != null) {
  490. tmpTable.delete();
  491. }
  492. for (LockFile lf : subtableLocks) {
  493. lf.unlock();
  494. }
  495. if (lock != null) {
  496. lock.unlock();
  497. }
  498. }
  499. }
  500. /**
  501. * Calculate an approximate log2.
  502. *
  503. * @param sz
  504. * @return log2
  505. */
  506. static int log(long sz) {
  507. long base = 2;
  508. if (sz <= 0) {
  509. throw new IllegalArgumentException("log2 negative"); //$NON-NLS-1$
  510. }
  511. int l = 0;
  512. while (sz > 0) {
  513. l++;
  514. sz /= base;
  515. }
  516. return l - 1;
  517. }
  518. /**
  519. * A segment is a consecutive list of reftables of the same approximate
  520. * size.
  521. */
  522. static class Segment {
  523. // the approximate log_2 of the size.
  524. int log;
  525. // The total bytes in this segment
  526. long bytes;
  527. int start;
  528. int end; // exclusive.
  529. int size() {
  530. return end - start;
  531. }
  532. Segment(int start, int end, int log, long bytes) {
  533. this.log = log;
  534. this.start = start;
  535. this.end = end;
  536. this.bytes = bytes;
  537. }
  538. Segment() {
  539. this(0, 0, 0, 0);
  540. }
  541. @Override
  542. public int hashCode() {
  543. return 0; // appease error-prone
  544. }
  545. @Override
  546. public boolean equals(Object other) {
  547. Segment o = (Segment) other;
  548. return o.bytes == bytes && o.log == log && o.start == start
  549. && o.end == end;
  550. }
  551. @SuppressWarnings("boxing")
  552. @Override
  553. public String toString() {
  554. return String.format("{ [%d,%d) l=%d sz=%d }", start, end, log, //$NON-NLS-1$
  555. bytes);
  556. }
  557. }
  558. static List<Segment> segmentSizes(long sizes[]) {
  559. List<Segment> segments = new ArrayList<>();
  560. Segment cur = new Segment();
  561. for (int i = 0; i < sizes.length; i++) {
  562. int l = log(sizes[i]);
  563. if (l != cur.log && cur.bytes > 0) {
  564. segments.add(cur);
  565. cur = new Segment();
  566. cur.start = i;
  567. cur.log = l;
  568. }
  569. cur.log = l;
  570. cur.end = i + 1;
  571. cur.bytes += sizes[i];
  572. }
  573. segments.add(cur);
  574. return segments;
  575. }
  576. private static Optional<Segment> autoCompactCandidate(long sizes[]) {
  577. if (sizes.length == 0) {
  578. return Optional.empty();
  579. }
  580. // The cost of compaction is proportional to the size, and we want to
  581. // avoid frequent large compactions. We do this by playing the game 2048
  582. // here: first compact together the smallest tables if there are more
  583. // than one. Then try to see if the result will be big enough to match
  584. // up with next up.
  585. List<Segment> segments = segmentSizes(sizes);
  586. segments = segments.stream().filter(s -> s.size() > 1)
  587. .collect(Collectors.toList());
  588. if (segments.isEmpty()) {
  589. return Optional.empty();
  590. }
  591. Optional<Segment> optMinSeg = segments.stream()
  592. .min(Comparator.comparing(s -> Integer.valueOf(s.log)));
  593. // Input is non-empty, so always present.
  594. Segment smallCollected = optMinSeg.get();
  595. while (smallCollected.start > 0) {
  596. int prev = smallCollected.start - 1;
  597. long prevSize = sizes[prev];
  598. if (log(smallCollected.bytes) < log(prevSize)) {
  599. break;
  600. }
  601. smallCollected.start = prev;
  602. smallCollected.bytes += prevSize;
  603. }
  604. return Optional.of(smallCollected);
  605. }
  606. /**
  607. * Heuristically tries to compact the stack if the stack has a suitable
  608. * shape.
  609. *
  610. * @throws IOException
  611. */
  612. private void autoCompact() throws IOException {
  613. Optional<Segment> cand = autoCompactCandidate(tableSizes());
  614. if (cand.isPresent()) {
  615. if (!compactRange(cand.get().start, cand.get().end - 1)) {
  616. stats.failed++;
  617. }
  618. }
  619. }
  620. // 68b footer, 24b header = 92.
  621. private static long OVERHEAD = 91;
  622. private long[] tableSizes() throws IOException {
  623. long[] sizes = new long[stack.size()];
  624. for (int i = 0; i < stack.size(); i++) {
  625. // If we don't subtract the overhead, the file size isn't
  626. // proportional to the number of entries. This will cause us to
  627. // compact too often, which is expensive.
  628. sizes[i] = stack.get(i).reftableReader.size() - OVERHEAD;
  629. }
  630. return sizes;
  631. }
  632. void compactFully() throws IOException {
  633. if (!compactRange(0, stack.size() - 1)) {
  634. stats.failed++;
  635. }
  636. }
  637. }