1 /*
2 * Copyright (C) 2017, Google Inc. 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
11 package org.eclipse.jgit.internal.storage.reftable;
12
13 import java.io.IOException;
14 import java.io.OutputStream;
15 import java.util.ArrayDeque;
16 import java.util.ArrayList;
17 import java.util.List;
18
19 import org.eclipse.jgit.internal.storage.reftable.ReftableWriter.Stats;
20 import org.eclipse.jgit.lib.PersonIdent;
21 import org.eclipse.jgit.lib.ReflogEntry;
22
23 /**
24 * Merges reftables and compacts them into a single output.
25 * <p>
26 * For a partial compaction callers should {@link #setIncludeDeletes(boolean)}
27 * to {@code true} to ensure the new reftable continues to use a delete marker
28 * to shadow any lower reftable that may have the reference present.
29 * <p>
30 * By default all log entries within the range defined by
31 * {@link #setReflogExpireMinUpdateIndex(long)} and {@link #setReflogExpireMaxUpdateIndex(long)} are
32 * copied, even if no references in the output file match the log records.
33 * Callers may truncate the log to a more recent time horizon with
34 * {@link #setReflogExpireOldestReflogTimeMillis(long)}, or disable the log altogether with
35 * {@code setOldestReflogTimeMillis(Long.MAX_VALUE)}.
36 */
37 public class ReftableCompactor {
38 private final ReftableWriter writer;
39 private final ArrayDeque<ReftableReader> tables = new ArrayDeque<>();
40
41 private boolean includeDeletes;
42 private long reflogExpireMinUpdateIndex = 0;
43 private long reflogExpireMaxUpdateIndex = Long.MAX_VALUE;
44 private long reflogExpireOldestReflogTimeMillis;
45 private Stats stats;
46
47 /**
48 * Creates a new compactor.
49 *
50 * @param out
51 * stream to write the compacted tables to. Caller is responsible
52 * for closing {@code out}.
53 */
54 public ReftableCompactor(OutputStream out) {
55 writer = new ReftableWriter(out);
56 }
57
58 /**
59 * Set configuration for the reftable.
60 *
61 * @param cfg
62 * configuration for the reftable.
63 * @return {@code this}
64 */
65 public ReftableCompactor setConfig(ReftableConfig cfg) {
66 writer.setConfig(cfg);
67 return this;
68 }
69
70 /**
71 * Whether to include deletions in the output, which may be necessary for
72 * partial compaction.
73 *
74 * @param deletes
75 * {@code true} to include deletions in the output, which may be
76 * necessary for partial compaction.
77 * @return {@code this}
78 */
79 public ReftableCompactor setIncludeDeletes(boolean deletes) {
80 includeDeletes = deletes;
81 return this;
82 }
83
84 /**
85 * Set the minimum update index for log entries that appear in the compacted
86 * reftable.
87 *
88 * @param min
89 * the minimum update index for log entries that appear in the
90 * compacted reftable. This should be 1 higher than the prior
91 * reftable's {@code maxUpdateIndex} if this table will be used
92 * in a stack.
93 * @return {@code this}
94 */
95 public ReftableCompactor setReflogExpireMinUpdateIndex(long min) {
96 reflogExpireMinUpdateIndex = min;
97 return this;
98 }
99
100 /**
101 * Set the maximum update index for log entries that appear in the compacted
102 * reftable.
103 *
104 * @param max
105 * the maximum update index for log entries that appear in the
106 * compacted reftable. This should be at least 1 higher than the
107 * prior reftable's {@code maxUpdateIndex} if this table will be
108 * used in a stack.
109 * @return {@code this}
110 */
111 public ReftableCompactor setReflogExpireMaxUpdateIndex(long max) {
112 reflogExpireMaxUpdateIndex = max;
113 return this;
114 }
115
116 /**
117 * Set oldest reflog time to preserve.
118 *
119 * @param timeMillis
120 * oldest log time to preserve. Entries whose timestamps are
121 * {@code >= timeMillis} will be copied into the output file. Log
122 * entries that predate {@code timeMillis} will be discarded.
123 * Specified in Java standard milliseconds since the epoch.
124 * @return {@code this}
125 */
126 public ReftableCompactor setReflogExpireOldestReflogTimeMillis(long timeMillis) {
127 reflogExpireOldestReflogTimeMillis = timeMillis;
128 return this;
129 }
130
131 /**
132 * Add all of the tables, in the specified order.
133 *
134 * @param readers
135 * tables to compact. Tables should be ordered oldest first/most
136 * recent last so that the more recent tables can shadow the
137 * older results. Caller is responsible for closing the readers.
138 * @throws java.io.IOException
139 * update indexes of a reader cannot be accessed.
140 */
141 public void addAll(List<ReftableReader> readers) throws IOException {
142 for (ReftableReader r : readers) {
143 tables.add(r);
144 }
145 }
146
147 /**
148 * Write a compaction to {@code out}.
149 *
150 * @throws java.io.IOException
151 * if tables cannot be read, or cannot be written.
152 */
153 public void compact() throws IOException {
154 MergedReftable mr = new MergedReftable(new ArrayList<>(tables));
155 mr.setIncludeDeletes(includeDeletes);
156
157 writer.setMaxUpdateIndex(mr.maxUpdateIndex());
158 writer.setMinUpdateIndex(mr.minUpdateIndex());
159
160 writer.begin();
161 mergeRefs(mr);
162 mergeLogs(mr);
163 writer.finish();
164 stats = writer.getStats();
165 }
166
167 /**
168 * Get statistics of the last written reftable.
169 *
170 * @return statistics of the last written reftable.
171 */
172 public Stats getStats() {
173 return stats;
174 }
175
176 private void mergeRefs(MergedReftable mr) throws IOException {
177 try (RefCursor rc = mr.allRefs()) {
178 while (rc.next()) {
179 writer.writeRef(rc.getRef(), rc.getRef().getUpdateIndex());
180 }
181 }
182 }
183
184 private void mergeLogs(MergedReftable mr) throws IOException {
185 if (reflogExpireOldestReflogTimeMillis == Long.MAX_VALUE) {
186 return;
187 }
188
189 try (LogCursor lc = mr.allLogs()) {
190 while (lc.next()) {
191 long updateIndex = lc.getUpdateIndex();
192 if (updateIndex > reflogExpireMaxUpdateIndex || updateIndex < reflogExpireMinUpdateIndex) {
193 continue;
194 }
195
196 String refName = lc.getRefName();
197 ReflogEntry log = lc.getReflogEntry();
198 if (log == null) {
199 if (includeDeletes) {
200 writer.deleteLog(refName, updateIndex);
201 }
202 continue;
203 }
204
205 PersonIdent who = log.getWho();
206 if (who.getWhen().getTime() >= reflogExpireOldestReflogTimeMillis) {
207 writer.writeLog(
208 refName,
209 updateIndex,
210 who,
211 log.getOldId(),
212 log.getNewId(),
213 log.getComment());
214 }
215 }
216 }
217 }
218 }