From: Simon Brandhof Date: Fri, 19 Oct 2012 08:31:08 +0000 (+0200) Subject: SONAR-3887 API: new database semaphores X-Git-Tag: 3.4~468 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=3badf76805322e1b0e5c7dd87a8c7320a2414d05;p=sonarqube.git SONAR-3887 API: new database semaphores --- diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java b/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java index fd00b5e92df..0dd2a13dff4 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java @@ -60,6 +60,7 @@ public final class DaoUtils { ReviewCommentDao.class, ReviewDao.class, RuleDao.class, + SemaphoreDao.class, UserDao.class); } } diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java index 89c4dff46ff..a5f48c1176c 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java @@ -80,6 +80,7 @@ public final class DatabaseUtils { "rules_parameters", "rules_profiles", "rule_failures", + "semaphores", "schema_migrations", "snapshots", "snapshot_sources", diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java index e35b9cacb74..4ab9a14c89e 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java @@ -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 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 cfb7b9c0859..0ef2b7e79ee 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 @@ -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 index 00000000000..2519823fadc --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreDao.java @@ -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 index 00000000000..a2be689e1c6 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/persistence/SemaphoreMapper.java @@ -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 index 00000000000..df8dac1d124 --- /dev/null +++ b/sonar-core/src/main/resources/org/sonar/core/persistence/SemaphoreMapper.xml @@ -0,0 +1,27 @@ + + + + + + + INSERT INTO semaphores (name, created_at, updated_at, locked_at) + VALUES (#{name}, current_timestamp, current_timestamp, #{lockedAt}) + + + + + + update semaphores + set updated_at = current_timestamp, locked_at = current_timestamp + where name=#{name} + AND locked_at < #{lockedBefore} + + + + delete from semaphores where name=#{id} + + + + diff --git a/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql b/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql index 9b69e992007..355032ca39c 100644 --- a/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql +++ b/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql @@ -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; diff --git a/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl b/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl index 05ecbdbe0a0..59b0d51ff62 100644 --- a/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl +++ b/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl @@ -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"); diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java b/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java index d93f3f0fd41..8b2431dc67c 100644 --- a/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java +++ b/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java @@ -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 index 00000000000..46617b2830b --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/persistence/SemaphoreDaoTest.java @@ -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 index 00000000000..3d776e975c0 --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/persistence/SemaphoreDaoTest/old_semaphore.xml @@ -0,0 +1,3 @@ + + + \ 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 index 00000000000..16e1e5e1bc7 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/350_create_semaphores.rb @@ -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