View Javadoc
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 }