]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3306 Use a semaphore to prevent launching several analysis of the same project...
authorJulien Lancelot <julien.lancelot@gmail.com>
Mon, 3 Dec 2012 08:16:04 +0000 (09:16 +0100)
committerJulien Lancelot <julien.lancelot@gmail.com>
Mon, 3 Dec 2012 11:44:01 +0000 (12:44 +0100)
15 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DatabaseSemaphoreImpl.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/DatabaseSemaphoreImplTest.java
sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchModule.java
sonar-batch/src/main/java/org/sonar/batch/bootstrap/CheckSemaphore.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/bootstrap/DurationLabel.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/bootstrap/CheckSemaphoreTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/bootstrap/DurationLabelTest.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/persistence/Lock.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java
sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDto.java
sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java
sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml
sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java

index 5c992b4ed358e5688a80fbe7803430698e6c57c6..813a679bce94c35bfd587452a09147e97b620873 100644 (file)
@@ -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) {
index c640159ba67f0a0a7472c90cf7c6978d611950fd..e4f99459d67aec0ccf993f9061abcc3e57702ed4 100644 (file)
 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);
index de3fdcfffb04842013532a717eace55884859f13..949030df4f68f96ba5b7273a1ab80a20699840c6 100644 (file)
@@ -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 (file)
index 0000000..c3f7a60
--- /dev/null
@@ -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 (file)
index 0000000..bc3263f
--- /dev/null
@@ -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 (file)
index 0000000..d003336
--- /dev/null
@@ -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 (file)
index 0000000..3943092
--- /dev/null
@@ -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 (file)
index 0000000..1e47fbc
--- /dev/null
@@ -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;
+  }
+}
index 0ef2b7e79ee53e27fad42865b747e25b1d1c9512..bf46da2839da4e3813b885a968321eda62871218 100644 (file)
@@ -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);
index 3db88347dd9a43dc31a9416bb0b396546ea4498d..28e13c08d169407c903ad6c2e1189d74e77f9e78 100644 (file)
@@ -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;
   }
 }
index 77434345069fe76ce177cab8d8dc22dbd76540bc..7554ac76a6def2c898971b5ea3f9df132d33b4a7 100644 (file)
@@ -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;
+  }
 }
index 4e862baa61a70f8841a3eac57327a1f8844e7cb7..566708e0c0a4fe8c80493bcd2e6262d02c0e2c53 100644 (file)
@@ -32,4 +32,7 @@ public interface SemaphoreMapper {
   Date now();
 
   void release(String name);
+
+  SemaphoreDto selectSemaphore(@Param("name") String name);
+
 }
index cf824381d16ee531fd2a13fedc37d4b6db50e283..d1a8e6f48e2994009aaccf67a279a42c24409cc3 100644 (file)
     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>
 
index 5efd621aa302dc7c17b4540a1c9e0c1db99c2eb0..d0a97e07655d2ab6cd41562d601b58899ec654c1 100644 (file)
@@ -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();
           }
         }
index 9ad4e496a477e39ee327cfbaf4bffed8b2b1345c..b5231cb87586e1b512a3be664bec494644b1b243 100644 (file)
@@ -366,4 +366,9 @@ public interface CoreProperties {
    * @since 3.4
    */
   String DRY_RUN = "sonar.dryRun";
+
+  /**
+   * @since 3.4
+   */
+  String FORCE_ANALYSIS = "sonar.forceAnalysis";
 }