3 * Copyright (C) 2009-2024 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.LinkedList;
24 import java.util.List;
26 import java.util.function.Function;
27 import java.util.stream.Collectors;
28 import org.sonar.api.utils.System2;
29 import org.sonar.ce.task.projectanalysis.issue.ProtoIssueCache;
30 import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
31 import org.sonar.ce.task.projectanalysis.issue.UpdateConflictResolver;
32 import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
33 import org.sonar.ce.task.step.ComputationStep;
34 import org.sonar.core.issue.DefaultIssue;
35 import org.sonar.core.util.CloseableIterator;
36 import org.sonar.core.util.UuidFactory;
37 import org.sonar.db.BatchSession;
38 import org.sonar.db.DbClient;
39 import org.sonar.db.DbSession;
40 import org.sonar.db.issue.AnticipatedTransitionMapper;
41 import org.sonar.db.issue.IssueChangeMapper;
42 import org.sonar.db.issue.IssueDao;
43 import org.sonar.db.issue.IssueDto;
44 import org.sonar.db.issue.NewCodeReferenceIssueDto;
45 import org.sonar.db.newcodeperiod.NewCodePeriodType;
46 import org.sonar.server.issue.IssueStorage;
48 import static org.sonar.core.util.FileUtils.humanReadableByteCountSI;
50 public class PersistIssuesStep implements ComputationStep {
51 // holding up to 1000 DefaultIssue (max size of addedIssues and updatedIssues at any given time) in memory should not
52 // be a problem while making sure we leverage extensively the batch feature to speed up persistence
53 private static final int ISSUE_BATCHING_SIZE = BatchSession.MAX_BATCH_SIZE * 2;
55 private final DbClient dbClient;
56 private final System2 system2;
57 private final UpdateConflictResolver conflictResolver;
58 private final RuleRepository ruleRepository;
59 private final PeriodHolder periodHolder;
60 private final ProtoIssueCache protoIssueCache;
61 private final IssueStorage issueStorage;
62 private final UuidFactory uuidFactory;
64 public PersistIssuesStep(DbClient dbClient, System2 system2, UpdateConflictResolver conflictResolver,
65 RuleRepository ruleRepository, PeriodHolder periodHolder, ProtoIssueCache protoIssueCache, IssueStorage issueStorage,
66 UuidFactory uuidFactory) {
67 this.dbClient = dbClient;
68 this.system2 = system2;
69 this.conflictResolver = conflictResolver;
70 this.ruleRepository = ruleRepository;
71 this.periodHolder = periodHolder;
72 this.protoIssueCache = protoIssueCache;
73 this.issueStorage = issueStorage;
74 this.uuidFactory = uuidFactory;
78 public void execute(ComputationStep.Context context) {
79 context.getStatistics().add("cacheSize", humanReadableByteCountSI(protoIssueCache.fileSize()));
80 IssueStatistics statistics = new IssueStatistics();
81 try (DbSession dbSession = dbClient.openSession(true);
82 CloseableIterator<DefaultIssue> issues = protoIssueCache.traverse()) {
83 List<DefaultIssue> addedIssues = new ArrayList<>(ISSUE_BATCHING_SIZE);
84 List<DefaultIssue> updatedIssues = new ArrayList<>(ISSUE_BATCHING_SIZE);
85 List<DefaultIssue> noLongerNewIssues = new ArrayList<>(ISSUE_BATCHING_SIZE);
86 List<DefaultIssue> newCodeIssuesToMigrate = new ArrayList<>(ISSUE_BATCHING_SIZE);
87 IssueDao issueDao = dbClient.issueDao();
88 IssueChangeMapper changeMapper = dbSession.getMapper(IssueChangeMapper.class);
89 AnticipatedTransitionMapper anticipatedTransitionMapper = dbSession.getMapper(AnticipatedTransitionMapper.class);
90 while (issues.hasNext()) {
91 DefaultIssue issue = issues.next();
92 if (issue.isNew() || issue.isCopied()) {
93 addedIssues.add(issue);
94 if (addedIssues.size() >= ISSUE_BATCHING_SIZE) {
95 persistNewIssues(statistics, addedIssues, issueDao, changeMapper, anticipatedTransitionMapper, dbSession);
98 } else if (issue.isChanged()) {
99 updatedIssues.add(issue);
100 if (updatedIssues.size() >= ISSUE_BATCHING_SIZE) {
101 persistUpdatedIssues(statistics, updatedIssues, issueDao, changeMapper, dbSession);
102 updatedIssues.clear();
104 } else if (isOnBranchUsingReferenceBranch() && issue.isNoLongerNewCodeReferenceIssue()) {
105 noLongerNewIssues.add(issue);
106 if (noLongerNewIssues.size() >= ISSUE_BATCHING_SIZE) {
107 persistNoLongerNewIssues(statistics, noLongerNewIssues, issueDao, dbSession);
108 noLongerNewIssues.clear();
110 } else if (isOnBranchUsingReferenceBranch() && issue.isToBeMigratedAsNewCodeReferenceIssue()) {
111 newCodeIssuesToMigrate.add(issue);
112 if (newCodeIssuesToMigrate.size() >= ISSUE_BATCHING_SIZE) {
113 persistNewCodeIssuesToMigrate(statistics, newCodeIssuesToMigrate, issueDao, dbSession);
114 newCodeIssuesToMigrate.clear();
119 persistNewIssues(statistics, addedIssues, issueDao, changeMapper, anticipatedTransitionMapper, dbSession);
120 persistUpdatedIssues(statistics, updatedIssues, issueDao, changeMapper, dbSession);
121 persistNoLongerNewIssues(statistics, noLongerNewIssues, issueDao, dbSession);
122 persistNewCodeIssuesToMigrate(statistics, newCodeIssuesToMigrate, issueDao, dbSession);
123 flushSession(dbSession);
125 statistics.dumpTo(context);
129 private void persistNewIssues(IssueStatistics statistics, List<DefaultIssue> addedIssues,
130 IssueDao issueDao, IssueChangeMapper changeMapper, AnticipatedTransitionMapper anticipatedTransitionMapper, DbSession dbSession) {
132 final long now = system2.now();
134 List<IssueDto> issueDtos = new LinkedList<>();
135 addedIssues.forEach(addedIssue -> {
136 String ruleUuid = ruleRepository.getByKey(addedIssue.ruleKey()).getUuid();
137 IssueDto dto = IssueDto.toDtoForComputationInsert(addedIssue, ruleUuid, now);
138 issueDao.insertWithoutImpacts(dbSession, dto);
140 if (isOnBranchUsingReferenceBranch() && addedIssue.isOnChangedLine()) {
141 issueDao.insertAsNewCodeOnReferenceBranch(dbSession, NewCodeReferenceIssueDto.fromIssueDto(dto, now, uuidFactory));
143 statistics.inserts++;
144 issueStorage.insertChanges(changeMapper, addedIssue, uuidFactory);
145 addedIssue.getAnticipatedTransitionUuid().ifPresent(anticipatedTransitionMapper::delete);
148 issueDtos.forEach(issueDto -> issueDao.insertIssueImpacts(dbSession, issueDto));
151 private void persistUpdatedIssues(IssueStatistics statistics, List<DefaultIssue> updatedIssues, IssueDao issueDao,
152 IssueChangeMapper changeMapper, DbSession dbSession) {
153 if (updatedIssues.isEmpty()) {
157 long now = system2.now();
158 LinkedList<IssueDto> updatedIssueDtos = new LinkedList<>();
159 updatedIssues.forEach(i -> {
160 IssueDto dto = IssueDto.toDtoForUpdate(i, now);
161 boolean isUpdated = issueDao.updateIfBeforeSelectedDate(dbSession, dto);
163 updatedIssueDtos.add(dto);
165 statistics.updates++;
167 updatedIssueDtos.forEach(i -> issueDao.deleteIssueImpacts(dbSession, i));
168 updatedIssueDtos.forEach(i -> issueDao.insertIssueImpacts(dbSession, i));
170 // retrieve those of the updatedIssues which have not been updated and apply conflictResolver on them
171 List<String> updatedIssueKeys = updatedIssues.stream().map(DefaultIssue::key).toList();
172 List<IssueDto> conflictIssueKeys = issueDao.selectByKeysIfNotUpdatedAt(dbSession, updatedIssueKeys, now);
173 if (!conflictIssueKeys.isEmpty()) {
174 updatedIssueDtos.clear();
175 Map<String, DefaultIssue> issuesByKeys = updatedIssues.stream().collect(Collectors.toMap(DefaultIssue::key, Function.identity()));
177 .forEach(dbIssue -> {
178 DefaultIssue updatedIssue = issuesByKeys.get(dbIssue.getKey());
179 IssueDto issueToBeUpdated = conflictResolver.resolve(updatedIssue, dbIssue);
180 issueDao.updateWithoutIssueImpacts(dbSession, issueToBeUpdated);
181 updatedIssueDtos.add(issueToBeUpdated);
185 updatedIssueDtos.forEach(i -> issueDao.deleteIssueImpacts(dbSession, i));
186 updatedIssueDtos.forEach(i -> issueDao.insertIssueImpacts(dbSession, i));
189 updatedIssues.forEach(i -> issueStorage.insertChanges(changeMapper, i, uuidFactory));
192 private static void persistNoLongerNewIssues(IssueStatistics statistics, List<DefaultIssue> noLongerNewIssues, IssueDao issueDao, DbSession dbSession) {
193 if (noLongerNewIssues.isEmpty()) {
197 noLongerNewIssues.forEach(i -> {
198 issueDao.deleteAsNewCodeOnReferenceBranch(dbSession, i.key());
199 statistics.updates++;
204 private void persistNewCodeIssuesToMigrate(IssueStatistics statistics, List<DefaultIssue> newCodeIssuesToMigrate, IssueDao issueDao, DbSession dbSession) {
205 if (newCodeIssuesToMigrate.isEmpty()) {
209 long now = system2.now();
210 newCodeIssuesToMigrate.forEach(i -> {
211 issueDao.insertAsNewCodeOnReferenceBranch(dbSession, NewCodeReferenceIssueDto.fromIssueKey(i.key(), now, uuidFactory));
212 statistics.updates++;
216 private static void flushSession(DbSession dbSession) {
217 dbSession.flushStatements();
221 private boolean isOnBranchUsingReferenceBranch() {
222 if (periodHolder.hasPeriod()) {
223 return periodHolder.getPeriod().getMode().equals(NewCodePeriodType.REFERENCE_BRANCH.name());
229 public String getDescription() {
230 return "Persist issues";
233 private static class IssueStatistics {
234 private int inserts = 0;
235 private int updates = 0;
236 private int merged = 0;
238 private void dumpTo(ComputationStep.Context context) {
239 context.getStatistics()
240 .add("inserts", String.valueOf(inserts))
241 .add("updates", String.valueOf(updates))
242 .add("merged", String.valueOf(merged));