diff options
author | Julien Lancelot <julien.lancelot@gmail.com> | 2012-12-03 09:16:04 +0100 |
---|---|---|
committer | Julien Lancelot <julien.lancelot@gmail.com> | 2012-12-03 12:44:01 +0100 |
commit | 534b76c9e499860eca7003a6496f0e21484c976f (patch) | |
tree | 049ba785d53fd988d47dcc54b8bea82788e6cb3b /sonar-batch/src | |
parent | 852abadb6d99037d951f025e20b7a376c2f4a83e (diff) | |
download | sonarqube-534b76c9e499860eca7003a6496f0e21484c976f.tar.gz sonarqube-534b76c9e499860eca7003a6496f0e21484c976f.zip |
SONAR-3306 Use a semaphore to prevent launching several analysis of the same project at the same time
Diffstat (limited to 'sonar-batch/src')
5 files changed, 524 insertions, 0 deletions
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchModule.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchModule.java index de3fdcfffb0..949030df4f6 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchModule.java +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchModule.java @@ -103,6 +103,7 @@ public class BatchModule extends Module { container.addSingleton(DefaultUserFinder.class); container.addSingleton(ResourceTypes.class); container.addSingleton(MetricProvider.class); + container.addSingleton(CheckSemaphore.class); } private void registerDatabaseComponents() { diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/CheckSemaphore.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/CheckSemaphore.java new file mode 100644 index 00000000000..c3f7a6086e4 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/CheckSemaphore.java @@ -0,0 +1,101 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.batch.bootstrap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Project; +import org.sonar.api.utils.SonarException; +import org.sonar.batch.ProjectTree; +import org.sonar.core.persistence.Lock; +import org.sonar.core.persistence.SemaphoreDao; + +public class CheckSemaphore { + + private static final Logger LOG = LoggerFactory.getLogger(CheckSemaphore.class); + + private final SemaphoreDao semaphoreDao; + private final ProjectTree projectTree; + private final Settings settings; + + public CheckSemaphore(SemaphoreDao semaphoreDao, ProjectTree projectTree, Settings settings) { + this.semaphoreDao = semaphoreDao; + this.projectTree = projectTree; + this.settings = settings; + } + + public void start() { + if (!isInDryRunMode()) { + Lock lock = acquire(); + if (!lock.isAcquired()) { + LOG.error(getErrorMessage(lock)); + throw new SonarException("The project is already been analysing."); + } + } + } + + private String getErrorMessage(Lock lock) { + long duration = lock.getDurationSinceLocked(); + DurationLabel durationLabel = new DurationLabel(); + String durationDisplay = durationLabel.label(duration); + + return "It looks like an analysis of '"+ getProject().getName() +"' is already running (started "+ durationDisplay +"). " + + "If this is not the case, it probably means that previous analysis was interrupted " + + "and you should then force a re-run by using the option '"+ CoreProperties.FORCE_ANALYSIS +"=true'."; + } + + public void stop() { + if (!isInDryRunMode()) { + release(); + } + } + + private Lock acquire() { + LOG.debug("Acquire semaphore on project : {}", getProject()); + if (!isForceAnalyseActivated()) { + return semaphoreDao.acquire(getSemaphoreKey()); + } else { + return semaphoreDao.acquire(getSemaphoreKey(), 0); + } + } + + private void release() { + LOG.debug("Release semaphore on project : {}", getProject()); + semaphoreDao.release(getSemaphoreKey()); + } + + private String getSemaphoreKey() { + return "batch-" + getProject().getKey(); + } + + private Project getProject() { + return projectTree.getRootProject(); + } + + private boolean isInDryRunMode() { + return settings.getBoolean(CoreProperties.DRY_RUN); + } + + private boolean isForceAnalyseActivated() { + return settings.getBoolean(CoreProperties.FORCE_ANALYSIS); + } +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/DurationLabel.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/DurationLabel.java new file mode 100644 index 00000000000..bc3263fa472 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/DurationLabel.java @@ -0,0 +1,139 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.batch.bootstrap; + +import java.text.MessageFormat; + +public class DurationLabel { + + private String prefixAgo = null; + private String suffixAgo = "ago"; + private String seconds = "less than a minute"; + private String minute = "about a minute"; + private String minutes = "{0} minutes"; + private String hour = "about an hour"; + private String hours = "{0} hours"; + private String day = "a day"; + private String days = "{0} days"; + private String month = "about a month"; + private String months = "{0} months"; + private String year = "about a year"; + private String years = "{0} years"; + + public String label(long durationInMillis) { + double seconds = durationInMillis / 1000; + double minutes = seconds / 60; + double hours = minutes / 60; + double days = hours / 24; + double years = days / 365; + + final String time; + if (seconds < 45) { + time = this.seconds; + } else if (seconds < 90) { + time = this.minute; + } else if (minutes < 45) { + time = MessageFormat.format(this.minutes, Math.round(minutes)); + } else if (minutes < 90) { + time = this.hour; + } else if (hours < 24) { + time = MessageFormat.format(this.hours, Math.round(hours)); + } else if (hours < 48) { + time = this.day; + } else if (days < 30) { + time = MessageFormat.format(this.days, Math.floor(days)); + } else if (days < 60) { + time = this.month; + } else if (days < 365) { + time = MessageFormat.format(this.months, Math.floor(days / 30)); + } else if (years < 2) { + time = this.year; + } else { + time = MessageFormat.format(this.years, Math.floor(years)); + } + + return join(prefixAgo, time, suffixAgo); + } + + public String join(String prefix, String time, String suffix) { + StringBuilder joined = new StringBuilder(); + if (prefix != null && prefix.length() > 0) { + joined.append(prefix).append(' '); + } + joined.append(time); + if (suffix != null && suffix.length() > 0) { + joined.append(' ').append(suffix); + } + return joined.toString(); + } + + public String getPrefixAgo() { + return prefixAgo; + } + + public String getSuffixAgo() { + return suffixAgo; + } + + public String getSeconds() { + return seconds; + } + + public String getMinute() { + return minute; + } + + public String getMinutes() { + return minutes; + } + + public String getHour() { + return hour; + } + + public String getHours() { + return hours; + } + + public String getDay() { + return day; + } + + public String getDays() { + return days; + } + + public String getMonth() { + return month; + } + + public String getMonths() { + return months; + } + + public String getYear() { + return year; + } + + public String getYears() { + return years; + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/bootstrap/CheckSemaphoreTest.java b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/CheckSemaphoreTest.java new file mode 100644 index 00000000000..d003336713e --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/CheckSemaphoreTest.java @@ -0,0 +1,144 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.batch.bootstrap; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Project; +import org.sonar.api.utils.SonarException; +import org.sonar.batch.ProjectTree; +import org.sonar.core.persistence.Lock; +import org.sonar.core.persistence.SemaphoreDao; + +import java.util.Date; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CheckSemaphoreTest { + + private CheckSemaphore checkSemaphore; + + private SemaphoreDao semaphoreDao; + private ProjectTree projectTree; + private Settings settings; + + private Project project; + private Lock lock; + + @Before + public void setUp() { + lock = mock(Lock.class); + + semaphoreDao = mock(SemaphoreDao.class); + when(semaphoreDao.acquire(anyString())).thenReturn(lock); + when(semaphoreDao.acquire(anyString(), anyInt())).thenReturn(lock); + + projectTree = mock(ProjectTree.class); + settings = new Settings(); + setDryRunMode(false); + setForceMode(false); + + project = new Project("key", "branch", "name"); + when(projectTree.getRootProject()).thenReturn(project); + + checkSemaphore = new CheckSemaphore(semaphoreDao, projectTree, settings); + } + + @Test + public void shouldAcquireSemaphore() { + when(lock.isAcquired()).thenReturn(true); + checkSemaphore.start(); + + verify(semaphoreDao).acquire(anyString()); + } + + @Test + public void shouldUseProjectKeyInTheKeyOfTheSemaphore() { + project = new Project("key"); + when(projectTree.getRootProject()).thenReturn(project); + + when(lock.isAcquired()).thenReturn(true); + checkSemaphore.start(); + + verify(semaphoreDao).acquire("batch-key"); + } + + @Test + public void shouldUseProjectKeyAndBranchIfExistingInTheKeyOfTheSemaphore() { + when(lock.isAcquired()).thenReturn(true); + checkSemaphore.start(); + + verify(semaphoreDao).acquire("batch-key:branch"); + } + + @Test + public void shouldAcquireSemaphoreIfForceAnalyseActivated() { + setForceMode(true); + when(lock.isAcquired()).thenReturn(true); + checkSemaphore.start(); + verify(semaphoreDao).acquire(anyString(), anyInt()); + } + + @Test(expected = SonarException.class) + public void shouldNotAcquireSemaphoreIfTheProjectIsAlreadyBeenAnalysing() { + when(lock.getLocketAt()).thenReturn(new Date()); + when(lock.isAcquired()).thenReturn(false); + checkSemaphore.start(); + verify(semaphoreDao, never()).acquire(anyString()); + } + + @Test + public void shouldNotAcquireSemaphoreInDryRunMode() { + setDryRunMode(true); + settings = new Settings().setProperty(CoreProperties.DRY_RUN, true); + checkSemaphore.start(); + verify(semaphoreDao, never()).acquire(anyString()); + verify(semaphoreDao, never()).acquire(anyString(), anyInt()); + } + + @Test + public void shouldReleaseSemaphore() { + checkSemaphore.stop(); + verify(semaphoreDao).release(anyString()); + } + + @Test + public void shouldNotReleaseSemaphoreInDryRunMode() { + setDryRunMode(true); + checkSemaphore.stop(); + verify(semaphoreDao, never()).release(anyString()); + } + + private void setDryRunMode(boolean isInDryRunMode) { + settings.setProperty(CoreProperties.DRY_RUN, isInDryRunMode); + } + + private void setForceMode(boolean isInForcedMode) { + settings.setProperty(CoreProperties.FORCE_ANALYSIS, isInForcedMode); + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/bootstrap/DurationLabelTest.java b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/DurationLabelTest.java new file mode 100644 index 00000000000..39430928012 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/DurationLabelTest.java @@ -0,0 +1,139 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.batch.bootstrap; + +import org.junit.Test; + +import java.text.MessageFormat; + +import static org.fest.assertions.Assertions.assertThat; + +public class DurationLabelTest { + + private static final long SECOND = 1000; // One second in milliseconds + private static final long MINUTE = 60 * SECOND; // One minute in milliseconds + private static final long HOUR = 60 * MINUTE; // One hour in milliseconds + private static final long DAY = 24 * HOUR; // One day in milliseconds + private static final long MONTH = 30 * DAY; // 30 days in milliseconds + private static final long YEAR = 365 * DAY; // 365 days in milliseconds + + @Test + public void testAgoSeconds() { + DurationLabel durationLabel = new DurationLabel(); + String label = durationLabel.label(now() - System.currentTimeMillis()); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), durationLabel.getSeconds(), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoMinute() { + DurationLabel durationLabel = new DurationLabel(); + String label = durationLabel.label(now() - ago(MINUTE)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), durationLabel.getMinute(), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoMinutes() { + DurationLabel durationlabel = new DurationLabel(); + int minutes = 2; + String label = durationlabel.label(now() - ago(minutes * MINUTE)); + String expected = durationlabel.join(durationlabel.getPrefixAgo(), + MessageFormat.format(durationlabel.getMinutes(), minutes), durationlabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoHour() { + DurationLabel durationLabel = new DurationLabel(); + String label = durationLabel.label(now() - ago(HOUR)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), durationLabel.getHour(), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoHours() { + DurationLabel durationLabel = new DurationLabel(); + long hours = 3; + String label = durationLabel.label(now() - ago(hours * HOUR)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), MessageFormat.format(durationLabel.getHours(), hours), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoDay() { + DurationLabel durationLabel = new DurationLabel(); + String label = durationLabel.label(now() - ago(30 * HOUR)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), durationLabel.getDay(), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoDays() { + DurationLabel durationLabel = new DurationLabel(); + long days = 4; + String label = durationLabel.label(now() - ago(days * DAY)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), MessageFormat.format(durationLabel.getDays(), days), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoMonth() { + DurationLabel durationLabel = new DurationLabel(); + String label = durationLabel.label(now() - ago(35 * DAY)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), durationLabel.getMonth(), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testAgoMonths() { + DurationLabel durationLabel = new DurationLabel(); + long months = 2; + String label = durationLabel.label(now() - ago(months * MONTH)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), MessageFormat.format(durationLabel.getMonths(), months), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testYearAgo() { + DurationLabel durationLabel = new DurationLabel(); + String label = durationLabel.label(now() - ago(14 * MONTH)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), durationLabel.getYear(), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + @Test + public void testYearsAgo() { + DurationLabel durationLabel = new DurationLabel(); + long years = 7; + String label = durationLabel.label(now() - ago(years * YEAR)); + String expected = durationLabel.join(durationLabel.getPrefixAgo(), MessageFormat.format(durationLabel.getYears(), years), durationLabel.getSuffixAgo()); + assertThat(label).isEqualTo(expected); + } + + private long ago(long offset) { + return System.currentTimeMillis() - offset; + } + + private long now() { + return System.currentTimeMillis(); + } + +} |