From: Julien Lancelot Date: Mon, 3 Dec 2012 08:16:04 +0000 (+0100) Subject: SONAR-3306 Use a semaphore to prevent launching several analysis of the same project... X-Git-Tag: 3.4~213 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=534b76c9e499860eca7003a6496f0e21484c976f;p=sonarqube.git SONAR-3306 Use a semaphore to prevent launching several analysis of the same project at the same time --- 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 < #{lockedBefore} + + AND locked_at < #{lockedBefore} + delete from semaphores where name=#{id} + + 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(); @@ -111,6 +153,41 @@ public class SemaphoreDaoTest extends AbstractDaoTestCase { assertThat(isRecent(semaphore.lockedAt, 60)).isTrue(); } + @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"; }