summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@gmail.com>2012-12-03 09:16:04 +0100
committerJulien Lancelot <julien.lancelot@gmail.com>2012-12-03 12:44:01 +0100
commit534b76c9e499860eca7003a6496f0e21484c976f (patch)
tree049ba785d53fd988d47dcc54b8bea82788e6cb3b
parent852abadb6d99037d951f025e20b7a376c2f4a83e (diff)
downloadsonarqube-534b76c9e499860eca7003a6496f0e21484c976f.tar.gz
sonarqube-534b76c9e499860eca7003a6496f0e21484c976f.zip
SONAR-3306 Use a semaphore to prevent launching several analysis of the same project at the same time
-rw-r--r--plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DatabaseSemaphoreImpl.java2
-rw-r--r--plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/DatabaseSemaphoreImplTest.java7
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchModule.java1
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/bootstrap/CheckSemaphore.java101
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/bootstrap/DurationLabel.java139
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/bootstrap/CheckSemaphoreTest.java144
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/bootstrap/DurationLabelTest.java139
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/Lock.java72
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java39
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java54
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDto.java20
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java3
-rw-r--r--sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml8
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java85
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java5
15 files changed, 800 insertions, 19 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DatabaseSemaphoreImpl.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DatabaseSemaphoreImpl.java
index 5c992b4ed35..813a679bce9 100644
--- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DatabaseSemaphoreImpl.java
+++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DatabaseSemaphoreImpl.java
@@ -34,7 +34,7 @@ public class DatabaseSemaphoreImpl implements DatabaseSemaphore {
}
public boolean acquire(String name, int maxDurationInSeconds) {
- return dao.acquire(name, maxDurationInSeconds);
+ return dao.acquire(name, maxDurationInSeconds).isAcquired();
}
public void release(String name) {
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/DatabaseSemaphoreImplTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/DatabaseSemaphoreImplTest.java
index c640159ba67..e4f99459d67 100644
--- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/DatabaseSemaphoreImplTest.java
+++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/DatabaseSemaphoreImplTest.java
@@ -20,16 +20,23 @@
package org.sonar.plugins.core;
import org.junit.Test;
+import org.sonar.core.persistence.Lock;
import org.sonar.core.persistence.SemaphoreDao;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public class DatabaseSemaphoreImplTest {
@Test
public void should_be_a_bridge_over_dao() {
+ Lock lock = mock(Lock.class);
SemaphoreDao dao = mock(SemaphoreDao.class);
+ when(dao.acquire(anyString(), anyInt())).thenReturn(lock);
+
DatabaseSemaphoreImpl impl = new DatabaseSemaphoreImpl(dao);
impl.acquire("do-xxx", 50000);
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();
+ }
+
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/Lock.java b/sonar-core/src/main/java/org/sonar/core/persistence/Lock.java
new file mode 100644
index 00000000000..1e47fbcba2e
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/Lock.java
@@ -0,0 +1,72 @@
+/*
+ * 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.core.persistence;
+
+import java.util.Date;
+
+/**
+ * @since 3.4
+ */
+public class Lock {
+
+ private String name;
+ private boolean acquired;
+ private Date locketAt;
+ private Date createdAt;
+ private Date updatedAt;
+ private Long durationSinceLocked;
+
+ public Lock(String name, boolean acquired, Date locketAt, Date createdAt, Date updatedAt) {
+ this.name = name;
+ this.acquired = acquired;
+ this.locketAt = locketAt;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Date getLocketAt() {
+ return locketAt;
+ }
+
+ public Date getCreatedAt() {
+ return createdAt;
+ }
+
+ public Date getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public boolean isAcquired() {
+ return acquired;
+ }
+
+ public Long getDurationSinceLocked() {
+ return durationSinceLocked;
+ }
+
+ public void setDurationSinceLocked(Long durationSinceLocked) {
+ this.durationSinceLocked = durationSinceLocked;
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java b/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
index 0ef2b7e79ee..bf46da2839d 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
@@ -24,7 +24,11 @@ import com.google.common.io.Closeables;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.Environment;
-import org.apache.ibatis.session.*;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.type.JdbcType;
import org.slf4j.LoggerFactory;
@@ -35,19 +39,36 @@ import org.sonar.api.database.model.MeasureData;
import org.sonar.api.database.model.MeasureMapper;
import org.sonar.api.database.model.MeasureModel;
import org.sonar.core.config.Logback;
-import org.sonar.core.dashboard.*;
+import org.sonar.core.dashboard.ActiveDashboardDto;
+import org.sonar.core.dashboard.ActiveDashboardMapper;
+import org.sonar.core.dashboard.DashboardDto;
+import org.sonar.core.dashboard.DashboardMapper;
+import org.sonar.core.dashboard.WidgetDto;
+import org.sonar.core.dashboard.WidgetMapper;
+import org.sonar.core.dashboard.WidgetPropertyDto;
+import org.sonar.core.dashboard.WidgetPropertyMapper;
import org.sonar.core.dependency.DependencyDto;
import org.sonar.core.dependency.DependencyMapper;
import org.sonar.core.dependency.ResourceSnapshotDto;
import org.sonar.core.dependency.ResourceSnapshotMapper;
import org.sonar.core.duplication.DuplicationMapper;
import org.sonar.core.duplication.DuplicationUnitDto;
-import org.sonar.core.filter.*;
+import org.sonar.core.filter.CriterionDto;
+import org.sonar.core.filter.CriterionMapper;
+import org.sonar.core.filter.FilterColumnDto;
+import org.sonar.core.filter.FilterColumnMapper;
+import org.sonar.core.filter.FilterDto;
+import org.sonar.core.filter.FilterMapper;
import org.sonar.core.properties.PropertiesMapper;
import org.sonar.core.properties.PropertyDto;
import org.sonar.core.purge.PurgeMapper;
import org.sonar.core.purge.PurgeableSnapshotDto;
-import org.sonar.core.resource.*;
+import org.sonar.core.resource.ResourceDto;
+import org.sonar.core.resource.ResourceIndexDto;
+import org.sonar.core.resource.ResourceIndexerMapper;
+import org.sonar.core.resource.ResourceKeyUpdaterMapper;
+import org.sonar.core.resource.ResourceMapper;
+import org.sonar.core.resource.SnapshotDto;
import org.sonar.core.review.ReviewCommentDto;
import org.sonar.core.review.ReviewCommentMapper;
import org.sonar.core.review.ReviewDto;
@@ -56,7 +77,14 @@ import org.sonar.core.rule.RuleDto;
import org.sonar.core.rule.RuleMapper;
import org.sonar.core.template.LoadedTemplateDto;
import org.sonar.core.template.LoadedTemplateMapper;
-import org.sonar.core.user.*;
+import org.sonar.core.user.AuthorDto;
+import org.sonar.core.user.AuthorMapper;
+import org.sonar.core.user.GroupDto;
+import org.sonar.core.user.GroupRoleDto;
+import org.sonar.core.user.RoleMapper;
+import org.sonar.core.user.UserDto;
+import org.sonar.core.user.UserMapper;
+import org.sonar.core.user.UserRoleDto;
import java.io.InputStream;
@@ -105,6 +133,7 @@ public class MyBatis implements BatchComponent, ServerComponent {
loadAlias(conf, "ReviewComment", ReviewCommentDto.class);
loadAlias(conf, "Rule", RuleDto.class);
loadAlias(conf, "Snapshot", SnapshotDto.class);
+ loadAlias(conf, "Semaphore", SemaphoreDto.class);
loadAlias(conf, "SchemaMigration", SchemaMigrationDto.class);
loadAlias(conf, "User", UserDto.class);
loadAlias(conf, "UserRole", UserRoleDto.class);
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java
index 3db88347dd9..28e13c08d16 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java
@@ -37,15 +37,37 @@ public class SemaphoreDao {
this.mybatis = mybatis;
}
- public boolean acquire(String name, int maxDurationInSeconds) {
+ public Lock acquire(String name, int maxDurationInSeconds) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Semaphore name must not be empty");
- Preconditions.checkArgument(maxDurationInSeconds > 0, "Semaphore max duration must be positive: " + maxDurationInSeconds);
+ Preconditions.checkArgument(maxDurationInSeconds >= 0, "Semaphore max duration must be positive: " + maxDurationInSeconds);
SqlSession session = mybatis.openSession();
try {
SemaphoreMapper mapper = session.getMapper(SemaphoreMapper.class);
- initialize(name, session, mapper);
- return doAcquire(name, maxDurationInSeconds, session, mapper);
+ Date lockedAt = org.sonar.api.utils.DateUtils.parseDate("2001-01-01");
+ createSemaphore(name, lockedAt, session, mapper);
+ boolean isAcquired = doAcquire(name, maxDurationInSeconds, session, mapper);
+ SemaphoreDto semaphore = mapper.selectSemaphore(name);
+ return createLock(semaphore, mapper, isAcquired);
+ } finally {
+ MyBatis.closeQuietly(session);
+ }
+ }
+
+ public Lock acquire(String name) {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Semaphore name must not be empty");
+
+ SqlSession session = mybatis.openSession();
+ try {
+ SemaphoreMapper mapper = session.getMapper(SemaphoreMapper.class);
+ SemaphoreDto semaphore = mapper.selectSemaphore(name);
+ Date now = mapper.now();
+ if (semaphore != null) {
+ return createLock(semaphore, mapper, false);
+ } else {
+ semaphore = createSemaphore(name, now, session, mapper);
+ return createLock(semaphore, mapper, true);
+ }
} finally {
MyBatis.closeQuietly(session);
}
@@ -69,17 +91,33 @@ public class SemaphoreDao {
return ok;
}
- private void initialize(String name, SqlSession session, SemaphoreMapper mapper) {
+ private SemaphoreDto createSemaphore(String name, Date lockedAt, SqlSession session, SemaphoreMapper mapper) {
try {
SemaphoreDto semaphore = new SemaphoreDto()
- .setName(name)
- .setLockedAt(org.sonar.api.utils.DateUtils.parseDate("2001-01-01"));
+ .setName(name)
+ .setLockedAt(lockedAt);
mapper.initialize(semaphore);
session.commit();
-
+ return semaphore;
} catch (Exception e) {
// probably because of the semaphore already exists
session.rollback();
+ return null;
+ }
+ }
+
+ private Lock createLock(SemaphoreDto semaphore, SemaphoreMapper mapper, boolean acquired) {
+ Lock lock = new Lock(semaphore.getName(), acquired, semaphore.getLockedAt(), semaphore.getCreatedAt(), semaphore.getUpdatedAt());
+ if (!acquired) {
+ lock.setDurationSinceLocked(getDurationSinceLocked(semaphore, mapper));
}
+ return lock;
+ }
+
+ private long getDurationSinceLocked(SemaphoreDto semaphore, SemaphoreMapper mapper) {
+ long now = mapper.now().getTime();
+ semaphore.getLockedAt();
+ long locketAt = semaphore.getLockedAt().getTime();
+ return now - locketAt;
}
}
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDto.java b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDto.java
index 77434345069..7554ac76a6d 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDto.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDto.java
@@ -31,6 +31,8 @@ public class SemaphoreDto {
private String name;
private String checksum;
private Date lockedAt;
+ private Date createdAt;
+ private Date updatedAt;
public String getName() {
return name;
@@ -59,4 +61,22 @@ public class SemaphoreDto {
this.id = id;
return this;
}
+
+ public Date getCreatedAt() {
+ return createdAt;
+ }
+
+ public SemaphoreDto setCreatedAt(Date createdAt) {
+ this.createdAt = createdAt;
+ return this;
+ }
+
+ public Date getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public SemaphoreDto setUpdatedAt(Date updatedAt) {
+ this.updatedAt = updatedAt;
+ return this;
+ }
}
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java
index 4e862baa61a..566708e0c0a 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java
@@ -32,4 +32,7 @@ public interface SemaphoreMapper {
Date now();
void release(String name);
+
+ SemaphoreDto selectSemaphore(@Param("name") String name);
+
}
diff --git a/sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml b/sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml
index cf824381d16..d1a8e6f48e2 100644
--- a/sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml
+++ b/sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml
@@ -28,12 +28,18 @@
update semaphores
set updated_at = current_timestamp, locked_at = current_timestamp
where name=#{name}
- AND locked_at &lt; #{lockedBefore}
+ <if test="lockedBefore != null">
+ AND locked_at &lt; #{lockedBefore}
+ </if>
</update>
<delete id="release" parameterType="String">
delete from semaphores where name=#{id}
</delete>
+ <select id="selectSemaphore" parameterType="String" resultType="Semaphore">
+ select s.id, s.name as name, s.locked_at as lockedAt, s.created_at as createdAt, s.updated_at as updatedAt from semaphores s where s.name=#{name}
+ </select>
+
</mapper>
diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java b/sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java
index 5efd621aa30..d0a97e07655 100644
--- a/sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java
@@ -70,7 +70,45 @@ public class SemaphoreDaoTest extends AbstractDaoTestCase {
@Test
public void create_and_acquire_semaphore() throws Exception {
SemaphoreDao dao = new SemaphoreDao(getMyBatis());
- assertThat(dao.acquire("foo", 60)).isTrue();
+ Lock lock = dao.acquire("foo", 60);
+ assertThat(lock.isAcquired()).isTrue();
+ assertThat(lock.getDurationSinceLocked()).isNull();
+
+ Semaphore semaphore = selectSemaphore("foo");
+ assertThat(semaphore).isNotNull();
+ assertThat(semaphore.name).isEqualTo("foo");
+ assertThat(isRecent(semaphore.createdAt, 60)).isTrue();
+ assertThat(isRecent(semaphore.updatedAt, 60)).isTrue();
+ assertThat(isRecent(semaphore.lockedAt, 60)).isTrue();
+
+ dao.release("foo");
+ assertThat(selectSemaphore("foo")).isNull();
+ }
+
+ @Test
+ public void create_and_acquire_semaphore_when_timeout_is_zeo() throws Exception {
+ SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+ Lock lock = dao.acquire("foo", 0);
+ assertThat(lock.isAcquired()).isTrue();
+ assertThat(lock.getDurationSinceLocked()).isNull();
+
+ Semaphore semaphore = selectSemaphore("foo");
+ assertThat(semaphore).isNotNull();
+ assertThat(semaphore.name).isEqualTo("foo");
+ assertThat(isRecent(semaphore.createdAt, 60)).isTrue();
+ assertThat(isRecent(semaphore.updatedAt, 60)).isTrue();
+ assertThat(isRecent(semaphore.lockedAt, 60)).isTrue();
+
+ dao.release("foo");
+ assertThat(selectSemaphore("foo")).isNull();
+ }
+
+ @Test
+ public void create_and_acquire_semaphore_when_no_timeout() throws Exception {
+ SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+ Lock lock = dao.acquire("foo");
+ assertThat(lock.isAcquired()).isTrue();
+ assertThat(lock.getDurationSinceLocked()).isNull();
Semaphore semaphore = selectSemaphore("foo");
assertThat(semaphore).isNotNull();
@@ -87,7 +125,9 @@ public class SemaphoreDaoTest extends AbstractDaoTestCase {
public void fail_to_acquire_locked_semaphore() throws Exception {
setupData("old_semaphore");
SemaphoreDao dao = new SemaphoreDao(getMyBatis());
- assertThat(dao.acquire("foo", Integer.MAX_VALUE)).isFalse();
+ Lock lock = dao.acquire("foo", Integer.MAX_VALUE);
+ assertThat(lock.isAcquired()).isFalse();
+ assertThat(lock.getDurationSinceLocked()).isNotNull();
Semaphore semaphore = selectSemaphore("foo");
assertThat(semaphore).isNotNull();
@@ -101,7 +141,9 @@ public class SemaphoreDaoTest extends AbstractDaoTestCase {
public void acquire_long_locked_semaphore() throws Exception {
setupData("old_semaphore");
SemaphoreDao dao = new SemaphoreDao(getMyBatis());
- assertThat(dao.acquire("foo", 60)).isTrue();
+ Lock lock = dao.acquire("foo", 60);
+ assertThat(lock.isAcquired()).isTrue();
+ assertThat(lock.getDurationSinceLocked()).isNull();
Semaphore semaphore = selectSemaphore("foo");
assertThat(semaphore).isNotNull();
@@ -112,6 +154,41 @@ public class SemaphoreDaoTest extends AbstractDaoTestCase {
}
@Test
+ public void acquire_locked_semaphore_when_timeout_is_zeo() throws Exception {
+ setupData("old_semaphore");
+ SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+ Lock lock = dao.acquire("foo", 0);
+ assertThat(lock.isAcquired()).isTrue();
+ assertThat(lock.getDurationSinceLocked()).isNull();
+
+ Semaphore semaphore = selectSemaphore("foo");
+ assertThat(semaphore).isNotNull();
+ assertThat(semaphore.name).isEqualTo("foo");
+ assertThat(isRecent(semaphore.createdAt, 60)).isFalse();
+ assertThat(isRecent(semaphore.updatedAt, 60)).isTrue();
+ assertThat(isRecent(semaphore.lockedAt, 60)).isTrue();
+
+ dao.release("foo");
+ assertThat(selectSemaphore("foo")).isNull();
+ }
+
+ @Test
+ public void fail_to_acquire_locked_semaphore_when_no_timeout() throws Exception {
+ setupData("old_semaphore");
+ SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+ Lock lock = dao.acquire("foo");
+ assertThat(lock.isAcquired()).isFalse();
+ assertThat(lock.getDurationSinceLocked()).isNotNull();
+
+ Semaphore semaphore = selectSemaphore("foo");
+ assertThat(semaphore).isNotNull();
+ assertThat(semaphore.name).isEqualTo("foo");
+ assertThat(isRecent(semaphore.createdAt, 60)).isFalse();
+ assertThat(isRecent(semaphore.updatedAt, 60)).isFalse();
+ assertThat(isRecent(semaphore.lockedAt, 60)).isFalse();
+ }
+
+ @Test
public void test_concurrent_locks() throws Exception {
SemaphoreDao dao = new SemaphoreDao(getMyBatis());
@@ -184,7 +261,7 @@ public class SemaphoreDaoTest extends AbstractDaoTestCase {
try {
barrier.await();
for (int i = 0; i < 100; i++) {
- if (dao.acquire("my-lock", 60 * 5)) {
+ if (dao.acquire("my-lock", 60 * 5).isAcquired()) {
locks.incrementAndGet();
}
}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
index 9ad4e496a47..b5231cb8758 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
@@ -366,4 +366,9 @@ public interface CoreProperties {
* @since 3.4
*/
String DRY_RUN = "sonar.dryRun";
+
+ /**
+ * @since 3.4
+ */
+ String FORCE_ANALYSIS = "sonar.forceAnalysis";
}