blob: c432e7947e7e363b697cfaeac7f69ae54c5757b5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.ci.tcbot.issue;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
import javax.inject.Provider;
import org.apache.ignite.ci.HelperConfig;
import org.apache.ignite.ci.analysis.SuiteInBranch;
import org.apache.ignite.ci.analysis.TestInBranch;
import org.apache.ignite.ci.di.AutoProfiling;
import org.apache.ignite.ci.di.MonitoredTask;
import org.apache.ignite.ci.issue.EventTemplate;
import org.apache.ignite.ci.issue.EventTemplates;
import org.apache.ignite.ci.issue.Issue;
import org.apache.ignite.ci.issue.IssueKey;
import org.apache.ignite.ci.jobs.CheckQueueJob;
import org.apache.ignite.ci.mail.EmailSender;
import org.apache.ignite.ci.mail.SlackSender;
import org.apache.ignite.ci.tcbot.chain.TrackedBranchChainsProcessor;
import org.apache.ignite.ci.tcbot.conf.ITcBotConfig;
import org.apache.ignite.ci.tcbot.conf.TcServerConfig;
import org.apache.ignite.ci.tcbot.user.IUserStorage;
import org.apache.ignite.ci.teamcity.ignited.IRunHistory;
import org.apache.ignite.ci.teamcity.ignited.IStringCompactor;
import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnited;
import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnitedProvider;
import org.apache.ignite.ci.teamcity.ignited.SyncMode;
import org.apache.ignite.ci.teamcity.ignited.change.ChangeCompacted;
import org.apache.ignite.ci.teamcity.ignited.fatbuild.FatBuildCompacted;
import org.apache.ignite.ci.user.ICredentialsProv;
import org.apache.ignite.ci.user.TcHelperUser;
import org.apache.ignite.ci.web.model.current.ChainAtServerCurrentStatus;
import org.apache.ignite.ci.web.model.current.SuiteCurrentStatus;
import org.apache.ignite.ci.web.model.current.TestFailure;
import org.apache.ignite.ci.web.model.current.TestFailuresSummary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.ignite.ci.teamcity.ignited.runhist.RunHistSync.normalizeBranch;
/**
*
*/
public class IssueDetector {
/** Logger. */
private static final Logger logger = LoggerFactory.getLogger(IssueDetector.class);
/** Slack prefix, using this for email address will switch notifier to slack (if configured). */
private static final String SLACK = "slack:";
@Inject private IIssuesStorage issuesStorage;
@Inject private IUserStorage userStorage;
private final AtomicBoolean init = new AtomicBoolean();
private ICredentialsProv backgroundOpsCreds;
@Deprecated //todo use scheduler
private ScheduledExecutorService executorService;
@Inject private Provider<CheckQueueJob> checkQueueJobProv;
/** Tracked Branch Processor. */
@Inject private TrackedBranchChainsProcessor tbProc;
/** Server provider. */
@Inject private ITeamcityIgnitedProvider tcProv;
/** String Compactor. */
@Inject private IStringCompactor compactor;
/** Config. */
@Inject private ITcBotConfig cfg;
/** Send notification guard. */
private final AtomicBoolean sndNotificationGuard = new AtomicBoolean();
private String registerIssuesAndNotifyLater(TestFailuresSummary res,
ICredentialsProv creds) {
if (creds == null)
return null;
String newIssues = registerNewIssues(res, creds);
if (sndNotificationGuard.compareAndSet(false, true))
executorService.schedule(this::sendNewNotifications, 90, TimeUnit.SECONDS);
return newIssues;
}
private void sendNewNotifications() {
try {
sendNewNotificationsEx();
}
catch (Exception e) {
System.err.println("Fail to sent notifications");
e.printStackTrace();
logger.error("Failed to send notification", e.getMessage());
}
finally {
sndNotificationGuard.set(false);
}
}
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
@AutoProfiling
@MonitoredTask(name = "Send Notifications")
protected String sendNewNotificationsEx() throws IOException {
List<TcHelperUser> userForPossibleNotifications = new ArrayList<>();
userStorage.allUsers()
.filter(TcHelperUser::hasEmail)
.filter(TcHelperUser::hasSubscriptions)
.forEach(userForPossibleNotifications::add);
String slackCh = HelperConfig.loadEmailSettings().getProperty(HelperConfig.SLACK_CHANNEL);
Map<String, Notification> toBeSent = new HashMap<>();
AtomicInteger issuesChecked = new AtomicInteger();
issuesStorage.allIssues()
.peek(issue -> issuesChecked.incrementAndGet())
.filter(issue -> {
long detected = issue.detectedTs == null ? 0 : issue.detectedTs;
long issueAgeMs = System.currentTimeMillis() - detected;
return issueAgeMs <= TimeUnit.HOURS.toMillis(2);
})
.forEach(issue -> {
List<String> addrs = new ArrayList<>();
if (slackCh != null && TcServerConfig.DEFAULT_TRACKED_BRANCH_NAME.equals(issue.trackedBranchName))
addrs.add(SLACK + "#" + slackCh);
for (TcHelperUser next : userForPossibleNotifications) {
if (next.getCredentials(issue.issueKey().server) != null) {
if (next.isSubscribed(issue.trackedBranchName)) {
logger.info("User " + next + " is candidate for notification " + next.email
+ " for " + issue);
addrs.add(next.email);
}
}
}
for (String nextAddr : addrs) {
if (issuesStorage.setNotified(issue.issueKey, nextAddr)) {
toBeSent.computeIfAbsent(nextAddr, addr -> {
Notification notification = new Notification();
notification.ts = System.currentTimeMillis();
notification.addr = addr;
return notification;
}).addIssue(issue);
}
}
});
if (toBeSent.isEmpty())
return "Noting to notify, " + issuesChecked + " issues checked";
StringBuilder res = new StringBuilder();
Collection<Notification> values = toBeSent.values();
for (Notification next : values) {
if (next.addr.startsWith(SLACK)) {
String slackUser = next.addr.substring(SLACK.length());
List<String> messages = next.toSlackMarkup();
for (String msg : messages) {
final boolean snd = SlackSender.sendMessage(slackUser, msg);
res.append("Send ").append(slackUser).append(": ").append(snd);
if (!snd)
break;
}
}
else {
String builds = next.buildIdToIssue.keySet().toString();
String subj = "[MTCGA]: " + next.countIssues() + " new failures in builds " + builds + " needs to be handled";
EmailSender.sendEmail(next.addr, subj, next.toHtml(), next.toPlainText());
res.append("Send ").append(next.addr).append(" subject: ").append(subj);
}
}
return res + ", " + issuesChecked.get() + "issues checked";
}
/**
* @param res summary of failures in test
* @param creds Credentials provider.
* @return Displayable string with operation status.
*/
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
@AutoProfiling
@MonitoredTask(name = "Register new issues")
protected String registerNewIssues(TestFailuresSummary res, ICredentialsProv creds) {
int newIssues = 0;
for (ChainAtServerCurrentStatus next : res.servers) {
String srvId = next.serverId;
if (!tcProv.hasAccess(srvId, creds))
continue;
ITeamcityIgnited tcIgnited = tcProv.server(srvId, creds);
for (SuiteCurrentStatus suiteCurrentStatus : next.suites) {
String normalizeBranch = normalizeBranch(suiteCurrentStatus.branchName());
final String trackedBranch = res.getTrackedBranch();
for (TestFailure testFailure : suiteCurrentStatus.testFailures) {
if (registerTestFailIssues(tcIgnited, srvId, normalizeBranch, testFailure, trackedBranch))
newIssues++;
}
if (registerSuiteFailIssues(tcIgnited, srvId, normalizeBranch, suiteCurrentStatus, trackedBranch))
newIssues++;
}
}
return "New issues found " + newIssues;
}
private boolean registerSuiteFailIssues(ITeamcityIgnited tcIgnited,
String srvId,
String normalizeBranch,
SuiteCurrentStatus suiteFailure,
String trackedBranch) {
String suiteId = suiteFailure.suiteId;
SuiteInBranch key = new SuiteInBranch(suiteId, normalizeBranch);
IRunHistory runStat = tcIgnited.getSuiteRunHist(key);
if (runStat == null)
return false;
boolean issueFound = false;
Integer firstFailedBuildId = runStat.detectTemplate(EventTemplates.newCriticalFailure);
if (firstFailedBuildId != null && suiteFailure.hasCriticalProblem != null && suiteFailure.hasCriticalProblem) {
IssueKey issueKey = new IssueKey(srvId, firstFailedBuildId, suiteId);
if (issuesStorage.containsIssueKey(issueKey))
return false; //duplicate
Issue issue = new Issue(issueKey);
issue.trackedBranchName = trackedBranch;
issue.displayName = suiteFailure.name;
issue.webUrl = suiteFailure.webToHist;
issue.displayType = "New Critical Failure";
locateChanges(tcIgnited, firstFailedBuildId, issue);
logger.info("Register new issue for suite fail: " + issue);
issuesStorage.saveIssue(issue);
issueFound = true;
}
return issueFound;
}
private void locateChanges(ITeamcityIgnited teamcity, int buildId, Issue issue) {
final FatBuildCompacted fatBuild = teamcity.getFatBuild(buildId);
final int[] changes = fatBuild.changes();
final Collection<ChangeCompacted> allChanges = teamcity.getAllChanges(changes);
for (ChangeCompacted next : allChanges) {
issue.addChange(next.vcsUsername(compactor),
teamcity.host() + "viewModification.html?modId=" + next.id());
}
}
private boolean registerTestFailIssues(ITeamcityIgnited tcIgnited,
String srvId,
String normalizeBranch,
TestFailure testFailure,
String trackedBranch) {
String name = testFailure.name;
TestInBranch testInBranch = new TestInBranch(name, normalizeBranch);
IRunHistory runStat = tcIgnited.getTestRunHist(testInBranch);
if (runStat == null)
return false;
String displayType = null;
Integer firstFailedBuildId = runStat.detectTemplate(EventTemplates.newContributedTestFailure);
if (firstFailedBuildId != null)
displayType = "Recently contributed test failed";
if (firstFailedBuildId == null) {
firstFailedBuildId = runStat.detectTemplate(EventTemplates.newFailure);
if (firstFailedBuildId != null) {
displayType = "New test failure";
final String flakyComments = runStat.getFlakyComments();
if (!Strings.isNullOrEmpty(flakyComments)) {
if (runStat.detectTemplate(EventTemplates.newFailureForFlakyTest) == null) {
logger.info("Skipping registering new issue for test fail:" +
" Test seems to be flaky " + name + ": " + flakyComments);
firstFailedBuildId = null;
}
else
displayType = "New stable failure of a flaky test";
}
}
}
if (firstFailedBuildId == null)
return false;
int buildId = firstFailedBuildId;
IssueKey issueKey = new IssueKey(srvId, buildId, name);
if (issuesStorage.containsIssueKey(issueKey))
return false; //duplicate
Issue issue = new Issue(issueKey);
issue.trackedBranchName = trackedBranch;
issue.displayName = testFailure.testName;
issue.webUrl = testFailure.webUrl;
issue.displayType = displayType;
locateChanges(tcIgnited, buildId, issue);
logger.info("Register new issue for test fail: " + issue);
issuesStorage.saveIssue(issue);
return true;
}
/**
*
*/
public boolean isAuthorized() {
return backgroundOpsCreds != null;
}
public void startBackgroundCheck(ICredentialsProv prov) {
try {
if (init.compareAndSet(false, true)) {
this.backgroundOpsCreds = prov;
executorService = Executors.newScheduledThreadPool(3);
executorService.scheduleAtFixedRate(this::checkFailures, 0, 15, TimeUnit.MINUTES);
final CheckQueueJob checkQueueJob = checkQueueJobProv.get();
checkQueueJob.init(backgroundOpsCreds);
executorService.scheduleAtFixedRate(checkQueueJob, 0, 10, TimeUnit.MINUTES);
}
}
catch (Exception e) {
e.printStackTrace();
init.set(false);
throw e;
}
}
/**
*
*/
private void checkFailures() {
List<String> ids = cfg.getTrackedBranchesIds();
for (String tbranchName : ids) {
try {
checkFailuresEx(tbranchName);
}
catch (Exception e) {
e.printStackTrace();
logger.error("Failure periodic check failed: " + e.getMessage(), e);
}
}
}
/**
* @param brachName
*/
@AutoProfiling
@MonitoredTask(name = "Detect Issues in tracked branch", nameExtArgIndex = 0)
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
protected String checkFailuresEx(String brachName) {
int buildsToQry = EventTemplates.templates.stream().mapToInt(EventTemplate::cntEvents).max().getAsInt();
ICredentialsProv creds = Preconditions.checkNotNull(backgroundOpsCreds, "Server should be authorized");
tbProc.getTrackedBranchTestFailures(
brachName,
false,
buildsToQry,
creds,
SyncMode.RELOAD_QUEUED);
TestFailuresSummary failures =
tbProc.getTrackedBranchTestFailures(brachName,
false,
1,
creds,
SyncMode.RELOAD_QUEUED
);
String issResult = registerIssuesAndNotifyLater(failures, backgroundOpsCreds);
return "Tests " + failures.failedTests + " Suites " + failures.failedToFinish + " were checked. " + issResult;
}
public void stop() {
if (executorService != null)
executorService.shutdownNow();
}
}