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 17KB

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