ReviewCommentDao.class,
ReviewDao.class,
RuleDao.class,
+ SemaphoreDao.class,
UserDao.class);
}
}
"rules_parameters",
"rules_profiles",
"rule_failures",
+ "semaphores",
"schema_migrations",
"snapshots",
"snapshot_sources",
*/
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
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);
--- /dev/null
+/*
+ * 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
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+<?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 < #{lockedBefore}
+ </update>
+
+ <delete id="unlock" parameterType="String">
+ delete from semaphores where name=#{id}
+ </delete>
+
+</mapper>
+
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;
"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
-- ----------------------------------------------
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");
} 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);
}
}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+ }
+}
--- /dev/null
+<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
--- /dev/null
+#
+# 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