]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3887 API: new database semaphores
authorSimon Brandhof <simon.brandhof@gmail.com>
Fri, 19 Oct 2012 08:31:08 +0000 (10:31 +0200)
committerSimon Brandhof <simon.brandhof@gmail.com>
Fri, 19 Oct 2012 08:31:08 +0000 (10:31 +0200)
13 files changed:
sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java
sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java
sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java
sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql
sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl
sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java
sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/persistence/SemaphoreDaoTest/old_semaphore.xml [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/db/migrate/350_create_semaphores.rb [new file with mode: 0644]

index fd00b5e92df58bab63efbe82222bbda9821a25ac..0dd2a13dff4499ef455ac68caa1aec4a189a08b0 100644 (file)
@@ -60,6 +60,7 @@ public final class DaoUtils {
       ReviewCommentDao.class,
       ReviewDao.class,
       RuleDao.class,
+      SemaphoreDao.class,
       UserDao.class);
   }
 }
index 89c4dff46ff7cb4d1859d5a1f0b70a5e1f1a88fa..a5f48c1176cbacd6d4fcd120883f9d6fb488c520 100644 (file)
@@ -80,6 +80,7 @@ public final class DatabaseUtils {
     "rules_parameters",
     "rules_profiles",
     "rule_failures",
+    "semaphores",
     "schema_migrations",
     "snapshots",
     "snapshot_sources",
index e35b9cacb74428414fe29e328c815f2ecc135025..4ab9a14c89ead9357954be022eeaf6e458ab2cff 100644 (file)
@@ -35,7 +35,7 @@ import java.util.List;
  */
 public class DatabaseVersion implements BatchComponent, ServerComponent {
 
-  public static final int LAST_VERSION = 335;
+  public static final int LAST_VERSION = 350;
 
   public static enum Status {
     UP_TO_DATE, REQUIRES_UPGRADE, REQUIRES_DOWNGRADE, FRESH_INSTALL
index cfb7b9c0859733f0d218ce95041d7c2f4e348fe7..0ef2b7e79ee53e27fad42865b747e25b1d1c9512 100644 (file)
@@ -116,7 +116,7 @@ public class MyBatis implements BatchComponent, ServerComponent {
     Class<?>[] mappers = {ActiveDashboardMapper.class, AuthorMapper.class, FilterMapper.class, CriterionMapper.class, FilterColumnMapper.class, DashboardMapper.class,
       DependencyMapper.class, DuplicationMapper.class, LoadedTemplateMapper.class, PropertiesMapper.class, PurgeMapper.class,
       ResourceKeyUpdaterMapper.class, ResourceIndexerMapper.class, ResourceMapper.class, ResourceSnapshotMapper.class, ReviewCommentMapper.class,
-      ReviewMapper.class, RoleMapper.class, RuleMapper.class, SchemaMigrationMapper.class, UserMapper.class, WidgetMapper.class, WidgetPropertyMapper.class,
+      ReviewMapper.class, RoleMapper.class, RuleMapper.class, SchemaMigrationMapper.class, SemaphoreMapper.class, UserMapper.class, WidgetMapper.class, WidgetPropertyMapper.class,
       MeasureMapper.class};
     loadMappers(conf, mappers);
     configureLogback(mappers);
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
new file mode 100644 (file)
index 0000000..2519823
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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 com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import org.apache.commons.lang.time.DateUtils;
+import org.apache.ibatis.session.SqlSession;
+
+import java.util.Date;
+
+/**
+ * @since 3.4
+ */
+public class SemaphoreDao {
+
+  private final MyBatis mybatis;
+
+  public SemaphoreDao(MyBatis mybatis) {
+    this.mybatis = mybatis;
+  }
+
+  public boolean lock(String name, int durationInSeconds) {
+    Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Semaphore name must not be empty");
+    Preconditions.checkArgument(durationInSeconds > 0, "Semaphore duration must be positive");
+
+    SqlSession session = mybatis.openSession();
+    try {
+      SemaphoreMapper mapper = session.getMapper(SemaphoreMapper.class);
+      initialize(name, session, mapper);
+      return doLock(name, durationInSeconds, session, mapper);
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
+  public void unlock(String name) {
+    Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Semaphore name must not be empty");
+    SqlSession session = mybatis.openSession();
+    try {
+      session.getMapper(SemaphoreMapper.class).unlock(name);
+      session.commit();
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
+  private boolean doLock(String name, int durationInSeconds, SqlSession session, SemaphoreMapper mapper) {
+    Date lockedBefore = DateUtils.addSeconds(mapper.now(), -durationInSeconds);
+    boolean ok = mapper.lock(name, lockedBefore) == 1;
+    session.commit();
+    return ok;
+  }
+
+  private void initialize(String name, SqlSession session, SemaphoreMapper mapper) {
+    try {
+      mapper.initialize(name, org.sonar.api.utils.DateUtils.parseDate("2001-01-01"));
+      session.commit();
+
+    } catch (Exception e) {
+      // probably because of the semaphore already exists
+    }
+  }
+}
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
new file mode 100644 (file)
index 0000000..a2be689
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+
+public interface SemaphoreMapper {
+
+  int initialize(@Param("name") String name, @Param("lockedAt") Date lockedAt);
+
+  int lock(@Param("name") String name, @Param("lockedBefore") Date lockedBefore);
+
+  Date now();
+
+  void unlock(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
new file mode 100644 (file)
index 0000000..df8dac1
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.core.persistence.SemaphoreMapper">
+
+  <insert id="initialize" parameterType="map" useGeneratedKeys="false">
+    INSERT INTO semaphores (name, created_at, updated_at, locked_at)
+    VALUES (#{name}, current_timestamp, current_timestamp, #{lockedAt})
+  </insert>
+
+  <select id="now" resultType="Date">
+    select current_timestamp
+  </select>
+
+  <update id="lock" parameterType="map">
+    update semaphores
+    set updated_at = current_timestamp, locked_at = current_timestamp
+    where name=#{name}
+    AND locked_at &lt; #{lockedBefore}
+  </update>
+
+  <delete id="unlock" parameterType="String">
+    delete from semaphores where name=#{id}
+  </delete>
+
+</mapper>
+
index 9b69e992007049526b829e64251da73f872f1aeb..355032ca39cc51edcf5458d3ae01f47debba7ff9 100644 (file)
@@ -177,6 +177,7 @@ INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('332');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('333');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('334');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('335');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('350');
 
 INSERT INTO USERS(ID, LOGIN, NAME, EMAIL, CRYPTED_PASSWORD, SALT, CREATED_AT, UPDATED_AT, REMEMBER_TOKEN, REMEMBER_TOKEN_EXPIRES_AT) VALUES (1, 'admin', 'Administrator', '', 'a373a0e667abb2604c1fd571eb4ad47fe8cc0878', '48bc4b0d93179b5103fd3885ea9119498e9d161b', '2011-09-26 22:27:48.0', '2011-09-26 22:27:48.0', null, null);
 ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 2;
index 05ecbdbe0a038be0504cbd327e7343cbdc3ac606..59b0d51ff628927c0f0864e7f0ae6d11b055398b 100644 (file)
@@ -505,6 +505,14 @@ CREATE TABLE "AUTHORS" (
   "UPDATED_AT" TIMESTAMP
 );
 
+CREATE TABLE "SEMAPHORES" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "NAME" VARCHAR(4000),
+  "CREATED_AT" TIMESTAMP,
+  "UPDATED_AT" TIMESTAMP,
+  "LOCKED_AT" TIMESTAMP
+);
+
 -- ----------------------------------------------
 -- DDL Statements for indexes
 -- ----------------------------------------------
@@ -604,3 +612,5 @@ CREATE INDEX "INDEX_ACTIVE_RULE_NOTES_ON_ACTIVE_RULE_ID" ON "ACTIVE_RULE_NOTES"
 CREATE INDEX "INDEX_RULE_NOTES_ON_ACTIVE_RULE_ID" ON "RULE_NOTES" ("RULE_ID");
 
 CREATE INDEX "REVIEWS_RID" ON "REVIEWS" ("RESOURCE_ID");
+
+CREATE UNIQUE INDEX "SEMAPHORES_UNIQUE_NAME" ON "SEMAPHORES" ("NAME");
index d93f3f0fd41823e2b52d363a29a9c4a439ef6988..8b2431dc67c7f17953e2eeeb4155b242776585c9 100644 (file)
@@ -73,13 +73,7 @@ public class H2Database implements Database {
     } catch (SQLException e) {
       throw new IllegalStateException("Fail to create schema", e);
     } finally {
-      if (connection != null) {
-        try {
-          connection.close();
-        } catch (SQLException e) {
-          // ignore
-        }
-      }
+      DatabaseUtils.closeQuietly(connection);
     }
   }
 
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
new file mode 100644 (file)
index 0000000..46617b2
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * 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 org.apache.commons.lang.time.DateUtils;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Timestamp;
+import java.util.Date;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class SemaphoreDaoTest extends AbstractDaoTestCase {
+
+  @Test
+  public void create_and_lock_semaphore() throws Exception {
+    SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+    assertThat(dao.lock("foo", 60)).isTrue();
+
+    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.unlock("foo");
+    assertThat(selectSemaphore("foo")).isNull();
+  }
+
+  @Test
+  public void fail_to_acquire_locked_semaphore() throws Exception {
+    setupData("old_semaphore");
+    SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+    assertThat(dao.lock("foo", Integer.MAX_VALUE)).isFalse();
+
+    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 acquire_long_locked_semaphore() throws Exception {
+    setupData("old_semaphore");
+    SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+    assertThat(dao.lock("foo", 60)).isTrue();
+
+    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();
+  }
+
+  @Test
+  public void test_concurrent_locks() throws Exception {
+    SemaphoreDao dao = new SemaphoreDao(getMyBatis());
+
+    for (int tests = 0; tests < 5000; tests++) {
+      dao.unlock("my-lock");
+      int size = 5;
+      CyclicBarrier barrier = new CyclicBarrier(size);
+      CountDownLatch latch = new CountDownLatch(size);
+
+      AtomicInteger locks = new AtomicInteger(0);
+      for (int i = 0; i < size; i++) {
+        new Runner(dao, locks, barrier, latch).start();
+      }
+      latch.await();
+
+      // semaphore was locked only 1 time
+      assertThat(locks.get()).isEqualTo(1);
+    }
+  }
+
+  private Semaphore selectSemaphore(String name) throws Exception {
+    Connection connection = getConnection();
+    PreparedStatement statement = null;
+    ResultSet rs = null;
+    try {
+      statement = connection.prepareStatement("SELECT * FROM semaphores WHERE name='" + name + "'");
+      rs = statement.executeQuery();
+      if (rs.next()) {
+        return new Semaphore(rs.getString("name"), rs.getTimestamp("created_at"), rs.getTimestamp("updated_at"), rs.getTimestamp("locked_at"));
+      }
+      return null;
+    } finally {
+      DatabaseUtils.closeQuietly(rs);
+      DatabaseUtils.closeQuietly(statement);
+      DatabaseUtils.closeQuietly(connection);
+    }
+  }
+
+  private static class Semaphore {
+    String name;
+    Date createdAt, updatedAt, lockedAt;
+
+    private Semaphore(String name, Timestamp createdAt, Timestamp updatedAt, Timestamp lockedAt) {
+      this.name = name;
+      this.createdAt = createdAt;
+      this.updatedAt = updatedAt;
+      this.lockedAt = lockedAt;
+    }
+  }
+
+  private static boolean isRecent(Date date, int durationInSeconds) {
+    Date now = new Date();
+    return date.before(now) && DateUtils.addSeconds(date, durationInSeconds).after(now);
+  }
+
+  private static class Runner extends Thread {
+    SemaphoreDao dao;
+    AtomicInteger locks;
+    CountDownLatch latch;
+    CyclicBarrier barrier;
+
+    Runner(SemaphoreDao dao, AtomicInteger atomicSeq, CyclicBarrier barrier, CountDownLatch latch) {
+      this.dao = dao;
+      this.locks = atomicSeq;
+      this.latch = latch;
+      this.barrier = barrier;
+    }
+
+    public void run() {
+      try {
+        barrier.await();
+        for (int i = 0; i < 100; i++) {
+          if (dao.lock("my-lock", 60 * 5)) {
+            locks.incrementAndGet();
+          }
+        }
+        latch.countDown();
+
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+    }
+  }
+}
diff --git a/sonar-core/src/test/resources/org/sonar/core/persistence/SemaphoreDaoTest/old_semaphore.xml b/sonar-core/src/test/resources/org/sonar/core/persistence/SemaphoreDaoTest/old_semaphore.xml
new file mode 100644 (file)
index 0000000..3d776e9
--- /dev/null
@@ -0,0 +1,3 @@
+<dataset>
+  <semaphores id="1" name="foo" created_at="2010-01-25" updated_at="2010-01-25" locked_at="2010-01-25"/>
+</dataset>
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/350_create_semaphores.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/350_create_semaphores.rb
new file mode 100644 (file)
index 0000000..16e1e5e
--- /dev/null
@@ -0,0 +1,35 @@
+#
+# Sonar, entreprise quality control 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
+#
+
+#
+# Sonar 3.4
+#
+class CreateSemaphores < ActiveRecord::Migration
+
+  def self.up
+    create_table :semaphores do |t|
+      t.string :name, :limit => 4000, :null => false
+      t.datetime :locked_at
+      t.timestamps
+    end
+    add_index :semaphores, :name, :unique => true, :name => 'uniq_semaphore_names'
+  end
+
+end