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

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