Skip to content

Commit

Permalink
SONAR-6113 Short-circuit sending of notifications when there are no s…
Browse files Browse the repository at this point in the history
…ubscribers
  • Loading branch information
Simon Brandhof committed Feb 2, 2015
1 parent 8430a7f commit 7bcccf0
Show file tree
Hide file tree
Showing 33 changed files with 908 additions and 431 deletions.
Expand Up @@ -34,11 +34,12 @@
*/ */
public class IssueCache extends DiskCache<DefaultIssue> { public class IssueCache extends DiskCache<DefaultIssue> {


// this constructor is used by picocontainer
public IssueCache(TempFolder tempFolder, System2 system2) throws IOException { public IssueCache(TempFolder tempFolder, System2 system2) throws IOException {
super(tempFolder.newFile("issues", ".dat"), system2); super(tempFolder.newFile("issues", ".dat"), system2);
} }


IssueCache(File file, System2 system2) { public IssueCache(File file, System2 system2) {
super(file, system2); super(file, system2);
} }
} }
Expand Up @@ -19,73 +19,78 @@
*/ */
package org.sonar.server.computation.step; package org.sonar.server.computation.step;


import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multiset;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.notifications.Notification;
import org.sonar.api.rule.Severity;
import org.sonar.api.utils.DateUtils;
import org.sonar.core.component.ComponentDto; import org.sonar.core.component.ComponentDto;
import org.sonar.server.computation.ComputationContext; import org.sonar.server.computation.ComputationContext;
import org.sonar.server.computation.issue.IssueCache; import org.sonar.server.computation.issue.IssueCache;
import org.sonar.server.computation.issue.RuleCache; import org.sonar.server.computation.issue.RuleCache;
import org.sonar.server.issue.notification.IssueNotifications; import org.sonar.server.issue.notification.IssueChangeNotification;
import org.sonar.server.issue.notification.NewIssuesNotification;
import org.sonar.server.notifications.NotificationService;
import org.sonar.server.util.CloseableIterator; import org.sonar.server.util.CloseableIterator;


import java.util.Set;

/** /**
* Reads issues from disk cache and send related notifications. For performance reasons, * Reads issues from disk cache and send related notifications. For performance reasons,
* the standard notification DB queue is not used as a temporary storage. Notifications * the standard notification DB queue is not used as a temporary storage. Notifications
* are directly processed by {@link org.sonar.server.notifications.NotificationService}. * are directly processed by {@link org.sonar.server.notifications.NotificationService}.
*/ */
public class SendIssueNotificationsStep implements ComputationStep { public class SendIssueNotificationsStep implements ComputationStep {
/**
* Types of the notifications sent by this step
*/
static final Set<String> NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE);


private final IssueCache issueCache; private final IssueCache issueCache;
private final RuleCache rules; private final RuleCache rules;
private final IssueNotifications service; private final NotificationService service;


public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service) {
IssueNotifications service) {
this.issueCache = issueCache; this.issueCache = issueCache;
this.rules = rules; this.rules = rules;
this.service = service; this.service = service;
} }


@Override @Override
public void execute(ComputationContext context) { public void execute(ComputationContext context) {
NewIssuesStatistics newIssuesStatistics = new NewIssuesStatistics(); if (service.hasProjectSubscribersForTypes(context.getProject().uuid(), NOTIF_TYPES)) {
doExecute(context);
}
}

private void doExecute(ComputationContext context) {
NewIssuesNotification.Stats newIssueStats = new NewIssuesNotification.Stats();
CloseableIterator<DefaultIssue> issues = issueCache.traverse(); CloseableIterator<DefaultIssue> issues = issueCache.traverse();
try { try {
while (issues.hasNext()) { while (issues.hasNext()) {
DefaultIssue issue = issues.next(); DefaultIssue issue = issues.next();
if (issue.isNew() && issue.resolution() == null) { if (issue.isNew() && issue.resolution() == null) {
newIssuesStatistics.add(issue); newIssueStats.add(issue);
} else if (issue.isChanged() && issue.mustSendNotifications()) { } else if (issue.isChanged() && issue.mustSendNotifications()) {
service.sendChanges(issue, null, rules.ruleName(issue.ruleKey()), IssueChangeNotification changeNotification = new IssueChangeNotification();
context.getProject(), /* TODO */null, null, true); changeNotification.setRuleName(rules.ruleName(issue.ruleKey()));
changeNotification.setIssue(issue);
changeNotification.setProject(context.getProject());
service.deliver(changeNotification);
} }
} }


} finally { } finally {
issues.close(); issues.close();
} }
sendNewIssuesStatistics(context, newIssuesStatistics); sendNewIssuesStatistics(context, newIssueStats);
} }


private void sendNewIssuesStatistics(ComputationContext context, NewIssuesStatistics newIssuesStatistics) { private void sendNewIssuesStatistics(ComputationContext context, NewIssuesNotification.Stats stats) {
if (!newIssuesStatistics.isEmpty()) { if (stats.size() > 0) {
ComponentDto project = context.getProject(); ComponentDto project = context.getProject();
Notification notification = new Notification("new-issues") NewIssuesNotification notification = new NewIssuesNotification();
.setFieldValue("projectName", project.longName()) notification.setProject(project);
.setFieldValue("projectKey", project.key()) notification.setAnalysisDate(context.getAnalysisDate());
.setDefaultMessage(newIssuesStatistics.size() + " new issues on " + project.longName() + ".\n") notification.setStatistics(project, stats);
.setFieldValue("projectDate", DateUtils.formatDateTime(context.getAnalysisDate())) service.deliver(notification);
.setFieldValue("projectUuid", project.uuid())
.setFieldValue("count", String.valueOf(newIssuesStatistics.size()));
for (String severity : Severity.ALL) {
notification.setFieldValue("count-" + severity, String.valueOf(newIssuesStatistics.issuesWithSeverity(severity)));
}
service.send(notification, true);
} }
} }


Expand All @@ -94,24 +99,4 @@ public String getDescription() {
return "Send issue notifications"; return "Send issue notifications";
} }


static class NewIssuesStatistics {
private final Multiset<String> set = HashMultiset.create();

void add(Issue issue) {
set.add(issue.severity());
}

int issuesWithSeverity(String severity) {
return set.count(severity);
}

int size() {
return set.size();
}

boolean isEmpty() {
return set.isEmpty();
}
}

} }
Expand Up @@ -28,6 +28,7 @@
import org.sonar.api.issue.Issue; import org.sonar.api.issue.Issue;
import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.issue.internal.IssueChangeContext; import org.sonar.api.issue.internal.IssueChangeContext;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.Rule; import org.sonar.api.rules.Rule;
import org.sonar.core.component.ComponentDto; import org.sonar.core.component.ComponentDto;
Expand All @@ -36,7 +37,7 @@
import org.sonar.core.persistence.DbSession; import org.sonar.core.persistence.DbSession;
import org.sonar.server.db.DbClient; import org.sonar.server.db.DbClient;
import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.issue.notification.IssueNotifications; import org.sonar.server.issue.notification.IssueChangeNotification;
import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.rule.DefaultRuleFinder;
import org.sonar.server.search.QueryContext; import org.sonar.server.search.QueryContext;
import org.sonar.server.user.UserSession; import org.sonar.server.user.UserSession;
Expand All @@ -62,16 +63,16 @@ public class IssueBulkChangeService {
private final IssueService issueService; private final IssueService issueService;
private final IssueStorage issueStorage; private final IssueStorage issueStorage;
private final DefaultRuleFinder ruleFinder; private final DefaultRuleFinder ruleFinder;
private final IssueNotifications issueNotifications; private final NotificationManager notificationService;
private final List<Action> actions; private final List<Action> actions;


public IssueBulkChangeService(DbClient dbClient, IssueService issueService, IssueStorage issueStorage, DefaultRuleFinder ruleFinder, public IssueBulkChangeService(DbClient dbClient, IssueService issueService, IssueStorage issueStorage, DefaultRuleFinder ruleFinder,
IssueNotifications issueNotifications, List<Action> actions) { NotificationManager notificationService, List<Action> actions) {
this.dbClient = dbClient; this.dbClient = dbClient;
this.issueService = issueService; this.issueService = issueService;
this.issueStorage = issueStorage; this.issueStorage = issueStorage;
this.ruleFinder = ruleFinder; this.ruleFinder = ruleFinder;
this.issueNotifications = issueNotifications; this.notificationService = notificationService;
this.actions = actions; this.actions = actions;
} }


Expand Down Expand Up @@ -103,10 +104,12 @@ public IssueBulkChangeResult execute(IssueBulkChangeQuery issueBulkChangeQuery,
String projectKey = issue.projectKey(); String projectKey = issue.projectKey();
if (projectKey != null) { if (projectKey != null) {
Rule rule = repository.rule(issue.ruleKey()); Rule rule = repository.rule(issue.ruleKey());
issueNotifications.sendChanges((DefaultIssue) issue, issueChangeContext.login(), notificationService.scheduleForSending(new IssueChangeNotification()
rule != null ? rule.getName() : null, .setIssue((DefaultIssue) issue)
repository.project(projectKey), .setChangeAuthorLogin(issueChangeContext.login())
repository.component(issue.componentKey()), null, false); .setRuleName(rule != null ? rule.getName() : null)
.setProject(repository.project(projectKey))
.setComponent(repository.component(issue.componentKey())));
} }
} }
concernedProjects.add(issue.projectKey()); concernedProjects.add(issue.projectKey());
Expand Down
Expand Up @@ -29,6 +29,7 @@
import org.sonar.api.issue.Issue; import org.sonar.api.issue.Issue;
import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.issue.internal.IssueChangeContext; import org.sonar.api.issue.internal.IssueChangeContext;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity; import org.sonar.api.rule.Severity;
import org.sonar.api.rules.Rule; import org.sonar.api.rules.Rule;
Expand All @@ -50,7 +51,7 @@
import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.actionplan.ActionPlanService; import org.sonar.server.issue.actionplan.ActionPlanService;
import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.issue.notification.IssueNotifications; import org.sonar.server.issue.notification.IssueChangeNotification;
import org.sonar.server.search.FacetValue; import org.sonar.server.search.FacetValue;
import org.sonar.server.search.IndexClient; import org.sonar.server.search.IndexClient;
import org.sonar.server.search.QueryContext; import org.sonar.server.search.QueryContext;
Expand Down Expand Up @@ -80,7 +81,7 @@ public class IssueService implements ServerComponent {
private final IssueWorkflow workflow; private final IssueWorkflow workflow;
private final IssueUpdater issueUpdater; private final IssueUpdater issueUpdater;
private final IssueStorage issueStorage; private final IssueStorage issueStorage;
private final IssueNotifications issueNotifications; private final NotificationManager notificationService;
private final ActionPlanService actionPlanService; private final ActionPlanService actionPlanService;
private final RuleFinder ruleFinder; private final RuleFinder ruleFinder;
private final IssueDao deprecatedIssueDao; private final IssueDao deprecatedIssueDao;
Expand All @@ -92,7 +93,7 @@ public IssueService(DbClient dbClient, IndexClient indexClient,
IssueWorkflow workflow, IssueWorkflow workflow,
IssueStorage issueStorage, IssueStorage issueStorage,
IssueUpdater issueUpdater, IssueUpdater issueUpdater,
IssueNotifications issueNotifications, NotificationManager notificationService,
ActionPlanService actionPlanService, ActionPlanService actionPlanService,
RuleFinder ruleFinder, RuleFinder ruleFinder,
IssueDao deprecatedIssueDao, IssueDao deprecatedIssueDao,
Expand All @@ -105,7 +106,7 @@ public IssueService(DbClient dbClient, IndexClient indexClient,
this.issueUpdater = issueUpdater; this.issueUpdater = issueUpdater;
this.actionPlanService = actionPlanService; this.actionPlanService = actionPlanService;
this.ruleFinder = ruleFinder; this.ruleFinder = ruleFinder;
this.issueNotifications = issueNotifications; this.notificationService = notificationService;
this.deprecatedIssueDao = deprecatedIssueDao; this.deprecatedIssueDao = deprecatedIssueDao;
this.userFinder = userFinder; this.userFinder = userFinder;
this.userIndex = userIndex; this.userIndex = userIndex;
Expand Down Expand Up @@ -333,11 +334,13 @@ void saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context
} }
issueStorage.save(session, issue); issueStorage.save(session, issue);
Rule rule = getNullableRuleByKey(issue.ruleKey()); Rule rule = getNullableRuleByKey(issue.ruleKey());
issueNotifications.sendChanges(issue, context.login(), notificationService.scheduleForSending(new IssueChangeNotification()
rule != null ? rule.getName() : null, .setIssue(issue)
dbClient.componentDao().getByKey(session, projectKey), .setChangeAuthorLogin(context.login())
dbClient.componentDao().getNullableByKey(session, issue.componentKey()), .setRuleName(rule != null ? rule.getName() : null)
comment, false); .setProject(dbClient.componentDao().getByKey(session, projectKey))
.setComponent(dbClient.componentDao().getNullableByKey(session, issue.componentKey()))
.setComment(comment));
} }


/** /**
Expand Down
Expand Up @@ -37,7 +37,7 @@ public class ChangesOnMyIssueNotificationDispatcher extends NotificationDispatch
private NotificationManager notificationManager; private NotificationManager notificationManager;


public ChangesOnMyIssueNotificationDispatcher(NotificationManager notificationManager) { public ChangesOnMyIssueNotificationDispatcher(NotificationManager notificationManager) {
super("issue-changes"); super(IssueChangeNotification.TYPE);
this.notificationManager = notificationManager; this.notificationManager = notificationManager;
} }


Expand Down
@@ -0,0 +1,91 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.issue.notification;

import org.sonar.api.component.Component;
import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.issue.internal.FieldDiffs;
import org.sonar.api.notifications.Notification;

import javax.annotation.Nullable;

import java.io.Serializable;
import java.util.Map;

public class IssueChangeNotification extends Notification {

public static final String TYPE = "issue-changes";

public IssueChangeNotification() {
super(TYPE);
}

public IssueChangeNotification setIssue(DefaultIssue issue) {
setFieldValue("key", issue.key());
setFieldValue("reporter", issue.reporter());
setFieldValue("assignee", issue.assignee());
setFieldValue("message", issue.message());
setFieldValue("componentKey", issue.componentKey());
FieldDiffs currentChange = issue.currentChange();
if (currentChange != null) {
for (Map.Entry<String, FieldDiffs.Diff> entry : currentChange.diffs().entrySet()) {
String type = entry.getKey();
FieldDiffs.Diff diff = entry.getValue();
Serializable newValue = diff.newValue();
Serializable oldValue = diff.oldValue();
setFieldValue("old." + type, oldValue != null ? oldValue.toString() : null);
setFieldValue("new." + type, newValue != null ? newValue.toString() : null);
}
}
return this;
}

public IssueChangeNotification setProject(Component project) {
setFieldValue("projectName", project.longName());
setFieldValue("projectKey", project.key());
return this;
}

public IssueChangeNotification setComponent(Component component) {
setFieldValue("componentName", component.longName());
return this;
}

public IssueChangeNotification setChangeAuthorLogin(@Nullable String s) {
if (s != null) {
setFieldValue("changeAuthor", s);
}
return this;
}

public IssueChangeNotification setRuleName(@Nullable String s) {
if (s != null) {
setFieldValue("ruleName", s);
}
return this;
}

public IssueChangeNotification setComment(@Nullable String s) {
if (s != null) {
setFieldValue("comment", s);
}
return this;
}
}
Expand Up @@ -47,7 +47,7 @@ public IssueChangesEmailTemplate(EmailSettings settings, UserFinder userFinder)


@Override @Override
public EmailMessage format(Notification notif) { public EmailMessage format(Notification notif) {
if (!"issue-changes".equals(notif.getType())) { if (!IssueChangeNotification.TYPE.equals(notif.getType())) {
return null; return null;
} }


Expand Down

0 comments on commit 7bcccf0

Please sign in to comment.