3 * Copyright (C) 2009-2022 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.ce.task.projectanalysis.step;
22 import java.util.ArrayList;
23 import java.util.List;
25 import org.sonar.api.utils.System2;
26 import org.sonar.ce.task.projectanalysis.issue.ProtoIssueCache;
27 import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
28 import org.sonar.ce.task.projectanalysis.issue.UpdateConflictResolver;
29 import org.sonar.ce.task.step.ComputationStep;
30 import org.sonar.core.issue.DefaultIssue;
31 import org.sonar.core.util.CloseableIterator;
32 import org.sonar.core.util.UuidFactory;
33 import org.sonar.db.BatchSession;
34 import org.sonar.db.DbClient;
35 import org.sonar.db.DbSession;
36 import org.sonar.db.issue.IssueChangeMapper;
37 import org.sonar.db.issue.IssueDto;
38 import org.sonar.db.issue.IssueMapper;
39 import org.sonar.db.issue.NewCodeReferenceIssueDto;
40 import org.sonar.server.issue.IssueStorage;
42 import static org.sonar.core.util.FileUtils.humanReadableByteCountSI;
43 import static org.sonar.core.util.stream.MoreCollectors.toList;
44 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
46 public class PersistIssuesStep implements ComputationStep {
47 // holding up to 1000 DefaultIssue (max size of addedIssues and updatedIssues at any given time) in memory should not
48 // be a problem while making sure we leverage extensively the batch feature to speed up persistence
49 private static final int ISSUE_BATCHING_SIZE = BatchSession.MAX_BATCH_SIZE * 2;
51 private final DbClient dbClient;
52 private final System2 system2;
53 private final UpdateConflictResolver conflictResolver;
54 private final RuleRepository ruleRepository;
55 private final ProtoIssueCache protoIssueCache;
56 private final IssueStorage issueStorage;
57 private final UuidFactory uuidFactory;
59 public PersistIssuesStep(DbClient dbClient, System2 system2, UpdateConflictResolver conflictResolver,
60 RuleRepository ruleRepository, ProtoIssueCache protoIssueCache, IssueStorage issueStorage, UuidFactory uuidFactory) {
61 this.dbClient = dbClient;
62 this.system2 = system2;
63 this.conflictResolver = conflictResolver;
64 this.ruleRepository = ruleRepository;
65 this.protoIssueCache = protoIssueCache;
66 this.issueStorage = issueStorage;
67 this.uuidFactory = uuidFactory;
71 public void execute(ComputationStep.Context context) {
72 context.getStatistics().add("cacheSize", humanReadableByteCountSI(protoIssueCache.fileSize()));
73 IssueStatistics statistics = new IssueStatistics();
74 try (DbSession dbSession = dbClient.openSession(true);
75 CloseableIterator<DefaultIssue> issues = protoIssueCache.traverse()) {
76 List<DefaultIssue> addedIssues = new ArrayList<>(ISSUE_BATCHING_SIZE);
77 List<DefaultIssue> updatedIssues = new ArrayList<>(ISSUE_BATCHING_SIZE);
78 List<DefaultIssue> noLongerNewIssues = new ArrayList<>(ISSUE_BATCHING_SIZE);
80 IssueMapper mapper = dbSession.getMapper(IssueMapper.class);
81 IssueChangeMapper changeMapper = dbSession.getMapper(IssueChangeMapper.class);
82 while (issues.hasNext()) {
83 DefaultIssue issue = issues.next();
84 if (issue.isNew() || issue.isCopied()) {
85 addedIssues.add(issue);
86 if (addedIssues.size() >= ISSUE_BATCHING_SIZE) {
87 persistNewIssues(statistics, addedIssues, mapper, changeMapper);
90 } else if (issue.isChanged()) {
91 updatedIssues.add(issue);
92 if (updatedIssues.size() >= ISSUE_BATCHING_SIZE) {
93 persistUpdatedIssues(statistics, updatedIssues, mapper, changeMapper);
94 updatedIssues.clear();
96 } else if (issue.isNoLongerNewCodeReferenceIssue()) {
97 noLongerNewIssues.add(issue);
98 if (noLongerNewIssues.size() >= ISSUE_BATCHING_SIZE) {
99 persistNoLongerNewIssues(statistics, noLongerNewIssues, mapper);
100 noLongerNewIssues.clear();
104 persistNewIssues(statistics, addedIssues, mapper, changeMapper);
105 persistUpdatedIssues(statistics, updatedIssues, mapper, changeMapper);
106 persistNoLongerNewIssues(statistics, noLongerNewIssues, mapper);
107 flushSession(dbSession);
109 statistics.dumpTo(context);
113 private void persistNewIssues(IssueStatistics statistics, List<DefaultIssue> addedIssues, IssueMapper mapper, IssueChangeMapper changeMapper) {
114 if (addedIssues.isEmpty()) {
118 long now = system2.now();
119 addedIssues.forEach(i -> {
120 String ruleUuid = ruleRepository.getByKey(i.ruleKey()).getUuid();
121 IssueDto dto = IssueDto.toDtoForComputationInsert(i, ruleUuid, now);
123 if (i.isOnReferencedBranch() && i.isOnChangedLine()) {
124 mapper.insertAsNewCodeOnReferenceBranch(NewCodeReferenceIssueDto.fromIssueDto(dto, now, uuidFactory));
126 statistics.inserts++;
129 addedIssues.forEach(i -> issueStorage.insertChanges(changeMapper, i, uuidFactory));
132 private void persistUpdatedIssues(IssueStatistics statistics, List<DefaultIssue> updatedIssues, IssueMapper mapper, IssueChangeMapper changeMapper) {
133 if (updatedIssues.isEmpty()) {
137 long now = system2.now();
138 updatedIssues.forEach(i -> {
139 IssueDto dto = IssueDto.toDtoForUpdate(i, now);
140 mapper.updateIfBeforeSelectedDate(dto);
141 statistics.updates++;
144 // retrieve those of the updatedIssues which have not been updated and apply conflictResolver on them
145 List<String> updatedIssueKeys = updatedIssues.stream().map(DefaultIssue::key).collect(toList(updatedIssues.size()));
146 List<IssueDto> conflictIssueKeys = mapper.selectByKeysIfNotUpdatedAt(updatedIssueKeys, now);
147 if (!conflictIssueKeys.isEmpty()) {
148 Map<String, DefaultIssue> issuesByKeys = updatedIssues.stream().collect(uniqueIndex(DefaultIssue::key, updatedIssues.size()));
150 .forEach(dbIssue -> {
151 DefaultIssue updatedIssue = issuesByKeys.get(dbIssue.getKey());
152 conflictResolver.resolve(updatedIssue, dbIssue, mapper);
157 updatedIssues.forEach(i -> issueStorage.insertChanges(changeMapper, i, uuidFactory));
160 private static void persistNoLongerNewIssues(IssueStatistics statistics, List<DefaultIssue> noLongerNewIssues, IssueMapper mapper) {
161 if (noLongerNewIssues.isEmpty()) {
165 noLongerNewIssues.forEach(i -> {
166 mapper.deleteAsNewCodeOnReferenceBranch(i.key());
167 statistics.updates++;
172 private static void flushSession(DbSession dbSession) {
173 dbSession.flushStatements();
178 public String getDescription() {
179 return "Persist issues";
182 private static class IssueStatistics {
183 private int inserts = 0;
184 private int updates = 0;
185 private int merged = 0;
187 private void dumpTo(ComputationStep.Context context) {
188 context.getStatistics()
189 .add("inserts", String.valueOf(inserts))
190 .add("updates", String.valueOf(updates))
191 .add("merged", String.valueOf(merged));