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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  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. .setIncludeDeletes(first > 0);
  390. List<ReftableReader> compactMe = new ArrayList<>();
  391. long totalBytes = 0;
  392. for (int i = first; i <= last; i++) {
  393. compactMe.add(stack.get(i).reftableReader);
  394. totalBytes += stack.get(i).reftableReader.size();
  395. }
  396. c.addAll(compactMe);
  397. c.compact();
  398. // Even though the compaction did not definitely succeed, we keep
  399. // tally here as we've expended the effort.
  400. stats.bytes += totalBytes;
  401. stats.tables += first - last + 1;
  402. stats.attempted++;
  403. stats.refCount += c.getStats().refCount();
  404. stats.logCount += c.getStats().logCount();
  405. }
  406. return tmpTable;
  407. }
  408. /**
  409. * Compacts a range of the stack, following the file locking protocol
  410. * documented in the spec.
  411. *
  412. * @param first
  413. * index of first stack entry to be considered in compaction
  414. * @param last
  415. * index of last stack entry to be considered in compaction
  416. * @return true if a compaction was successfully applied.
  417. * @throws IOException
  418. * on I/O problem
  419. */
  420. boolean compactRange(int first, int last) throws IOException {
  421. if (first >= last) {
  422. return true;
  423. }
  424. LockFile lock = new LockFile(stackPath);
  425. File tmpTable = null;
  426. List<LockFile> subtableLocks = new ArrayList<>();
  427. try {
  428. if (!lock.lock()) {
  429. return false;
  430. }
  431. if (!isUpToDate()) {
  432. return false;
  433. }
  434. List<File> deleteOnSuccess = new ArrayList<>();
  435. for (int i = first; i <= last; i++) {
  436. File f = new File(reftableDir, stack.get(i).name);
  437. LockFile lf = new LockFile(f);
  438. if (!lf.lock()) {
  439. return false;
  440. }
  441. subtableLocks.add(lf);
  442. deleteOnSuccess.add(f);
  443. }
  444. lock.unlock();
  445. lock = null;
  446. tmpTable = compactLocked(first, last);
  447. lock = new LockFile(stackPath);
  448. if (!lock.lock()) {
  449. return false;
  450. }
  451. if (!isUpToDate()) {
  452. return false;
  453. }
  454. String fn = filename(
  455. stack.get(first).reftableReader.minUpdateIndex(),
  456. stack.get(last).reftableReader.maxUpdateIndex());
  457. // The spec suggests to use .log for log-only tables, and collect
  458. // all log entries in a single file at the bottom of the stack. That would
  459. // require supporting overlapping ranges for the different tables. For the
  460. // sake of simplicity, we simply ignore this and always produce a log +
  461. // ref combined table.
  462. fn += ".ref"; //$NON-NLS-1$
  463. File dest = new File(reftableDir, fn);
  464. FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE);
  465. tmpTable = null;
  466. StringBuilder sb = new StringBuilder();
  467. for (int i = 0; i < first; i++) {
  468. sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  469. }
  470. sb.append(fn + "\n"); //$NON-NLS-1$
  471. for (int i = last + 1; i < stack.size(); i++) {
  472. sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$
  473. }
  474. lock.write(sb.toString().getBytes(UTF_8));
  475. if (!lock.commit()) {
  476. dest.delete();
  477. return false;
  478. }
  479. for (File f : deleteOnSuccess) {
  480. Files.delete(f.toPath());
  481. }
  482. reload();
  483. return true;
  484. } finally {
  485. if (tmpTable != null) {
  486. tmpTable.delete();
  487. }
  488. for (LockFile lf : subtableLocks) {
  489. lf.unlock();
  490. }
  491. if (lock != null) {
  492. lock.unlock();
  493. }
  494. }
  495. }
  496. /**
  497. * Calculate an approximate log2.
  498. *
  499. * @param sz
  500. * @return log2
  501. */
  502. static int log(long sz) {
  503. long base = 2;
  504. if (sz <= 0) {
  505. throw new IllegalArgumentException("log2 negative"); //$NON-NLS-1$
  506. }
  507. int l = 0;
  508. while (sz > 0) {
  509. l++;
  510. sz /= base;
  511. }
  512. return l - 1;
  513. }
  514. /**
  515. * A segment is a consecutive list of reftables of the same approximate
  516. * size.
  517. */
  518. static class Segment {
  519. // the approximate log_2 of the size.
  520. int log;
  521. // The total bytes in this segment
  522. long bytes;
  523. int start;
  524. int end; // exclusive.
  525. int size() {
  526. return end - start;
  527. }
  528. Segment(int start, int end, int log, long bytes) {
  529. this.log = log;
  530. this.start = start;
  531. this.end = end;
  532. this.bytes = bytes;
  533. }
  534. Segment() {
  535. this(0, 0, 0, 0);
  536. }
  537. @Override
  538. public int hashCode() {
  539. return 0; // appease error-prone
  540. }
  541. @Override
  542. public boolean equals(Object other) {
  543. Segment o = (Segment) other;
  544. return o.bytes == bytes && o.log == log && o.start == start
  545. && o.end == end;
  546. }
  547. @SuppressWarnings("boxing")
  548. @Override
  549. public String toString() {
  550. return String.format("{ [%d,%d) l=%d sz=%d }", start, end, log, //$NON-NLS-1$
  551. bytes);
  552. }
  553. }
  554. static List<Segment> segmentSizes(long sizes[]) {
  555. List<Segment> segments = new ArrayList<>();
  556. Segment cur = new Segment();
  557. for (int i = 0; i < sizes.length; i++) {
  558. int l = log(sizes[i]);
  559. if (l != cur.log && cur.bytes > 0) {
  560. segments.add(cur);
  561. cur = new Segment();
  562. cur.start = i;
  563. cur.log = l;
  564. }
  565. cur.log = l;
  566. cur.end = i + 1;
  567. cur.bytes += sizes[i];
  568. }
  569. segments.add(cur);
  570. return segments;
  571. }
  572. private static Optional<Segment> autoCompactCandidate(long sizes[]) {
  573. if (sizes.length == 0) {
  574. return Optional.empty();
  575. }
  576. // The cost of compaction is proportional to the size, and we want to
  577. // avoid frequent large compactions. We do this by playing the game 2048
  578. // here: first compact together the smallest tables if there are more
  579. // than one. Then try to see if the result will be big enough to match
  580. // up with next up.
  581. List<Segment> segments = segmentSizes(sizes);
  582. segments = segments.stream().filter(s -> s.size() > 1)
  583. .collect(Collectors.toList());
  584. if (segments.isEmpty()) {
  585. return Optional.empty();
  586. }
  587. Optional<Segment> optMinSeg = segments.stream()
  588. .min(Comparator.comparing(s -> Integer.valueOf(s.log)));
  589. // Input is non-empty, so always present.
  590. Segment smallCollected = optMinSeg.get();
  591. while (smallCollected.start > 0) {
  592. int prev = smallCollected.start - 1;
  593. long prevSize = sizes[prev];
  594. if (log(smallCollected.bytes) < log(prevSize)) {
  595. break;
  596. }
  597. smallCollected.start = prev;
  598. smallCollected.bytes += prevSize;
  599. }
  600. return Optional.of(smallCollected);
  601. }
  602. /**
  603. * Heuristically tries to compact the stack if the stack has a suitable
  604. * shape.
  605. *
  606. * @throws IOException
  607. */
  608. private void autoCompact() throws IOException {
  609. Optional<Segment> cand = autoCompactCandidate(tableSizes());
  610. if (cand.isPresent()) {
  611. if (!compactRange(cand.get().start, cand.get().end - 1)) {
  612. stats.failed++;
  613. }
  614. }
  615. }
  616. // 68b footer, 24b header = 92.
  617. private static long OVERHEAD = 91;
  618. private long[] tableSizes() throws IOException {
  619. long[] sizes = new long[stack.size()];
  620. for (int i = 0; i < stack.size(); i++) {
  621. // If we don't subtract the overhead, the file size isn't
  622. // proportional to the number of entries. This will cause us to
  623. // compact too often, which is expensive.
  624. sizes[i] = stack.get(i).reftableReader.size() - OVERHEAD;
  625. }
  626. return sizes;
  627. }
  628. void compactFully() throws IOException {
  629. if (!compactRange(0, stack.size() - 1)) {
  630. stats.failed++;
  631. }
  632. }
  633. }