Line data Source code
1 : // Copyright (C) 2016 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.notedb;
16 :
17 : import com.google.auto.value.AutoValue;
18 : import com.google.common.annotations.VisibleForTesting;
19 : import com.google.common.cache.Cache;
20 : import com.google.common.collect.Table;
21 : import com.google.common.flogger.FluentLogger;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.entities.Change;
24 : import com.google.gerrit.entities.Project;
25 : import com.google.gerrit.entities.RefNames;
26 : import com.google.gerrit.proto.Protos;
27 : import com.google.gerrit.server.ReviewerByEmailSet;
28 : import com.google.gerrit.server.ReviewerSet;
29 : import com.google.gerrit.server.account.externalids.ExternalIdCache;
30 : import com.google.gerrit.server.cache.CacheModule;
31 : import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
32 : import com.google.gerrit.server.cache.serialize.CacheSerializer;
33 : import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
34 : import com.google.gerrit.server.notedb.AbstractChangeNotes.Args;
35 : import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
36 : import com.google.inject.Inject;
37 : import com.google.inject.Module;
38 : import com.google.inject.Singleton;
39 : import com.google.inject.name.Named;
40 : import java.io.IOException;
41 : import java.util.List;
42 : import java.util.Map;
43 : import java.util.Set;
44 : import java.util.concurrent.Callable;
45 : import java.util.concurrent.ExecutionException;
46 : import java.util.function.Supplier;
47 : import org.eclipse.jgit.errors.ConfigInvalidException;
48 : import org.eclipse.jgit.lib.ObjectId;
49 :
50 : @Singleton
51 : public class ChangeNotesCache {
52 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
53 :
54 : @VisibleForTesting static final String CACHE_NAME = "change_notes";
55 :
56 : public static Module module() {
57 152 : return new CacheModule() {
58 : @Override
59 : protected void configure() {
60 152 : bind(ChangeNotesCache.class);
61 152 : persist(CACHE_NAME, Key.class, ChangeNotesState.class)
62 152 : .weigher(Weigher.class)
63 152 : .maximumWeight(10 << 20)
64 152 : .diskLimit(-1)
65 152 : .version(5)
66 152 : .keySerializer(Key.Serializer.INSTANCE)
67 152 : .valueSerializer(ChangeNotesState.Serializer.INSTANCE);
68 152 : }
69 : };
70 : }
71 :
72 : @AutoValue
73 103 : public abstract static class Key {
74 : static Key create(Project.NameKey project, Change.Id changeId, ObjectId id) {
75 103 : return new AutoValue_ChangeNotesCache_Key(project, changeId, id.copy());
76 : }
77 :
78 : abstract Project.NameKey project();
79 :
80 : abstract Change.Id changeId();
81 :
82 : abstract ObjectId id();
83 :
84 152 : @VisibleForTesting
85 : enum Serializer implements CacheSerializer<Key> {
86 152 : INSTANCE;
87 :
88 : @Override
89 : public byte[] serialize(Key object) {
90 1 : return Protos.toByteArray(
91 1 : ChangeNotesKeyProto.newBuilder()
92 1 : .setProject(object.project().get())
93 1 : .setChangeId(object.changeId().get())
94 1 : .setId(ObjectIdConverter.create().toByteString(object.id()))
95 1 : .build());
96 : }
97 :
98 : @Override
99 : public Key deserialize(byte[] in) {
100 1 : ChangeNotesKeyProto proto = Protos.parseUnchecked(ChangeNotesKeyProto.parser(), in);
101 1 : return Key.create(
102 1 : Project.nameKey(proto.getProject()),
103 1 : Change.id(proto.getChangeId()),
104 1 : ObjectIdConverter.create().fromByteString(proto.getId()));
105 : }
106 : }
107 : }
108 :
109 152 : public static class Weigher implements com.google.common.cache.Weigher<Key, ChangeNotesState> {
110 : // Single object overhead.
111 : private static final int O = 16;
112 :
113 : // Single pointer overhead.
114 : private static final int P = 8;
115 :
116 : // Single int overhead.
117 : private static final int I = 4;
118 :
119 : // Single IntKey overhead.
120 : private static final int K = O + I;
121 :
122 : // Single Timestamp overhead.
123 : private static final int T = O + 8;
124 :
125 : /**
126 : * {@inheritDoc}
127 : *
128 : * <p>Take all columns and all collection sizes into account, but use estimated average element
129 : * sizes rather than iterating over collections. Numbers are largely hand-wavy based on
130 : * http://stackoverflow.com/questions/258120/what-is-the-memory-consumption-of-an-object-in-java
131 : *
132 : * <p>Should be kept up to date with {@link ChangeNotesState}. Please, keep weights listed in
133 : * the same order as fields.
134 : */
135 : @Override
136 : public int weigh(Key key, ChangeNotesState state) {
137 103 : return P
138 : + O
139 : + 20 // metaId
140 : + K // changeId
141 103 : + str(40) // changeKey
142 : + T // createdOn
143 : + T // lastUpdatedOn
144 : + P
145 : + K // owner
146 : + P
147 103 : + str(state.columns().branch())
148 : + P // status
149 : + P
150 103 : + patchSetId() // currentPatchSetId
151 : + P
152 103 : + str(state.columns().subject())
153 : + P
154 103 : + str(state.columns().topic())
155 : + P
156 103 : + str(state.columns().originalSubject())
157 : + P
158 103 : + str(state.columns().submissionId())
159 : + 1 // isPrivate
160 : + 1 // workInProgress
161 : + 1 // reviewStarted
162 : + P
163 : + K // revertOf
164 : + P
165 103 : + patchSetId() // cherryPickOf
166 : + P
167 103 : + set(state.hashtags(), str(10))
168 103 : + str(state.serverId()) // serverId
169 : + P
170 103 : + list(state.patchSets(), patchSet())
171 : + P
172 103 : + reviewerSet(state.reviewers(), 2) // REVIEWER or CC
173 : + P
174 103 : + reviewerSet(state.reviewersByEmail(), 2) // REVIEWER or CC
175 : + P
176 103 : + reviewerSet(state.pendingReviewers(), 3) // includes REMOVED
177 : + P
178 103 : + reviewerSet(state.pendingReviewersByEmail(), 3) // includes REMOVED
179 : + P
180 103 : + list(state.allPastReviewers(), approval())
181 : + P
182 103 : + list(state.reviewerUpdates(), 4 * O + K + K + P)
183 : + P
184 103 : + list(state.assigneeUpdates(), 4 * O + K + K)
185 : + P
186 103 : + set(state.attentionSet(), 4 * O + K + I + str(15))
187 : + P
188 103 : + list(state.allAttentionSetUpdates(), 4 * O + K + I + str(15))
189 : + P
190 103 : + list(state.submitRecords(), P + list(2, str(4) + P + K) + P)
191 : + P
192 103 : + list(state.changeMessages(), changeMessage())
193 : + P
194 103 : + map(state.publishedComments().asMap(), comment())
195 : + I // updateCount
196 : + T; // mergedOn
197 : }
198 :
199 : private static int str(String s) {
200 103 : if (s == null) {
201 103 : return P;
202 : }
203 103 : return str(s.length());
204 : }
205 :
206 : private static int str(int n) {
207 103 : return 8 + 24 + 2 * n;
208 : }
209 :
210 : private static int patchSetId() {
211 103 : return O + 4 + O + 4;
212 : }
213 :
214 : private static int set(Set<?> set, int elemSize) {
215 103 : if (set == null) {
216 0 : return P;
217 : }
218 103 : return hashtable(set.size(), elemSize);
219 : }
220 :
221 : private static int map(Map<?, ?> map, int elemSize) {
222 103 : if (map == null) {
223 0 : return P;
224 : }
225 103 : return hashtable(map.size(), elemSize);
226 : }
227 :
228 : private static int hashtable(int n, int elemSize) {
229 : // Made up numbers.
230 103 : int overhead = 32;
231 103 : int elemOverhead = O + 32;
232 103 : return overhead + elemOverhead * n * elemSize;
233 : }
234 :
235 : private static int list(List<?> list, int elemSize) {
236 103 : if (list == null) {
237 0 : return P;
238 : }
239 103 : return list(list.size(), elemSize);
240 : }
241 :
242 : private static int list(int n, int elemSize) {
243 103 : return O + O + n * (P + elemSize);
244 : }
245 :
246 : private static int hashBasedTable(
247 : Table<?, ?, ?> table, int numRows, int rowKey, int columnKey, int elemSize) {
248 103 : return O
249 103 : + hashtable(numRows, rowKey + hashtable(0, 0))
250 103 : + hashtable(table.size(), columnKey + elemSize);
251 : }
252 :
253 : private static int reviewerSet(ReviewerSet reviewers, int numRows) {
254 103 : final int rowKey = 1; // ReviewerStateInternal
255 103 : final int columnKey = K; // Account.Id
256 103 : final int cellValue = T; // Timestamp
257 103 : return hashBasedTable(reviewers.asTable(), numRows, rowKey, columnKey, cellValue);
258 : }
259 :
260 : private static int reviewerSet(ReviewerByEmailSet reviewers, int numRows) {
261 103 : final int rowKey = 1; // ReviewerStateInternal
262 103 : final int columnKey = P + 2 * str(20); // name and email, just a guess
263 103 : final int cellValue = T; // Timestamp
264 103 : return hashBasedTable(reviewers.asTable(), numRows, rowKey, columnKey, cellValue);
265 : }
266 :
267 : private static int patchSet() {
268 103 : return O
269 : + P
270 103 : + patchSetId()
271 103 : + str(40) // revision
272 : + P
273 : + K // uploader
274 : + P
275 : + T // createdOn
276 : + 1 // draft
277 103 : + str(40) // groups
278 : + P; // pushCertificate
279 : }
280 :
281 : private static int approval() {
282 103 : return O
283 : + P
284 103 : + patchSetId()
285 : + P
286 : + K
287 : + P
288 : + O
289 103 : + str(10)
290 : + 2 // value
291 : + P
292 : + T // granted
293 : + P // tag
294 : + P; // realAccountId
295 : }
296 :
297 : private static int changeMessage() {
298 103 : int key = K + str(20);
299 103 : return O
300 : + P
301 : + key
302 : + P
303 : + K // author
304 : + P
305 : + T // writtenON
306 103 : + str(64) // message
307 : + P
308 103 : + patchSetId()
309 : + P
310 : + P; // realAuthor
311 : }
312 :
313 : private static int comment() {
314 103 : int key = P + str(20) + P + str(32) + 4;
315 103 : int ident = O + 4;
316 103 : return O
317 : + P
318 : + key
319 : + 4 // lineNbr
320 : + P
321 : + ident // author
322 : + P
323 : + ident // realAuthor
324 : + P
325 : + T // writtenOn
326 : + 2 // side
327 103 : + str(32) // message
328 103 : + str(10) // parentUuid
329 : + (P + O + 4 + 4 + 4 + 4) / 2 // range on 50% of comments
330 : + P // tag
331 : + P
332 103 : + str(40) // revId
333 : + P
334 103 : + str(36); // serverId
335 : }
336 : }
337 :
338 : @AutoValue
339 103 : abstract static class Value {
340 : abstract ChangeNotesState state();
341 :
342 : /**
343 : * The {@link RevisionNoteMap} produced while parsing this change.
344 : *
345 : * <p>These instances are mutable and non-threadsafe, so it is only safe to return it to the
346 : * caller that actually incurred the cache miss. It is only used as an optimization; {@link
347 : * ChangeNotes} is capable of lazily loading it as necessary.
348 : */
349 : @Nullable
350 : abstract RevisionNoteMap<ChangeRevisionNote> revisionNoteMap();
351 : }
352 :
353 : private class Loader implements Callable<ChangeNotesState> {
354 : private final Key key;
355 : private final Supplier<ChangeNotesRevWalk> walkSupplier;
356 :
357 : private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
358 :
359 103 : private Loader(Key key, Supplier<ChangeNotesRevWalk> walkSupplier) {
360 103 : this.key = key;
361 103 : this.walkSupplier = walkSupplier;
362 103 : }
363 :
364 : @Override
365 : public ChangeNotesState call() throws ConfigInvalidException, IOException {
366 103 : logger.atFine().log(
367 103 : "Load change notes for change %s of project %s", key.changeId(), key.project());
368 103 : ChangeNotesParser parser =
369 : new ChangeNotesParser(
370 103 : key.changeId(),
371 103 : key.id(),
372 103 : walkSupplier.get(),
373 : args.changeNoteJson,
374 : args.metrics,
375 : args.serverId,
376 : externalIdCache);
377 103 : ChangeNotesState result = parser.parseAll();
378 : // This assignment only happens if call() was actually called, which only
379 : // happens when Cache#get(K, Callable<V>) incurs a cache miss.
380 103 : revisionNoteMap = parser.getRevisionNoteMap();
381 103 : return result;
382 : }
383 : }
384 :
385 : private final Cache<Key, ChangeNotesState> cache;
386 : private final Args args;
387 : private final ExternalIdCache externalIdCache;
388 :
389 : @Inject
390 : ChangeNotesCache(
391 : @Named(CACHE_NAME) Cache<Key, ChangeNotesState> cache,
392 : Args args,
393 146 : ExternalIdCache externalIdCache) {
394 146 : this.cache = cache;
395 146 : this.args = args;
396 146 : this.externalIdCache = externalIdCache;
397 146 : }
398 :
399 : Value get(
400 : Project.NameKey project,
401 : Change.Id changeId,
402 : ObjectId metaId,
403 : Supplier<ChangeNotesRevWalk> walkSupplier)
404 : throws IOException {
405 : try {
406 103 : Key key = Key.create(project, changeId, metaId);
407 103 : Loader loader = new Loader(key, walkSupplier);
408 103 : ChangeNotesState s = cache.get(key, loader);
409 103 : return new AutoValue_ChangeNotesCache_Value(s, loader.revisionNoteMap);
410 1 : } catch (ExecutionException e) {
411 1 : throw new IOException(
412 1 : String.format(
413 : "Error loading %s in %s at %s",
414 1 : RefNames.changeMetaRef(changeId), project, metaId.name()),
415 : e);
416 : }
417 : }
418 : }
|