Line data Source code
1 : // Copyright (C) 2017 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.update;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.base.Preconditions.checkState;
19 : import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
20 : import static com.google.common.flogger.LazyArgs.lazy;
21 : import static java.util.Comparator.comparing;
22 : import static java.util.Objects.requireNonNull;
23 : import static java.util.stream.Collectors.toMap;
24 : import static java.util.stream.Collectors.toSet;
25 :
26 : import com.google.common.base.Throwables;
27 : import com.google.common.collect.ArrayListMultimap;
28 : import com.google.common.collect.ImmutableList;
29 : import com.google.common.collect.ImmutableListMultimap;
30 : import com.google.common.collect.ImmutableMap;
31 : import com.google.common.collect.ListMultimap;
32 : import com.google.common.collect.MultimapBuilder;
33 : import com.google.common.collect.Multiset;
34 : import com.google.common.flogger.FluentLogger;
35 : import com.google.common.util.concurrent.Futures;
36 : import com.google.common.util.concurrent.ListenableFuture;
37 : import com.google.gerrit.common.Nullable;
38 : import com.google.gerrit.entities.AttentionSetUpdate;
39 : import com.google.gerrit.entities.BranchNameKey;
40 : import com.google.gerrit.entities.Change;
41 : import com.google.gerrit.entities.PatchSet;
42 : import com.google.gerrit.entities.Project;
43 : import com.google.gerrit.entities.ProjectChangeKey;
44 : import com.google.gerrit.entities.RefNames;
45 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
46 : import com.google.gerrit.extensions.config.FactoryModule;
47 : import com.google.gerrit.extensions.restapi.BadRequestException;
48 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
49 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
50 : import com.google.gerrit.extensions.restapi.RestApiException;
51 : import com.google.gerrit.server.CurrentUser;
52 : import com.google.gerrit.server.GerritPersonIdent;
53 : import com.google.gerrit.server.account.AccountState;
54 : import com.google.gerrit.server.change.NotifyResolver;
55 : import com.google.gerrit.server.extensions.events.AttentionSetObserver;
56 : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
57 : import com.google.gerrit.server.git.GitRepositoryManager;
58 : import com.google.gerrit.server.git.validators.OnSubmitValidators;
59 : import com.google.gerrit.server.index.change.ChangeIndexer;
60 : import com.google.gerrit.server.logging.Metadata;
61 : import com.google.gerrit.server.logging.RequestId;
62 : import com.google.gerrit.server.logging.TraceContext;
63 : import com.google.gerrit.server.notedb.ChangeNotes;
64 : import com.google.gerrit.server.notedb.ChangeUpdate;
65 : import com.google.gerrit.server.notedb.LimitExceededException;
66 : import com.google.gerrit.server.notedb.NoteDbUpdateManager;
67 : import com.google.gerrit.server.project.InvalidChangeOperationException;
68 : import com.google.gerrit.server.project.NoSuchChangeException;
69 : import com.google.gerrit.server.project.NoSuchProjectException;
70 : import com.google.gerrit.server.project.NoSuchRefException;
71 : import com.google.gerrit.server.query.change.ChangeData;
72 : import com.google.inject.Inject;
73 : import com.google.inject.Module;
74 : import com.google.inject.assistedinject.Assisted;
75 : import java.io.IOException;
76 : import java.time.Instant;
77 : import java.time.ZoneId;
78 : import java.util.ArrayList;
79 : import java.util.Collection;
80 : import java.util.HashMap;
81 : import java.util.List;
82 : import java.util.Map;
83 : import java.util.Objects;
84 : import java.util.Optional;
85 : import java.util.TreeMap;
86 : import java.util.function.Function;
87 : import org.eclipse.jgit.lib.BatchRefUpdate;
88 : import org.eclipse.jgit.lib.ObjectInserter;
89 : import org.eclipse.jgit.lib.PersonIdent;
90 : import org.eclipse.jgit.lib.Repository;
91 : import org.eclipse.jgit.revwalk.RevWalk;
92 : import org.eclipse.jgit.transport.PushCertificate;
93 : import org.eclipse.jgit.transport.ReceiveCommand;
94 : import org.eclipse.jgit.transport.ReceiveCommand.Result;
95 :
96 : /**
97 : * Helper for a set of change updates that should be applied to the NoteDb database.
98 : *
99 : * <p>An update operation can be divided into three phases:
100 : *
101 : * <ol>
102 : * <li>Git reference updates
103 : * <li>Review metadata updates
104 : * <li>Post-update steps
105 : * <li>
106 : * </ol>
107 : *
108 : * A single conceptual operation, such as a REST API call or a merge operation, may make multiple
109 : * changes at each step, which all need to be serialized relative to each other. Moreover, for
110 : * consistency, the git ref updates must be visible to the review metadata updates, since for
111 : * example the metadata might refer to newly-created patch set refs. In NoteDb, this is accomplished
112 : * by combining these two phases into a single {@link BatchRefUpdate}.
113 : *
114 : * <p>Similarly, all post-update steps, such as sending email, must run only after all storage
115 : * mutations have completed.
116 : */
117 : public class BatchUpdate implements AutoCloseable {
118 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
119 :
120 : public static Module module() {
121 152 : return new FactoryModule() {
122 : @Override
123 : public void configure() {
124 152 : factory(BatchUpdate.Factory.class);
125 152 : }
126 : };
127 : }
128 :
129 : public interface Factory {
130 : BatchUpdate create(Project.NameKey project, CurrentUser user, Instant when);
131 : }
132 :
133 : public static void execute(
134 : Collection<BatchUpdate> updates, ImmutableList<BatchUpdateListener> listeners, boolean dryrun)
135 : throws UpdateException, RestApiException {
136 110 : requireNonNull(listeners);
137 110 : if (updates.isEmpty()) {
138 68 : return;
139 : }
140 :
141 110 : checkDifferentProject(updates);
142 :
143 : try {
144 110 : List<ListenableFuture<ChangeData>> indexFutures = new ArrayList<>();
145 110 : List<ChangesHandle> changesHandles = new ArrayList<>(updates.size());
146 : try {
147 110 : for (BatchUpdate u : updates) {
148 110 : u.executeUpdateRepo();
149 110 : }
150 110 : notifyAfterUpdateRepo(listeners);
151 110 : for (BatchUpdate u : updates) {
152 110 : changesHandles.add(u.executeChangeOps(listeners, dryrun));
153 110 : }
154 110 : for (ChangesHandle h : changesHandles) {
155 110 : h.execute();
156 110 : if (h.requiresReindex()) {
157 109 : indexFutures.addAll(h.startIndexFutures());
158 : }
159 110 : }
160 110 : notifyAfterUpdateRefs(listeners);
161 110 : notifyAfterUpdateChanges(listeners);
162 : } finally {
163 110 : for (ChangesHandle h : changesHandles) {
164 110 : h.close();
165 110 : }
166 : }
167 :
168 110 : Map<Change.Id, ChangeData> changeDatas =
169 110 : Futures.allAsList(indexFutures).get().stream()
170 : // filter out null values that were returned for change deletions
171 110 : .filter(Objects::nonNull)
172 110 : .collect(toMap(cd -> cd.change().getId(), Function.identity()));
173 :
174 : // Fire ref update events only after all mutations are finished, since callers may assume a
175 : // patch set ref being created means the change was created, or a branch advancing meaning
176 : // some changes were closed.
177 110 : updates.forEach(BatchUpdate::fireRefChangeEvent);
178 :
179 110 : if (!dryrun) {
180 110 : for (BatchUpdate u : updates) {
181 110 : u.executePostOps(changeDatas);
182 110 : }
183 : }
184 26 : } catch (Exception e) {
185 0 : wrapAndThrowException(e);
186 110 : }
187 110 : }
188 :
189 : private static void notifyAfterUpdateRepo(ImmutableList<BatchUpdateListener> listeners)
190 : throws Exception {
191 110 : for (BatchUpdateListener listener : listeners) {
192 53 : listener.afterUpdateRepos();
193 53 : }
194 110 : }
195 :
196 : private static void notifyAfterUpdateRefs(ImmutableList<BatchUpdateListener> listeners)
197 : throws Exception {
198 110 : for (BatchUpdateListener listener : listeners) {
199 53 : listener.afterUpdateRefs();
200 53 : }
201 110 : }
202 :
203 : private static void notifyAfterUpdateChanges(ImmutableList<BatchUpdateListener> listeners)
204 : throws Exception {
205 110 : for (BatchUpdateListener listener : listeners) {
206 53 : listener.afterUpdateChanges();
207 53 : }
208 110 : }
209 :
210 : private static void checkDifferentProject(Collection<BatchUpdate> updates) {
211 110 : Multiset<Project.NameKey> projectCounts =
212 110 : updates.stream().map(u -> u.project).collect(toImmutableMultiset());
213 110 : checkArgument(
214 110 : projectCounts.entrySet().size() == updates.size(),
215 : "updates must all be for different projects, got: %s",
216 : projectCounts);
217 110 : }
218 :
219 : private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
220 : // Convert common non-REST exception types with user-visible messages to corresponding REST
221 : // exception types.
222 26 : if (e instanceof InvalidChangeOperationException || e instanceof LimitExceededException) {
223 1 : throw new ResourceConflictException(e.getMessage(), e);
224 26 : } else if (e instanceof NoSuchChangeException
225 : || e instanceof NoSuchRefException
226 : || e instanceof NoSuchProjectException) {
227 0 : throw new ResourceNotFoundException(e.getMessage(), e);
228 26 : } else if (e instanceof CommentsRejectedException) {
229 : // SC_BAD_REQUEST is not ideal because it's not a syntactic error, but there is no better
230 : // status code and it's isolated in monitoring.
231 3 : throw new BadRequestException(e.getMessage(), e);
232 : }
233 :
234 24 : Throwables.throwIfUnchecked(e);
235 :
236 : // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
237 : // ResourceConflictException to indicate an atomic update failure.
238 24 : Throwables.throwIfInstanceOf(e, UpdateException.class);
239 12 : Throwables.throwIfInstanceOf(e, RestApiException.class);
240 :
241 : // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
242 12 : throw new UpdateException(e);
243 : }
244 :
245 110 : class ContextImpl implements Context {
246 : @Override
247 : public RepoView getRepoView() throws IOException {
248 109 : return BatchUpdate.this.getRepoView();
249 : }
250 :
251 : @Override
252 : public RevWalk getRevWalk() throws IOException {
253 103 : return getRepoView().getRevWalk();
254 : }
255 :
256 : @Override
257 : public Project.NameKey getProject() {
258 103 : return project;
259 : }
260 :
261 : @Override
262 : public Instant getWhen() {
263 103 : return when;
264 : }
265 :
266 : @Override
267 : public ZoneId getZoneId() {
268 17 : return zoneId;
269 : }
270 :
271 : @Override
272 : public CurrentUser getUser() {
273 103 : return user;
274 : }
275 :
276 : @Override
277 : public NotifyResolver.Result getNotify(Change.Id changeId) {
278 103 : NotifyHandling notifyHandling = perChangeNotifyHandling.get(changeId);
279 103 : return notifyHandling != null ? notify.withHandling(notifyHandling) : notify;
280 : }
281 : }
282 :
283 110 : private class RepoContextImpl extends ContextImpl implements RepoContext {
284 : @Override
285 : public ObjectInserter getInserter() throws IOException {
286 103 : return getRepoView().getInserterWrapper();
287 : }
288 :
289 : @Override
290 : public void addRefUpdate(ReceiveCommand cmd) throws IOException {
291 109 : getRepoView().getCommands().add(cmd);
292 109 : }
293 : }
294 :
295 : private class ChangeContextImpl extends ContextImpl implements ChangeContext {
296 : private final ChangeNotes notes;
297 :
298 : /**
299 : * Updates where the caller instructed us to create one NoteDb commit per update. Keyed by
300 : * PatchSet.Id only for convenience.
301 : */
302 : private final Map<PatchSet.Id, ChangeUpdate> defaultUpdates;
303 :
304 : /**
305 : * Updates where the caller allowed us to combine potentially multiple adjustments into a single
306 : * commit in NoteDb by re-using the same ChangeUpdate instance. Will still be one commit per
307 : * patch set.
308 : */
309 : private final ListMultimap<PatchSet.Id, ChangeUpdate> distinctUpdates;
310 :
311 : private boolean deleted;
312 :
313 103 : ChangeContextImpl(ChangeNotes notes) {
314 103 : this.notes = requireNonNull(notes);
315 103 : defaultUpdates = new TreeMap<>(comparing(PatchSet.Id::get));
316 103 : distinctUpdates = ArrayListMultimap.create();
317 103 : }
318 :
319 : @Override
320 : public ChangeUpdate getUpdate(PatchSet.Id psId) {
321 103 : ChangeUpdate u = defaultUpdates.get(psId);
322 103 : if (u == null) {
323 103 : u = getNewChangeUpdate(psId);
324 103 : defaultUpdates.put(psId, u);
325 : }
326 103 : return u;
327 : }
328 :
329 : @Override
330 : public ChangeUpdate getDistinctUpdate(PatchSet.Id psId) {
331 4 : ChangeUpdate u = getNewChangeUpdate(psId);
332 4 : distinctUpdates.put(psId, u);
333 4 : return u;
334 : }
335 :
336 : private ChangeUpdate getNewChangeUpdate(PatchSet.Id psId) {
337 103 : ChangeUpdate u = changeUpdateFactory.create(notes, user, when);
338 103 : if (newChanges.containsKey(notes.getChangeId())) {
339 103 : u.setAllowWriteToNewRef(true);
340 : }
341 103 : u.setPatchSetId(psId);
342 103 : return u;
343 : }
344 :
345 : @Override
346 : public ChangeNotes getNotes() {
347 103 : return notes;
348 : }
349 :
350 : @Override
351 : public void deleteChange() {
352 11 : deleted = true;
353 11 : }
354 : }
355 :
356 : private class PostUpdateContextImpl extends ContextImpl implements PostUpdateContext {
357 : private final Map<Change.Id, ChangeData> changeDatas;
358 :
359 110 : PostUpdateContextImpl(Map<Change.Id, ChangeData> changeDatas) {
360 110 : this.changeDatas = changeDatas;
361 110 : }
362 :
363 : @Override
364 : public ChangeData getChangeData(Project.NameKey projectName, Change.Id changeId) {
365 83 : return changeDatas.computeIfAbsent(
366 0 : changeId, id -> changeDataFactory.create(projectName, changeId));
367 : }
368 :
369 : @Override
370 : public ChangeData getChangeData(Change change) {
371 103 : return changeDatas.computeIfAbsent(change.getId(), id -> changeDataFactory.create(change));
372 : }
373 : }
374 :
375 : /** Per-change result status from {@link #executeChangeOps}. */
376 103 : private enum ChangeResult {
377 103 : SKIPPED,
378 103 : UPSERTED,
379 103 : DELETED
380 : }
381 :
382 : private final GitRepositoryManager repoManager;
383 : private final ChangeData.Factory changeDataFactory;
384 : private final ChangeNotes.Factory changeNotesFactory;
385 : private final ChangeUpdate.Factory changeUpdateFactory;
386 : private final NoteDbUpdateManager.Factory updateManagerFactory;
387 : private final ChangeIndexer indexer;
388 : private final GitReferenceUpdated gitRefUpdated;
389 :
390 : private final Project.NameKey project;
391 : private final CurrentUser user;
392 : private final Instant when;
393 : private final ZoneId zoneId;
394 :
395 110 : private final ListMultimap<Change.Id, BatchUpdateOp> ops =
396 110 : MultimapBuilder.linkedHashKeys().arrayListValues().build();
397 110 : private final Map<Change.Id, Change> newChanges = new HashMap<>();
398 110 : private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
399 110 : private final Map<Change.Id, NotifyHandling> perChangeNotifyHandling = new HashMap<>();
400 :
401 : private RepoView repoView;
402 : private BatchRefUpdate batchRefUpdate;
403 : private ImmutableListMultimap<ProjectChangeKey, AttentionSetUpdate> attentionSetUpdates;
404 :
405 : private boolean executed;
406 : private OnSubmitValidators onSubmitValidators;
407 : private PushCertificate pushCert;
408 : private String refLogMessage;
409 110 : private NotifyResolver.Result notify = NotifyResolver.Result.all();
410 : // Batch operations doesn't need observer
411 : private AttentionSetObserver attentionSetObserver;
412 :
413 : @Inject
414 : BatchUpdate(
415 : GitRepositoryManager repoManager,
416 : @GerritPersonIdent PersonIdent serverIdent,
417 : ChangeData.Factory changeDataFactory,
418 : ChangeNotes.Factory changeNotesFactory,
419 : ChangeUpdate.Factory changeUpdateFactory,
420 : NoteDbUpdateManager.Factory updateManagerFactory,
421 : ChangeIndexer indexer,
422 : GitReferenceUpdated gitRefUpdated,
423 : AttentionSetObserver attentionSetObserver,
424 : @Assisted Project.NameKey project,
425 : @Assisted CurrentUser user,
426 110 : @Assisted Instant when) {
427 110 : this.repoManager = repoManager;
428 110 : this.changeDataFactory = changeDataFactory;
429 110 : this.changeNotesFactory = changeNotesFactory;
430 110 : this.changeUpdateFactory = changeUpdateFactory;
431 110 : this.updateManagerFactory = updateManagerFactory;
432 110 : this.indexer = indexer;
433 110 : this.gitRefUpdated = gitRefUpdated;
434 110 : this.project = project;
435 110 : this.user = user;
436 110 : this.when = when;
437 110 : this.attentionSetObserver = attentionSetObserver;
438 110 : zoneId = serverIdent.getZoneId();
439 110 : }
440 :
441 : @Override
442 : public void close() {
443 110 : if (repoView != null) {
444 110 : repoView.close();
445 : }
446 110 : }
447 :
448 : public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
449 1 : execute(ImmutableList.of(this), ImmutableList.of(listener), false);
450 1 : }
451 :
452 : public void execute() throws UpdateException, RestApiException {
453 108 : execute(ImmutableList.of(this), ImmutableList.of(), false);
454 108 : }
455 :
456 : public boolean isExecuted() {
457 53 : return executed;
458 : }
459 :
460 : public BatchUpdate setRepository(Repository repo, RevWalk revWalk, ObjectInserter inserter) {
461 109 : checkState(this.repoView == null, "repo already set");
462 109 : repoView = new RepoView(repo, revWalk, inserter);
463 109 : return this;
464 : }
465 :
466 : public BatchUpdate setPushCertificate(@Nullable PushCertificate pushCert) {
467 0 : this.pushCert = pushCert;
468 0 : return this;
469 : }
470 :
471 : public BatchUpdate setRefLogMessage(@Nullable String refLogMessage) {
472 101 : this.refLogMessage = refLogMessage;
473 101 : return this;
474 : }
475 :
476 : /**
477 : * Set the default notification settings for all changes in the batch.
478 : *
479 : * @param notify notification settings.
480 : * @return this.
481 : */
482 : public BatchUpdate setNotify(NotifyResolver.Result notify) {
483 102 : this.notify = requireNonNull(notify);
484 102 : return this;
485 : }
486 :
487 : /**
488 : * Override the {@link NotifyHandling} on a per-change basis.
489 : *
490 : * <p>Only the handling enum can be overridden; all changes share the same value for {@link
491 : * com.google.gerrit.server.change.NotifyResolver.Result#accounts()}.
492 : *
493 : * @param changeId change ID.
494 : * @param notifyHandling notify handling.
495 : * @return this.
496 : */
497 : public BatchUpdate setNotifyHandling(Change.Id changeId, NotifyHandling notifyHandling) {
498 37 : this.perChangeNotifyHandling.put(changeId, requireNonNull(notifyHandling));
499 37 : return this;
500 : }
501 :
502 : /**
503 : * Add a validation step for intended ref operations, which will be performed at the end of {@link
504 : * RepoOnlyOp#updateRepo(RepoContext)} step.
505 : */
506 : public BatchUpdate setOnSubmitValidators(OnSubmitValidators onSubmitValidators) {
507 53 : this.onSubmitValidators = onSubmitValidators;
508 53 : return this;
509 : }
510 :
511 : public Project.NameKey getProject() {
512 53 : return project;
513 : }
514 :
515 : private void initRepository() throws IOException {
516 110 : if (repoView == null) {
517 76 : repoView = new RepoView(repoManager, project);
518 : }
519 110 : }
520 :
521 : private RepoView getRepoView() throws IOException {
522 109 : initRepository();
523 109 : return repoView;
524 : }
525 :
526 : private Optional<AccountState> getAccount() {
527 109 : return user.isIdentifiedUser()
528 109 : ? Optional.of(user.asIdentifiedUser().state())
529 1 : : Optional.empty();
530 : }
531 :
532 : public Map<String, ReceiveCommand> getRefUpdates() {
533 69 : return repoView != null ? repoView.getCommands().getCommands() : ImmutableMap.of();
534 : }
535 :
536 : /**
537 : * Return the references successfully updated by this BatchUpdate with their command. In dryrun,
538 : * we assume all updates were successful.
539 : */
540 : public Map<BranchNameKey, ReceiveCommand> getSuccessfullyUpdatedBranches(boolean dryrun) {
541 69 : return getRefUpdates().entrySet().stream()
542 69 : .filter(entry -> dryrun || entry.getValue().getResult() == Result.OK)
543 69 : .collect(
544 69 : toMap(entry -> BranchNameKey.create(project, entry.getKey()), Map.Entry::getValue));
545 : }
546 :
547 : public BatchUpdate addOp(Change.Id id, BatchUpdateOp op) {
548 98 : checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
549 98 : requireNonNull(op);
550 98 : ops.put(id, op);
551 98 : return this;
552 : }
553 :
554 : public BatchUpdate addRepoOnlyOp(RepoOnlyOp op) {
555 66 : checkArgument(!(op instanceof BatchUpdateOp), "use addOp()");
556 66 : repoOnlyOps.add(op);
557 66 : return this;
558 : }
559 :
560 : public BatchUpdate insertChange(InsertChangeOp op) throws IOException {
561 103 : Context ctx = new ContextImpl();
562 103 : Change c = op.createChange(ctx);
563 103 : checkArgument(
564 103 : !newChanges.containsKey(c.getId()), "only one op allowed to create change %s", c.getId());
565 103 : newChanges.put(c.getId(), c);
566 103 : ops.get(c.getId()).add(0, op);
567 103 : return this;
568 : }
569 :
570 : private void executeUpdateRepo() throws UpdateException, RestApiException {
571 : try {
572 110 : logDebug("Executing updateRepo on %d ops", ops.size());
573 110 : RepoContextImpl ctx = new RepoContextImpl();
574 110 : for (Map.Entry<Change.Id, BatchUpdateOp> op : ops.entries()) {
575 103 : try (TraceContext.TraceTimer ignored =
576 103 : TraceContext.newTimer(
577 103 : op.getClass().getSimpleName() + "#updateRepo",
578 103 : Metadata.builder()
579 103 : .projectName(project.get())
580 103 : .changeId(op.getKey().get())
581 103 : .build())) {
582 103 : op.getValue().updateRepo(ctx);
583 : }
584 103 : }
585 :
586 110 : logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
587 110 : for (RepoOnlyOp op : repoOnlyOps) {
588 66 : op.updateRepo(ctx);
589 66 : }
590 :
591 110 : if (onSubmitValidators != null && !getRefUpdates().isEmpty()) {
592 : // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
593 : // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
594 : // first update's executeRefUpdates has finished, hence after first repo's refs have been
595 : // updated, which is too late.
596 53 : onSubmitValidators.validate(
597 53 : project, ctx.getRevWalk().getObjectReader(), repoView.getCommands());
598 : }
599 10 : } catch (Exception e) {
600 1 : Throwables.throwIfInstanceOf(e, RestApiException.class);
601 1 : throw new UpdateException(e);
602 110 : }
603 110 : }
604 :
605 : private void fireRefChangeEvent() {
606 110 : if (batchRefUpdate != null) {
607 109 : gitRefUpdated.fire(project, batchRefUpdate, getAccount().orElse(null));
608 : }
609 110 : }
610 :
611 : private void fireAttentionSetUpdateEvents(PostUpdateContext ctx) {
612 110 : for (ProjectChangeKey key : attentionSetUpdates.keySet()) {
613 51 : ChangeData change = ctx.getChangeData(key.projectName(), key.changeId());
614 51 : AccountState account = ctx.getAccount();
615 51 : for (AttentionSetUpdate update : attentionSetUpdates.get(key)) {
616 51 : attentionSetObserver.fire(change, account, update, ctx.getWhen());
617 51 : }
618 51 : }
619 110 : }
620 :
621 : private class ChangesHandle implements AutoCloseable {
622 : private final NoteDbUpdateManager manager;
623 : private final boolean dryrun;
624 : private final Map<Change.Id, ChangeResult> results;
625 :
626 110 : ChangesHandle(NoteDbUpdateManager manager, boolean dryrun) {
627 110 : this.manager = manager;
628 110 : this.dryrun = dryrun;
629 110 : results = new HashMap<>();
630 110 : }
631 :
632 : @Override
633 : public void close() {
634 110 : manager.close();
635 110 : }
636 :
637 : void setResult(Change.Id id, ChangeResult result) {
638 103 : ChangeResult old = results.putIfAbsent(id, result);
639 103 : checkArgument(old == null, "result for change %s already set: %s", id, old);
640 103 : }
641 :
642 : void execute() throws IOException {
643 110 : BatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
644 110 : BatchUpdate.this.executed = manager.isExecuted();
645 110 : BatchUpdate.this.attentionSetUpdates = manager.attentionSetUpdates();
646 110 : }
647 :
648 : boolean requiresReindex() {
649 : // We do not need to reindex changes if there are no ref updates, or if updated refs
650 : // are all draft comment refs (since draft fields are not stored in the change index).
651 110 : BatchRefUpdate bru = BatchUpdate.this.batchRefUpdate;
652 110 : return !(bru == null
653 109 : || bru.getCommands().isEmpty()
654 109 : || bru.getCommands().stream()
655 110 : .allMatch(cmd -> RefNames.isRefsDraftsComments(cmd.getRefName())));
656 : }
657 :
658 : ImmutableList<ListenableFuture<ChangeData>> startIndexFutures() {
659 109 : if (dryrun) {
660 0 : return ImmutableList.of();
661 : }
662 109 : logDebug("Reindexing %d changes", results.size());
663 109 : ImmutableList.Builder<ListenableFuture<ChangeData>> indexFutures =
664 109 : ImmutableList.builderWithExpectedSize(results.size());
665 109 : for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
666 103 : Change.Id id = e.getKey();
667 103 : switch (e.getValue()) {
668 : case UPSERTED:
669 103 : indexFutures.add(indexer.indexAsync(project, id));
670 103 : break;
671 : case DELETED:
672 11 : indexFutures.add(indexer.deleteAsync(id));
673 11 : break;
674 : case SKIPPED:
675 45 : break;
676 : default:
677 0 : throw new IllegalStateException("unexpected result: " + e.getValue());
678 : }
679 103 : }
680 109 : return indexFutures.build();
681 : }
682 : }
683 :
684 : private ChangesHandle executeChangeOps(
685 : ImmutableList<BatchUpdateListener> batchUpdateListeners, boolean dryrun) throws Exception {
686 110 : logDebug("Executing change ops");
687 110 : initRepository();
688 110 : Repository repo = repoView.getRepository();
689 110 : checkState(
690 110 : repo.getRefDatabase().performsAtomicTransactions(),
691 : "cannot use NoteDb with a repository that does not support atomic batch ref updates: %s",
692 : repo);
693 :
694 110 : ChangesHandle handle =
695 : new ChangesHandle(
696 : updateManagerFactory
697 110 : .create(project)
698 110 : .setBatchUpdateListeners(batchUpdateListeners)
699 110 : .setChangeRepo(
700 110 : repo, repoView.getRevWalk(), repoView.getInserter(), repoView.getCommands()),
701 : dryrun);
702 110 : if (user.isIdentifiedUser()) {
703 110 : handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, zoneId));
704 : }
705 110 : handle.manager.setRefLogMessage(refLogMessage);
706 110 : handle.manager.setPushCertificate(pushCert);
707 110 : for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
708 103 : Change.Id id = e.getKey();
709 103 : ChangeContextImpl ctx = newChangeContext(id);
710 103 : boolean dirty = false;
711 103 : logDebug(
712 : "Applying %d ops for change %s: %s",
713 103 : e.getValue().size(),
714 : id,
715 103 : lazy(() -> e.getValue().stream().map(op -> op.getClass().getName()).collect(toSet())));
716 103 : for (BatchUpdateOp op : e.getValue()) {
717 103 : try (TraceContext.TraceTimer ignored =
718 103 : TraceContext.newTimer(
719 103 : op.getClass().getSimpleName() + "#updateChange",
720 103 : Metadata.builder().projectName(project.get()).changeId(id.get()).build())) {
721 103 : dirty |= op.updateChange(ctx);
722 : }
723 103 : }
724 103 : if (!dirty) {
725 52 : logDebug("No ops reported dirty, short-circuiting");
726 52 : handle.setResult(id, ChangeResult.SKIPPED);
727 52 : continue;
728 : }
729 103 : ctx.defaultUpdates.values().forEach(handle.manager::add);
730 103 : ctx.distinctUpdates.values().forEach(handle.manager::add);
731 103 : if (ctx.deleted) {
732 11 : logDebug("Change %s was deleted", id);
733 11 : handle.manager.deleteChange(id);
734 11 : handle.setResult(id, ChangeResult.DELETED);
735 : } else {
736 103 : handle.setResult(id, ChangeResult.UPSERTED);
737 : }
738 103 : }
739 110 : return handle;
740 : }
741 :
742 : private ChangeContextImpl newChangeContext(Change.Id id) {
743 103 : logDebug("Opening change %s for update", id);
744 103 : Change c = newChanges.get(id);
745 103 : boolean isNew = c != null;
746 103 : if (!isNew) {
747 : // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for
748 : // existence and populating columns from the parsed notes state.
749 : // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way.
750 95 : c = ChangeNotes.Factory.newChange(project, id);
751 : } else {
752 103 : logDebug("Change %s is new", id);
753 : }
754 103 : ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
755 103 : return new ChangeContextImpl(notes);
756 : }
757 :
758 : private void executePostOps(Map<Change.Id, ChangeData> changeDatas) throws Exception {
759 110 : PostUpdateContextImpl ctx = new PostUpdateContextImpl(changeDatas);
760 110 : for (BatchUpdateOp op : ops.values()) {
761 103 : try (TraceContext.TraceTimer ignored =
762 103 : TraceContext.newTimer(op.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
763 103 : op.postUpdate(ctx);
764 : }
765 103 : }
766 :
767 110 : for (RepoOnlyOp op : repoOnlyOps) {
768 66 : try (TraceContext.TraceTimer ignored =
769 66 : TraceContext.newTimer(op.getClass().getSimpleName() + "#postUpdate", Metadata.empty())) {
770 66 : op.postUpdate(ctx);
771 : }
772 66 : }
773 110 : try (TraceContext.TraceTimer ignored =
774 110 : TraceContext.newTimer("fireAttentionSetUpdates#postUpdate", Metadata.empty())) {
775 110 : fireAttentionSetUpdateEvents(ctx);
776 : }
777 110 : }
778 :
779 : private static void logDebug(String msg) {
780 : // Only log if there is a requestId assigned, since those are the
781 : // expensive/complicated requests like MergeOp. Doing it every time would be
782 : // noisy.
783 110 : if (RequestId.isSet()) {
784 101 : logger.atFine().log("%s", msg);
785 : }
786 110 : }
787 :
788 : private static void logDebug(String msg, @Nullable Object arg) {
789 : // Only log if there is a requestId assigned, since those are the
790 : // expensive/complicated requests like MergeOp. Doing it every time would be
791 : // noisy.
792 110 : if (RequestId.isSet()) {
793 101 : logger.atFine().log(msg, arg);
794 : }
795 110 : }
796 :
797 : private static void logDebug(
798 : String msg, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
799 : // Only log if there is a requestId assigned, since those are the
800 : // expensive/complicated requests like MergeOp. Doing it every time would be
801 : // noisy.
802 103 : if (RequestId.isSet()) {
803 93 : logger.atFine().log(msg, arg1, arg2, arg3);
804 : }
805 103 : }
806 : }
|