You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ignite.apache.org by GitBox <gi...@apache.org> on 2018/11/19 16:34:13 UTC

[GitHub] asfgit closed pull request #74: IGNITE-10275 Refactor of visa caching. Jira spam fix.

asfgit closed pull request #74: IGNITE-10275 Refactor of visa caching. Jira spam fix.
URL: https://github.com/apache/ignite-teamcity-bot/pull/74
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
index f3ae04aa..7df50747 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
@@ -178,8 +178,12 @@ private BranchesTracked getTrackedBranches() {
 
             String comment = generateJiraComment(suitesStatuses, build.webUrl);
 
-            blockers = suitesStatuses.stream().mapToInt(suite ->
-                suite.testFailures.size()).sum();
+            blockers = suitesStatuses.stream().mapToInt(suite -> {
+                if (suite.testFailures.isEmpty())
+                    return 1;
+
+                return suite.testFailures.size();})
+                .sum();
 
             res = objectMapper.readValue(teamcity.sendJiraComment(ticket, comment), JiraCommentResponse.class);
         }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
index cf294684..b4686f74 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
@@ -17,19 +17,24 @@
 
 package org.apache.ignite.ci.observer;
 
-import java.util.Collection;
 import java.util.Objects;
 import java.util.Timer;
 import javax.inject.Inject;
+import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
+import org.apache.ignite.ci.ITcHelper;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.user.ICredentialsProv;
-import org.apache.ignite.ci.web.model.VisaRequest;
-import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
+import org.apache.ignite.ci.web.model.ContributionKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  *
  */
 public class BuildObserver {
+    /** Logger. */
+    private static final Logger logger = LoggerFactory.getLogger(BuildObserver.class);
+
     /** Time between observing actions in milliseconds. */
     private static final long PERIOD = 10 * 60 * 1_000;
 
@@ -39,8 +44,8 @@
     /** Task, which should be done periodically. */
     private ObserverTask observerTask;
 
-    /** Visas History Storage. */
-    @Inject private VisasHistoryStorage visasStorage;
+    /** Helper. */
+    @Inject private ITcHelper tcHelper;
 
     /**
      */
@@ -60,6 +65,25 @@ public void stop() {
         timer.cancel();
     }
 
+    /** */
+    public ObserverTask getObserverTask() {
+        return observerTask;
+    }
+
+    /** */
+    public boolean stopObservation(ContributionKey key) {
+        try {
+            observerTask.removeBuildInfo(key);
+
+            return true;
+        }
+        catch (Exception e) {
+            logger.error("Observation stop: " + e.getMessage(), e);
+
+            return false;
+        }
+    }
+
     /**
      * @param srvId Server id.
      * @param prov Credentials.
@@ -69,28 +93,26 @@ public void stop() {
     public void observe(String srvId, ICredentialsProv prov, String ticket, String branchForTc, Build... builds) {
         BuildsInfo buildsInfo = new BuildsInfo(srvId, prov, ticket, branchForTc, builds);
 
-        visasStorage.put(new VisaRequest(buildsInfo));
-
         observerTask.addInfo(buildsInfo);
     }
 
     /**
-     * @param srvId Server id.
-     * @param branch Branch.
+     * @param key {@code Contribution Key}.
      */
-    public String getObservationStatus(String srvId, String branch) {
+    public String getObservationStatus(ContributionKey key) {
         StringBuilder sb = new StringBuilder();
 
-        Collection<BuildsInfo> builds = observerTask.getInfos();
+        BuildsInfo buildsInfo = observerTask.getInfo(key);
+
+        ICredentialsProv creds = tcHelper.getServerAuthorizerCreds();
+
+        IAnalyticsEnabledTeamcity teamcity = tcHelper.server(key.srvId, creds);
 
-        for (BuildsInfo bi : builds) {
-            if (Objects.equals(bi.branchForTc, branch)
-                && Objects.equals(bi.srvId, srvId)) {
-                sb.append(bi.ticket).append(" to be commented, waiting for builds. ");
-                sb.append(bi.finishedBuildsCount());
-                sb.append(" builds done from ");
-                sb.append(bi.buildsCount());
-            }
+        if (Objects.nonNull(buildsInfo)) {
+            sb.append(buildsInfo.ticket).append(" to be commented, waiting for builds. ");
+            sb.append(buildsInfo.finishedBuildsCount(teamcity));
+            sb.append(" builds done from ");
+            sb.append(buildsInfo.buildsCount());
         }
 
         return sb.toString();
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
index 71171694..e02ad5b3 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
@@ -17,11 +17,11 @@
 
 package org.apache.ignite.ci.observer;
 
+import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.Objects;
 import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.tcmodel.result.Build;
@@ -34,13 +34,13 @@
  */
 public class BuildsInfo {
     /** */
-    public static final String FINISHED_STATE = "finished";
+    public static final String FINISHED_STATUS = "finished";
 
     /** */
-    public static final String RUNNING_STATE = "running";
+    public static final String RUNNING_STATUS = "running";
 
     /** */
-    public static final String FINISHED_WITH_FAILURES_STATE = "finished with failures";
+    public static final String CANCELLED_STATUS = "cancelled";
 
     /** */
     public final String userName;
@@ -61,17 +61,17 @@
     public final Date date;
 
     /** Finished builds. */
-    private final Map<Integer, Boolean> finishedBuilds = new HashMap<>();
+    private final List<Integer> builds = new ArrayList<>();
 
     /** */
-    public BuildsInfo(CompactBuildsInfo buildsInfo, IStringCompactor strCompactor) {
-        this.userName = strCompactor.getStringFromId(buildsInfo.userName());
-        this.date = buildsInfo.date();
-        this.srvId = strCompactor.getStringFromId(buildsInfo.srvId());
-        this.ticket = strCompactor.getStringFromId(buildsInfo.ticket());
-        this.branchForTc = strCompactor.getStringFromId(buildsInfo.branchForTc());
-        this.buildTypeId = strCompactor.getStringFromId(buildsInfo.buildTypeId());
-        this.finishedBuilds.putAll(buildsInfo.getFinishedBuilds());
+    public BuildsInfo(CompactBuildsInfo compactBuildsInfo, IStringCompactor strCompactor) {
+        this.userName = strCompactor.getStringFromId(compactBuildsInfo.userName());
+        this.date = compactBuildsInfo.date();
+        this.srvId = strCompactor.getStringFromId(compactBuildsInfo.srvId());
+        this.ticket = strCompactor.getStringFromId(compactBuildsInfo.ticket());
+        this.branchForTc = strCompactor.getStringFromId(compactBuildsInfo.branchForTc());
+        this.buildTypeId = strCompactor.getStringFromId(compactBuildsInfo.buildTypeId());
+        this.builds.addAll(compactBuildsInfo.getBuilds());
     }
 
     /**
@@ -90,71 +90,73 @@ public BuildsInfo(String srvId, ICredentialsProv prov, String ticket, String bra
         this.buildTypeId = builds.length == 1 ? builds[0].buildTypeId : "IgniteTests24Java8_RunAll";
 
         for (Build build : builds)
-            finishedBuilds.put(build.getId(), false);
+            this.builds.add(build.getId());
     }
 
     /**
      * @param teamcity Teamcity.
      */
     public String getState(IAnalyticsEnabledTeamcity teamcity) {
-        for (Map.Entry<Integer, Boolean> entry : finishedBuilds.entrySet()) {
-            if (entry.getValue() == null)
-                return FINISHED_WITH_FAILURES_STATE;
+        boolean isFinished = true;
 
-            if (!entry.getValue()) {
-                Build build = teamcity.getBuild(entry.getKey());
+        for (Integer id : builds) {
+            Build build = teamcity.getBuild(id);
 
-                if (build.isFinished()) {
-                    if (build.isUnknown()) {
-                        entry.setValue(null);
+            if (build.isUnknown())
+                return CANCELLED_STATUS;
 
-                        return FINISHED_WITH_FAILURES_STATE;
-                    }
-
-                    entry.setValue(true);
-                }
-            }
+            if (!build.isFinished())
+                isFinished = false;
         }
 
-        return finishedBuilds.containsValue(false) ? RUNNING_STATE : FINISHED_STATE;
+        return isFinished ? FINISHED_STATUS : RUNNING_STATUS;
     }
 
     /**
      * @param teamcity Teamcity.
      */
     public boolean isFinished(IAnalyticsEnabledTeamcity teamcity) {
-        return FINISHED_STATE.equals(getState(teamcity));
+        return FINISHED_STATUS.equals(getState(teamcity));
     }
 
     /**
      * @param teamcity Teamcity.
      */
-    public boolean isFinishedWithFailures(IAnalyticsEnabledTeamcity teamcity) {
-        return FINISHED_WITH_FAILURES_STATE.equals(getState(teamcity));
+    public boolean isCancelled(IAnalyticsEnabledTeamcity teamcity) {
+        return CANCELLED_STATUS.equals(getState(teamcity));
     }
 
     /**
      * Return builds count.
      */
     public int buildsCount(){
-        return finishedBuilds.size();
+        return builds.size();
     }
 
     /**
      * Return finished builds count.
      */
-    public int finishedBuildsCount(){
-        return (int)finishedBuilds.values().stream().filter(v -> v).count();
+    public int finishedBuildsCount(IAnalyticsEnabledTeamcity teamcity){
+        int finishedCnt = 0;
+
+        for (Integer id : builds) {
+            Build build = teamcity.getBuild(id);
+
+            if (build.isFinished())
+                ++finishedCnt;
+        }
+
+        return finishedCnt;
     }
 
     /** */
     public ContributionKey getContributionKey() {
-        return new ContributionKey(srvId, ticket, branchForTc);
+        return new ContributionKey(srvId, branchForTc);
     }
 
     /** */
-    public Map<Integer, Boolean> getBuilds() {
-        return Collections.unmodifiableMap(finishedBuilds);
+    public List<Integer> getBuilds() {
+        return Collections.unmodifiableList(builds);
     }
 
     /** {@inheritDoc} */
@@ -171,12 +173,12 @@ public ContributionKey getContributionKey() {
             Objects.equals(buildTypeId, info.buildTypeId) &&
             Objects.equals(branchForTc, info.branchForTc) &&
             Objects.equals(ticket, info.ticket) &&
-            Objects.equals(finishedBuilds.keySet(), info.finishedBuilds.keySet()) &&
+            Objects.equals(builds, info.builds) &&
             Objects.equals(date, info.date);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
-        return Objects.hash(srvId, buildTypeId, branchForTc, ticket, finishedBuilds.keySet(), date);
+        return Objects.hash(srvId, buildTypeId, branchForTc, ticket, builds, date);
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/CompactBuildsInfo.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/CompactBuildsInfo.java
index c07f05af..df700481 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/CompactBuildsInfo.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/CompactBuildsInfo.java
@@ -17,10 +17,10 @@
 
 package org.apache.ignite.ci.observer;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.Objects;
 import org.apache.ignite.ci.teamcity.ignited.IStringCompactor;
 
@@ -46,9 +46,11 @@
     /** */
     private Date date;
 
-    /** Finished builds. */
-    private final Map<Integer, Boolean> finishedBuilds = new HashMap<>();
+    /** Builds. */
+    private final List<Integer> builds = new ArrayList<>();
 
+
+    /** */
     public CompactBuildsInfo() {
 
     }
@@ -61,12 +63,12 @@ public CompactBuildsInfo(BuildsInfo buildsInfo, IStringCompactor strCompactor) {
         this.ticket = strCompactor.getStringId(buildsInfo.ticket);
         this.branchForTc = strCompactor.getStringId(buildsInfo.branchForTc);
         this.buildTypeId = strCompactor.getStringId(buildsInfo.buildTypeId);
-        this.finishedBuilds.putAll(buildsInfo.getBuilds());
+        this.builds.addAll(buildsInfo.getBuilds());
     }
 
     /** */
-    public Map<Integer, Boolean> getFinishedBuilds() {
-        return Collections.unmodifiableMap(finishedBuilds);
+    public List<Integer> getBuilds() {
+        return Collections.unmodifiableList(builds);
     }
 
     /** */
@@ -144,44 +146,48 @@ public void ticket(int ticket) {
             Objects.equals(buildTypeId, info.buildTypeId) &&
             Objects.equals(branchForTc, info.branchForTc) &&
             Objects.equals(ticket, info.ticket) &&
-            Objects.equals(finishedBuilds.keySet(), info.finishedBuilds.keySet()) &&
+            Objects.equals(builds, info.builds) &&
             Objects.equals(date, info.date);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
-        return Objects.hash(srvId, buildTypeId, branchForTc, ticket, finishedBuilds.keySet(), date);
+        return Objects.hash(srvId, buildTypeId, branchForTc, ticket, builds, date);
     }
 
+    /** */
     public void userName(int val) {
         this.userName = val;
     }
 
+    /** */
     public void date(long ts) {
         this.date = new Date(ts);
     }
 
+    /** */
     public int userName() {
         return userName;
     }
 
+    /** */
     public Date date() {
         return date;
     }
 
+    /** */
     public int srvId() {
         return srvId;
     }
 
+    /** */
     public void srvId(int srvId) {
         this.srvId = srvId;
     }
 
+    /** */
     public void addBuild(int... arr) {
-        for (int i = 0; i < arr.length; i++) {
-            int i1 = arr[i];
-
-            finishedBuilds.put(i1, false);
-        }
+        for (int i = 0; i < arr.length; i++)
+            builds.add(arr[i]);
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
index 86768688..68335c8b 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
@@ -19,13 +19,16 @@
 
 import com.google.common.base.Preconditions;
 import java.util.ArrayList;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TimerTask;
-import javax.cache.Cache;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
 import javax.inject.Inject;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
@@ -37,9 +40,13 @@
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.teamcity.ignited.IStringCompactor;
 import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.model.CompactContributionKey;
+import org.apache.ignite.ci.web.model.ContributionKey;
 import org.apache.ignite.ci.web.model.Visa;
+import org.apache.ignite.ci.web.model.VisaRequest;
 import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
 import org.apache.ignite.internal.util.typedef.X;
+import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,7 +58,7 @@
     private static final Logger logger = LoggerFactory.getLogger(ObserverTask.class);
 
     /** */
-    public static final String BUILDS_CACHE_NAME = "compactBuildsInfos";
+    public static final String BUILDS_CACHE_NAME = "compactBuildsInfosCache";
 
     /** Helper. */
     @Inject private ITcHelper tcHelper;
@@ -63,33 +70,76 @@
     @Inject private Ignite ignite;
 
     /** */
-    @Inject private VisasHistoryStorage visasHistoryStorage;
+    @Inject private VisasHistoryStorage visasHistStorage;
 
     /** */
     @Inject private IStringCompactor strCompactor;
 
+    /** */
+    private ReentrantLock observationLock = new ReentrantLock();
+
     /**
      */
     ObserverTask() {
     }
 
     /** */
-    private IgniteCache<CompactBuildsInfo, Object> compactInfos() {
+    private IgniteCache<CompactContributionKey, CompactBuildsInfo> compactInfos() {
         return ignite.getOrCreateCache(TcHelperDb.getCacheV2TxConfig(BUILDS_CACHE_NAME));
     }
 
+    /** */
+    @Nullable public BuildsInfo getInfo(ContributionKey key) {
+        CompactBuildsInfo compactBuildsInfo = compactInfos().get(new CompactContributionKey(key, strCompactor));
+
+        return Objects.isNull(compactBuildsInfo) ? null : compactBuildsInfo.toBuildInfo(strCompactor);
+    }
+
+
     /** */
     public Collection<BuildsInfo> getInfos() {
         List<BuildsInfo> buildsInfos = new ArrayList<>();
 
-        compactInfos().forEach(entry -> buildsInfos.add(entry.getKey().toBuildInfo(strCompactor)));
+        compactInfos().forEach(entry -> buildsInfos.add(entry.getValue().toBuildInfo(strCompactor)));
 
         return buildsInfos;
     }
 
     /** */
     public void addInfo(BuildsInfo info) {
-        compactInfos().put(new CompactBuildsInfo(info, strCompactor), new Object());
+        visasHistStorage.put(new VisaRequest(info));
+
+        compactInfos().put(new CompactContributionKey(info.getContributionKey(), strCompactor),
+            new CompactBuildsInfo(info, strCompactor));
+    }
+
+    /** */
+    public void removeBuildInfo(ContributionKey key) {
+        observationLock.lock();
+
+        try {
+            removeBuildInfo(new CompactContributionKey(key, strCompactor));
+        }
+        finally {
+            observationLock.unlock();
+        }
+    }
+
+    /** */
+    private void removeBuildInfo(CompactContributionKey key) {
+        try {
+            boolean rmv = compactInfos().remove(key);
+
+            Preconditions.checkState(rmv, "Key not found: " + key.toContributionKey(strCompactor).toString());
+        }
+        catch (Exception e) {
+            logger.error("Cache remove: " + e.getMessage(), e);
+
+            throw new RuntimeException("Observer queue: " +
+                getInfos().stream().map(bi -> bi.getContributionKey().toString())
+                    .collect(Collectors.joining(", ")) +
+                " Error: " + X.getFullStackTrace(e));
+        }
     }
 
     /** {@inheritDoc} */
@@ -108,88 +158,80 @@ public void addInfo(BuildsInfo info) {
     @AutoProfiling
     @MonitoredTask(name = "Build Observer")
     protected String runObserverTask() {
-        if (!tcHelper.isServerAuthorized())
-            return "Server authorization required.";
-
-        int checkedBuilds = 0;
-        int notFinishedBuilds = 0;
-        Set<String> ticketsNotified = new HashSet<>();
-
-        ObjectMapper objMapper = new ObjectMapper();
+        observationLock.lock();
 
-        List<String> rmvdVisas = new ArrayList<>();
+        try {
+            if (!tcHelper.isServerAuthorized())
+                return "Server authorization required.";
 
-        List<String> queuedVisas = new ArrayList<>();
+            int checkedBuilds = 0;
+            int notFinishedBuilds = 0;
+            Set<String> ticketsNotified = new HashSet<>();
 
-        for (Cache.Entry<CompactBuildsInfo, Object> entry : compactInfos()) {
-            CompactBuildsInfo compactInfo = entry.getKey();
+            Map<CompactContributionKey, Boolean> rmv = new HashMap<>();
 
-            try {
-                queuedVisas.add(objMapper.writeValueAsString(compactInfo));
-            }
-            catch (Exception e) {
-                logger.error("JSON string parse failed: " + e.getMessage(), e);
+            for (IgniteCache.Entry<CompactContributionKey, CompactBuildsInfo> entry : compactInfos()) {
+                CompactBuildsInfo compactInfo = entry.getValue();
 
-                return "Exception while JSON parsing " + e.getClass().getSimpleName() + ": " + e.getMessage();
-            }
+                BuildsInfo info = compactInfo.toBuildInfo(strCompactor);
 
-            BuildsInfo info = compactInfo.toBuildInfo(strCompactor);
+                IAnalyticsEnabledTeamcity teamcity = tcHelper.server(info.srvId, tcHelper.getServerAuthorizerCreds());
 
-            checkedBuilds += info.buildsCount();
+                checkedBuilds += info.buildsCount();
 
-            IAnalyticsEnabledTeamcity teamcity = tcHelper.server(info.srvId, tcHelper.getServerAuthorizerCreds());
+                if (info.isCancelled(teamcity)) {
+                    rmv.put(entry.getKey(), false);
 
-            if (info.isFinishedWithFailures(teamcity)) {
-                boolean rmv = compactInfos().remove(compactInfo);
+                    logger.error("JIRA will not be commented." +
+                        " [ticket: " + info.ticket + ", branch:" + info.branchForTc + "] : " +
+                        "one or more re-runned blocker's builds finished with UNKNOWN status.");
 
-                Preconditions.checkState(rmv, "Key not found: " + compactInfo);
+                    continue;
+                }
 
-                logger.error("JIRA will not be commented." +
-                    " [ticket: " + info.ticket + ", branch:" + info.branchForTc + "] : " +
-                    "one or more re-runned blocker's builds finished with UNKNOWN status.");
+                if (!info.isFinished(teamcity)) {
+                    notFinishedBuilds += info.buildsCount() - info.finishedBuildsCount(teamcity);
 
-                continue;
-            }
+                    continue;
+                }
 
-            if (!info.isFinished(teamcity)) {
-                notFinishedBuilds += info.buildsCount() - info.finishedBuildsCount();
+                Visa visa = visasHistStorage.getVisaRequest(info.getContributionKey(), info.date).getResult();
 
-                continue;
-            }
+                if (Objects.isNull(visa))
+                    continue;
 
-            try {
-                rmvdVisas.add(objMapper.writeValueAsString(compactInfo));
-            }
-            catch (Exception e) {
-                logger.error("JSON string parse failed: " + e.getMessage(), e);
+                if (!visa.isSuccess()) {
+                    ICredentialsProv creds = tcHelper.getServerAuthorizerCreds();
 
-                return "Exception while JSON parsing: " + e.getClass().getSimpleName() + ": " + e.getMessage();
-            }
+                    visa = jiraIntegration.notifyJira(info.srvId, creds, info.buildTypeId,
+                        info.branchForTc, info.ticket);
 
-            try {
-                boolean rmv = compactInfos().remove(compactInfo);
+                    visasHistStorage.updateVisaRequestResult(info.getContributionKey(), info.date, visa);
 
-                if (!rmv)
-                    continue;
-            }
-            catch (Exception e) {
-                logger.error("cache remove: " + e.getMessage(), e);
+                    if (visa.isSuccess())
+                        ticketsNotified.add(info.ticket);
+                }
 
-                return X.getFullStackTrace(e);
+                if (visa.isSuccess())
+                    rmv.put(entry.getKey(), false);
             }
 
-            ICredentialsProv creds = tcHelper.getServerAuthorizerCreds();
-
-            Visa visa = jiraIntegration.notifyJira(info.srvId, creds, info.buildTypeId,
-                info.branchForTc, info.ticket);
+            rmv.entrySet().forEach(entry -> {
+                try {
+                    removeBuildInfo(entry.getKey());
 
-            visasHistoryStorage.updateVisaRequestRes(info.getContributionKey(), info.date, visa);
+                    entry.setValue(true);
+                }
+                catch (Exception e) {
+                   logger.error(e.getMessage(), e);
+                }
+            });
 
-            ticketsNotified.add(info.ticket);
+            return "Checked " + checkedBuilds + " not finished " + notFinishedBuilds + " notified: " + ticketsNotified +
+                " Rmv problems: " + rmv.values().stream().filter(v -> !v).count();
+        }
+        finally {
+            observationLock.unlock();
         }
-
-        return "Checked " + checkedBuilds + " not finished " + notFinishedBuilds + " notified: " + ticketsNotified +
-            " Visas in queue: [" + String.join(", ", queuedVisas) +
-            "] Visas to rmv: [" + String.join(", ", rmvdVisas) + ']';
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
index 751050e7..312a033d 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Strings;
 import com.google.inject.Provider;
+import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -38,6 +39,7 @@
 import org.apache.ignite.ci.github.pure.IGitHubConnectionProvider;
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.observer.BuildObserver;
+import org.apache.ignite.ci.observer.ObserverTask;
 import org.apache.ignite.ci.observer.BuildsInfo;
 import org.apache.ignite.ci.tcbot.chain.PrChainsProcessor;
 import org.apache.ignite.ci.tcmodel.result.Build;
@@ -46,6 +48,7 @@
 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.web.model.ContributionKey;
 import org.apache.ignite.ci.web.model.VisaRequest;
 import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.user.ICredentialsProv;
@@ -56,10 +59,20 @@
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static org.apache.ignite.ci.observer.BuildsInfo.CANCELLED_STATUS;
+import static org.apache.ignite.ci.observer.BuildsInfo.FINISHED_STATUS;
+import static org.apache.ignite.ci.observer.BuildsInfo.RUNNING_STATUS;
+
 /**
  * Provides method for TC Bot Visa obtaining
  */
 public class TcBotTriggerAndSignOffService {
+    /** */
+    private static final ThreadLocal<DateFormat> THREAD_FORMATTER = new ThreadLocal<DateFormat>() {
+        @Override protected DateFormat initialValue() {
+            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        }
+    };
 
     @Inject Provider<BuildObserver> buildObserverProvider;
 
@@ -88,12 +101,7 @@
 
 
     @Inject PrChainsProcessor prChainsProcessor;
-
-
-    /** */
-    //todo fix concurrency issue:
-    private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
+    
     /** */
     public void startObserver() {
         buildObserverProvider.get();
@@ -105,6 +113,8 @@ public void startObserver() {
 
         IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, prov);
 
+        ObserverTask observerTask = buildObserverProvider.get().getObserverTask();
+
         for (VisaRequest visaRequest : visasHistoryStorage.getVisas()) {
             VisaStatus visaStatus = new VisaStatus();
 
@@ -112,15 +122,19 @@ public void startObserver() {
 
             Visa visa = visaRequest.getResult();
 
-            visaStatus.date = formatter.format(info.date);
+            visaStatus.date = THREAD_FORMATTER.get().format(info.date);
             visaStatus.branchName = info.branchForTc;
             visaStatus.userName = info.userName;
             visaStatus.ticket = info.ticket;
 
-            if (info.isFinished(teamcity)) {
-                if (visa.isEmpty())
-                    visaStatus.state = BuildsInfo.FINISHED_STATE + " [ waiting results ]";
-                else if (visa.isSuccess()) {
+            String buildsStatus = visaStatus.status = info.getState(teamcity);
+
+            BuildsInfo observInfo = observerTask.getInfo(info.getContributionKey());
+
+            boolean isObserving = Objects.nonNull(observInfo) && observInfo.date.equals(info.date);
+
+            if (FINISHED_STATUS.equals(buildsStatus)) {
+                if (visa.isSuccess()) {
                     visaStatus.commentUrl = "https://issues.apache.org/jira/browse/" + visaStatus.ticket +
                         "?focusedCommentId=" + visa.getJiraCommentResponse().getId() +
                         "&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-" +
@@ -128,13 +142,18 @@ else if (visa.isSuccess()) {
 
                     visaStatus.blockers = visa.getBlockers();
 
-                    visaStatus.state = BuildsInfo.FINISHED_STATE;
+                    visaStatus.status = FINISHED_STATUS;
                 }
                 else
-                    visaStatus.state = BuildsInfo.FINISHED_WITH_FAILURES_STATE;
+                    visaStatus.status = isObserving ? "waiting results" : CANCELLED_STATUS;
             }
+            else if (RUNNING_STATUS.equals(buildsStatus))
+                visaStatus.status = isObserving ? RUNNING_STATUS : CANCELLED_STATUS;
             else
-                visaStatus.state = info.getState(teamcity);
+                visaStatus.status = buildsStatus;
+
+            if (isObserving)
+                visaStatus.cancelUrl = "/rest/visa/cancel?server=" + srvId + "&branch=" + info.branchForTc;
 
             visaStatuses.add(visaStatus);
         }
@@ -377,7 +396,7 @@ public ContributionCheckStatus contributionStatus(String srvId, ICredentialsProv
         else
             status.resolvedBranch = !allRunAlls.isEmpty() ? allRunAlls.get(0).branchName(compactor) : branchForTcA(prId);
 
-        String observationsStatus = observer.get().getObservationStatus(srvId, status.resolvedBranch);
+        String observationsStatus = observer.get().getObservationStatus(new ContributionKey(srvId, status.resolvedBranch));
 
         status.observationsStatus  = Strings.emptyToNull(observationsStatus);
 
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
index 1dfe31fc..e20c52b6 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
@@ -33,7 +33,7 @@
     @Nullable public String ticket;
 
     /** */
-    @Nullable public String state;
+    @Nullable public String status;
 
     /** */
     @Nullable public String commentUrl;
@@ -41,6 +41,9 @@
     /** */
     @Nullable public String date;
 
+    /** */
+    @Nullable public String cancelUrl;
+
     /** */
     public int blockers;
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactContributionKey.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactContributionKey.java
index 3c6a6f0c..217a9613 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactContributionKey.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactContributionKey.java
@@ -18,7 +18,6 @@
 package org.apache.ignite.ci.web.model;
 
 import java.util.Objects;
-
 import org.apache.ignite.ci.teamcity.ignited.IStringCompactor;
 
 /**
@@ -28,9 +27,6 @@
     /** */
     public final int srvId;
 
-    /** */
-    public final int ticket;
-
     /** */
     public final int branchForTc;
 
@@ -38,7 +34,11 @@
     public CompactContributionKey(ContributionKey key, IStringCompactor strCompactor) {
         this.branchForTc = strCompactor.getStringId(key.branchForTc);
         this.srvId = strCompactor.getStringId(key.srvId);
-        this.ticket = strCompactor.getStringId(key.ticket);
+    }
+
+    /** */
+    public ContributionKey toContributionKey(IStringCompactor strCompactor) {
+        return new ContributionKey(this, strCompactor);
     }
 
     /** {@inheritDoc} */
@@ -52,12 +52,11 @@ public CompactContributionKey(ContributionKey key, IStringCompactor strCompactor
         CompactContributionKey key = (CompactContributionKey)o;
 
         return Objects.equals(srvId, key.srvId) &&
-            Objects.equals(ticket, key.ticket) &&
             Objects.equals(branchForTc, key.branchForTc);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
-        return Objects.hash(srvId, branchForTc, ticket);
+        return Objects.hash(srvId, branchForTc);
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java
index fa62ea64..570f2c9d 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.ci.web.model;
 
+import org.apache.ignite.ci.teamcity.ignited.IStringCompactor;
+
 /**
  *
  */
@@ -24,16 +26,23 @@
     /** */
     public final String srvId;
 
-    /** */
-    public final String ticket;
-
     /** */
     public final String branchForTc;
 
     /** */
-    public ContributionKey(String srvId, String ticket, String branchForTc) {
+    public ContributionKey(String srvId, String branchForTc) {
         this.branchForTc = branchForTc;
         this.srvId = srvId;
-        this.ticket = ticket;
+    }
+
+    /** */
+    public ContributionKey(CompactContributionKey key, IStringCompactor strCompactor) {
+        this.branchForTc = strCompactor.getStringFromId(key.branchForTc);
+        this.srvId = strCompactor.getStringFromId(key.srvId);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "{srv: " + this.srvId + " branch: " + this.branchForTc + '}';
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java
index 4d234baf..e826cc4e 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java
@@ -76,7 +76,6 @@ public boolean isEmpty() {
         return EMPTY_VISA_STATUS.equals(status);
     }
 
-
     /** */
     @Override public String toString() {
         return status;
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java
index 9a50296a..28299f7a 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java
@@ -42,7 +42,7 @@
  */
 public class VisasHistoryStorage {
     /** */
-    private static final String VISAS_CACHE_NAME = "visasCompactCache";
+    private static final String VISAS_CACHE_NAME = "compactVisasHistoryCache";
 
     /** */
     @Inject
@@ -68,7 +68,6 @@ public void put(VisaRequest visaReq) {
 
         CompactContributionKey key = new CompactContributionKey(new ContributionKey(
             visaReq.getInfo().srvId,
-            visaReq.getInfo().ticket,
             visaReq.getInfo().branchForTc), strCompactor);
 
         Map<Date, CompactVisaRequest> contributionVisas = visas().get(key);
@@ -82,7 +81,7 @@ public void put(VisaRequest visaReq) {
     }
 
     /** */
-    public VisaRequest getVisaReq(ContributionKey key, Date date) {
+    public VisaRequest getVisaRequest(ContributionKey key, Date date) {
         Map<Date, CompactVisaRequest> reqs = visas().get(new CompactContributionKey(key, strCompactor));
 
         if (Objects.isNull(reqs))
@@ -92,8 +91,8 @@ public VisaRequest getVisaReq(ContributionKey key, Date date) {
     }
 
     /** */
-    public boolean updateVisaRequestRes(ContributionKey key, Date date, Visa visa) {
-        VisaRequest req = getVisaReq(key, date);
+    public boolean updateVisaRequestResult(ContributionKey key, Date date, Visa visa) {
+        VisaRequest req = getVisaRequest(key, date);
 
         if (req == null)
             return false;
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
index 18e2f608..f70975dc 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
@@ -27,6 +27,7 @@
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
+import org.apache.ignite.ci.observer.BuildObserver;
 import org.apache.ignite.ci.tcbot.visa.ContributionCheckStatus;
 import org.apache.ignite.ci.tcbot.visa.ContributionToCheck;
 import org.apache.ignite.ci.tcbot.visa.CurrentVisaStatus;
@@ -34,7 +35,9 @@
 import org.apache.ignite.ci.tcbot.visa.VisaStatus;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.web.CtxListener;
+import org.apache.ignite.ci.web.model.ContributionKey;
 import org.apache.ignite.ci.web.rest.exception.ServiceUnauthorizedException;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 @Path("visa")
@@ -48,6 +51,16 @@
     @Context
     private HttpServletRequest req;
 
+    /** */
+    @GET
+    @Path("cancel")
+    public boolean stopObservation(@NotNull @QueryParam("server") String srv,
+        @NotNull @QueryParam("branch") String branchForTc) {
+            return CtxListener.getInjector(ctx)
+                .getInstance(BuildObserver.class)
+                .stopObservation(new ContributionKey(srv, branchForTc));
+    }
+
     /**
      * @param srvId Server id.
      */
diff --git a/ignite-tc-helper-web/src/main/webapp/visas.html b/ignite-tc-helper-web/src/main/webapp/visas.html
index 87c1864b..cc8a9501 100644
--- a/ignite-tc-helper-web/src/main/webapp/visas.html
+++ b/ignite-tc-helper-web/src/main/webapp/visas.html
@@ -22,7 +22,7 @@
     <br>
     <div id="loadStatus"></div>
     <br>
-    <table id="visasTable" class="cell-border" style="width:100%">
+    <table id="visasTable" class="row-border" style="width:100%">
         <thead>
             <tr class="ui-widget-header ">
                 <th>.</th>
@@ -73,6 +73,16 @@
     $.ajax({ url: "rest/branches/version",  success: showVersionInfo, error: showErrInLoadStatus });
 });
 
+function sendCancelRequest(url) {
+    $.ajax({
+        url: url,
+        success: function (result) {
+            location.reload();
+        },
+        error: showErrInLoadStatus
+    });
+}
+
 function showVisasTable(result) {
     let visasTable = $('#visasTable');
 
@@ -91,15 +101,28 @@
         ],
         columns: [
             {
-                "data": "state",
+                "data": "status",
                 title: "Status",
                 "render": function (data, type, row, meta) {
+                    let res = '';
+
                     if (type === 'display') {
-                        if (data ==='finished' && row.commentUrl)
-                            data = "<a href='" + row.commentUrl + "'>" + data + "</a>";
+                        res += "<table width='100%'><tr><td width='70%' style='vertical-align: middle'>";
+
+                        if (data === 'finished' && row.commentUrl)
+                            res += "<a href='" + row.commentUrl + "'>" + data + "</a>";
+                        else
+                            res += data;
+
+                        res += "</td><td width=\"30%\">";
+
+                        if (row.cancelUrl != null)
+                            res += '<button class="more white short" onclick="sendCancelRequest(\'' + row.cancelUrl + '\')">&times</button>';
+
+                        res += "</td></tr>";
                     }
 
-                    return data;
+                    return res;
                 }
             },
             {
@@ -123,14 +146,13 @@
                 title: "Blockers",
                 "render": function (data, type, row, meta) {
                     if (type === 'display') {
-                        if (row.state != 'finished')
+                        if (row.status != 'finished')
                             data = "";
                     }
 
                     return data;
                 }
             }
-
         ]
     });
 }


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services