You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by dp...@apache.org on 2018/11/19 16:34:08 UTC

[ignite-teamcity-bot] branch master updated: IGNITE-10275 Several JIRA comments are issued in case ignite.cache.remove failed. - Fixes #74.

This is an automated email from the ASF dual-hosted git repository.

dpavlov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git


The following commit(s) were added to refs/heads/master by this push:
     new ca99bfd  IGNITE-10275 Several JIRA comments are issued in case ignite.cache.remove failed. - Fixes #74.
ca99bfd is described below

commit ca99bfd60d8c64186179df38cb1a8b0b4f52ff0d
Author: ololo3000 <pm...@gmail.com>
AuthorDate: Mon Nov 19 19:34:00 2018 +0300

    IGNITE-10275 Several JIRA comments are issued in case ignite.cache.remove failed. - Fixes #74.
    
    Signed-off-by: Dmitriy Pavlov <dp...@apache.org>
---
 .../main/java/org/apache/ignite/ci/TcHelper.java   |   8 +-
 .../apache/ignite/ci/observer/BuildObserver.java   |  60 ++++---
 .../org/apache/ignite/ci/observer/BuildsInfo.java  |  84 +++++-----
 .../ignite/ci/observer/CompactBuildsInfo.java      |  34 ++--
 .../apache/ignite/ci/observer/ObserverTask.java    | 174 +++++++++++++--------
 .../tcbot/visa/TcBotTriggerAndSignOffService.java  |  49 ++++--
 .../apache/ignite/ci/tcbot/visa/VisaStatus.java    |   5 +-
 .../ci/web/model/CompactContributionKey.java       |  13 +-
 .../ignite/ci/web/model/ContributionKey.java       |  19 ++-
 .../java/org/apache/ignite/ci/web/model/Visa.java  |   1 -
 .../ci/web/model/hist/VisasHistoryStorage.java     |   9 +-
 .../ignite/ci/web/rest/visa/TcBotVisaService.java  |  13 ++
 ignite-tc-helper-web/src/main/webapp/visas.html    |  36 ++++-
 13 files changed, 322 insertions(+), 183 deletions(-)

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 f3ae04a..7df5074 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 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
 
             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 cf29468..b4686f7 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 @@ public class BuildObserver {
     /** 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 class BuildObserver {
         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 class BuildObserver {
     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 7117169..e02ad5b 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 @@ import org.apache.ignite.ci.web.model.ContributionKey;
  */
 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 class BuildsInfo {
     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 class BuildsInfo {
         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 class BuildsInfo {
             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 c07f05a..df70048 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 @@ public class CompactBuildsInfo {
     /** */
     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 class CompactBuildsInfo {
         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 class CompactBuildsInfo {
             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 8676868..68335c8 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 @@ package org.apache.ignite.ci.observer;
 
 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.di.MonitoredTask;
 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 @@ public class ObserverTask extends TimerTask {
     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 @@ public class ObserverTask extends TimerTask {
     @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 class ObserverTask extends TimerTask {
     @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 751050e..312a033 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 @@ package org.apache.ignite.ci.tcbot.visa;
 
 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.IGitHubConnection;
 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.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.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.apache.ignite.internal.util.typedef.F;
 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 @@ public class TcBotTriggerAndSignOffService {
 
 
     @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 class TcBotTriggerAndSignOffService {
 
         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 class TcBotTriggerAndSignOffService {
 
             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 @@ public class TcBotTriggerAndSignOffService {
 
                     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 class TcBotTriggerAndSignOffService {
         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 1dfe31f..e20c52b 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 @@ public class VisaStatus {
     @Nullable public String ticket;
 
     /** */
-    @Nullable public String state;
+    @Nullable public String status;
 
     /** */
     @Nullable public String commentUrl;
@@ -42,5 +42,8 @@ public class VisaStatus {
     @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 3c6a6f0..217a961 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;
 
 /**
@@ -29,16 +28,17 @@ public class CompactContributionKey {
     public final int srvId;
 
     /** */
-    public final int ticket;
-
-    /** */
     public final int branchForTc;
 
     /** */
     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 class CompactContributionKey {
         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 fa62ea6..570f2c9 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;
+
 /**
  *
  */
@@ -25,15 +27,22 @@ public class ContributionKey {
     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 4d234ba..e826cc4 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 class Visa {
         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 9a50296..28299f7 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 @@ import org.apache.ignite.ci.web.model.VisaRequest;
  */
 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 class VisasHistoryStorage {
 
         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 class VisasHistoryStorage {
     }
 
     /** */
-    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 class VisasHistoryStorage {
     }
 
     /** */
-    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 18e2f60..f70975d 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.Produces;
 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.TcBotTriggerAndSignOffService;
 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 @@ public class TcBotVisaService {
     @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 87c1864..cc8a950 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 @@ $(document).ready(function() {
     $.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 @@ function showVisasTable(result) {
         ],
         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 @@ function showVisasTable(result) {
                 title: "Blockers",
                 "render": function (data, type, row, meta) {
                     if (type === 'display') {
-                        if (row.state != 'finished')
+                        if (row.status != 'finished')
                             data = "";
                     }
 
                     return data;
                 }
             }
-
         ]
     });
 }