mirror of
https://github.com/google/nomulus.git
synced 2025-05-19 10:49:35 +02:00
Add the schema and DAO for Locks in CloudSQL (#462)
* Add the schema and DAO for Locks in CloudSQL * Addresses some comments * Change number on flyway file * Small changes * More small changes * Use checkArgumentNotNull instead of checkNotNull * Address comments * fix javadocs * update persistence
This commit is contained in:
parent
62433c2238
commit
22a879e655
8 changed files with 392 additions and 0 deletions
132
core/src/main/java/google/registry/schema/server/Lock.java
Normal file
132
core/src/main/java/google/registry/schema/server/Lock.java
Normal file
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.schema.server;
|
||||
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.schema.server.Lock.LockId;
|
||||
import google.registry.util.DateTimeUtils;
|
||||
import java.io.Serializable;
|
||||
import java.time.ZonedDateTime;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
import javax.persistence.Table;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/**
|
||||
* A lock on some shared resource.
|
||||
*
|
||||
* <p>Locks are either specific to a tld or global to the entire system, in which case a tld of
|
||||
* {@link GLOBAL} is used.
|
||||
*
|
||||
* <p>This uses a compound primary key as defined in {@link LockId}.
|
||||
*/
|
||||
@Entity
|
||||
@Table
|
||||
@IdClass(LockId.class)
|
||||
public class Lock {
|
||||
|
||||
/** The resource name used to create the lock. */
|
||||
@Column(nullable = false)
|
||||
@Id
|
||||
String resourceName;
|
||||
|
||||
/** The tld used to create the lock. */
|
||||
@Column(nullable = false)
|
||||
@Id
|
||||
String tld;
|
||||
|
||||
/**
|
||||
* Unique log ID of the request that owns this lock.
|
||||
*
|
||||
* <p>When that request is no longer running (is finished), the lock can be considered implicitly
|
||||
* released.
|
||||
*
|
||||
* <p>See {@link RequestStatusCheckerImpl#getLogId} for details about how it's created in
|
||||
* practice.
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
String requestLogId;
|
||||
|
||||
/** When the lock was acquired. Used for logging. */
|
||||
@Column(nullable = false)
|
||||
ZonedDateTime acquiredTime;
|
||||
|
||||
/** When the lock can be considered implicitly released. */
|
||||
@Column(nullable = false)
|
||||
ZonedDateTime expirationTime;
|
||||
|
||||
/** The scope of a lock that is not specific to a single tld. */
|
||||
static final String GLOBAL = "GLOBAL";
|
||||
|
||||
/**
|
||||
* Validate input and create a new {@link Lock} for the given resource name in the specified tld.
|
||||
*/
|
||||
private Lock(
|
||||
String resourceName,
|
||||
String tld,
|
||||
String requestLogId,
|
||||
DateTime acquiredTime,
|
||||
Duration leaseLength) {
|
||||
this.resourceName = checkArgumentNotNull(resourceName, "The resource name cannot be null");
|
||||
this.tld = checkArgumentNotNull(tld, "The tld cannot be null. For a global lock, use GLOBAL");
|
||||
this.requestLogId =
|
||||
checkArgumentNotNull(requestLogId, "The requestLogId of the lock cannot be null");
|
||||
this.acquiredTime =
|
||||
DateTimeUtils.toZonedDateTime(
|
||||
checkArgumentNotNull(acquiredTime, "The acquired time of the lock cannot be null"));
|
||||
checkArgumentNotNull(leaseLength, "The lease length of the lock cannot be null");
|
||||
this.expirationTime = DateTimeUtils.toZonedDateTime(acquiredTime.plus(leaseLength));
|
||||
}
|
||||
|
||||
// Hibernate requires a default constructor.
|
||||
private Lock() {}
|
||||
|
||||
/** Constructs a {@link Lock} object. */
|
||||
public static Lock create(
|
||||
String resourceName,
|
||||
String tld,
|
||||
String requestLogId,
|
||||
DateTime acquiredTime,
|
||||
Duration leaseLength) {
|
||||
checkArgumentNotNull(
|
||||
tld, "The tld cannot be null. To create a global lock, use the createGlobal method");
|
||||
return new Lock(resourceName, tld, requestLogId, acquiredTime, leaseLength);
|
||||
}
|
||||
|
||||
/** Constructs a {@link Lock} object with a {@link GLOBAL} scope. */
|
||||
public static Lock createGlobal(
|
||||
String resourceName, String requestLogId, DateTime acquiredTime, Duration leaseLength) {
|
||||
return new Lock(resourceName, GLOBAL, requestLogId, acquiredTime, leaseLength);
|
||||
}
|
||||
|
||||
static class LockId extends ImmutableObject implements Serializable {
|
||||
|
||||
String resourceName;
|
||||
|
||||
String tld;
|
||||
|
||||
private LockId() {}
|
||||
|
||||
LockId(String resourceName, String tld) {
|
||||
this.resourceName = checkArgumentNotNull(resourceName, "The resource name cannot be null");
|
||||
this.tld = tld;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.schema.server;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.schema.server.Lock.GLOBAL;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import google.registry.schema.server.Lock.LockId;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Data access object class for {@link Lock}. */
|
||||
public class LockDao {
|
||||
|
||||
/** Saves the {@link Lock} object to Cloud SQL. */
|
||||
public static void saveNew(Lock lock) {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
jpaTm().getEntityManager().persist(lock);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns a {@link Lock} object with the given resourceName and tld from Cloud SQL if
|
||||
* it exists, else empty.
|
||||
*/
|
||||
public static Optional<Lock> load(String resourceName, String tld) {
|
||||
checkArgumentNotNull(resourceName, "The resource name of the lock to load cannot be null");
|
||||
checkArgumentNotNull(tld, "The tld of the lock to load cannot be null");
|
||||
return Optional.ofNullable(
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> jpaTm().getEntityManager().find(Lock.class, new LockId(resourceName, tld))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a global {@link Lock} object with the given resourceName from Cloud SQL if it exists,
|
||||
* else empty.
|
||||
*/
|
||||
public static Optional<Lock> load(String resourceName) {
|
||||
checkArgumentNotNull(resourceName, "The resource name of the lock to load cannot be null");
|
||||
return Optional.ofNullable(
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm().getEntityManager().find(Lock.class, new LockId(resourceName, GLOBAL))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given {@link Lock} object from Cloud SQL. This method is idempotent and will simply
|
||||
* return if the lock has already been deleted.
|
||||
*/
|
||||
public static void delete(Lock lock) {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
Optional<Lock> loadedLock = load(lock.resourceName, lock.tld);
|
||||
if (loadedLock.isPresent()) {
|
||||
jpaTm().getEntityManager().remove(loadedLock.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@
|
|||
<class>google.registry.schema.domain.RegistryLock</class>
|
||||
<class>google.registry.schema.tmch.ClaimsList</class>
|
||||
<class>google.registry.schema.cursor.Cursor</class>
|
||||
<class>google.registry.schema.server.Lock</class>
|
||||
<class>google.registry.schema.tld.PremiumList</class>
|
||||
<class>google.registry.schema.tld.PremiumEntry</class>
|
||||
<class>google.registry.schema.tld.ReservedList</class>
|
||||
|
|
|
@ -19,6 +19,7 @@ import google.registry.model.domain.DomainBaseSqlTest;
|
|||
import google.registry.model.registry.RegistryLockDaoTest;
|
||||
import google.registry.persistence.transaction.JpaEntityCoverage;
|
||||
import google.registry.schema.cursor.CursorDaoTest;
|
||||
import google.registry.schema.server.LockDaoTest;
|
||||
import google.registry.schema.tld.PremiumListDaoTest;
|
||||
import google.registry.schema.tld.ReservedListDaoTest;
|
||||
import google.registry.schema.tmch.ClaimsListDaoTest;
|
||||
|
@ -56,6 +57,7 @@ import org.junit.runners.Suite.SuiteClasses;
|
|||
CreateReservedListCommandTest.class,
|
||||
CursorDaoTest.class,
|
||||
DomainLockUtilsTest.class,
|
||||
LockDaoTest.class,
|
||||
LockDomainCommandTest.class,
|
||||
DomainBaseSqlTest.class,
|
||||
PremiumListDaoTest.class,
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.schema.server;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import google.registry.persistence.transaction.JpaTestRules;
|
||||
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.util.Optional;
|
||||
import javax.persistence.RollbackException;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link Lock}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class LockDaoTest {
|
||||
|
||||
private final FakeClock fakeClock = new FakeClock();
|
||||
|
||||
@Rule
|
||||
public final JpaIntegrationWithCoverageRule jpaRule =
|
||||
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageRule();
|
||||
|
||||
@Test
|
||||
public void save_worksSuccessfully() {
|
||||
Lock lock =
|
||||
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.saveNew(lock);
|
||||
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
|
||||
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
|
||||
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void save_failsWhenLockAlreadyExists() {
|
||||
Lock lock =
|
||||
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.saveNew(lock);
|
||||
Lock lock2 =
|
||||
Lock.create("testResource", "tld", "testLogId2", fakeClock.nowUtc(), Duration.millis(4));
|
||||
RollbackException thrown = assertThrows(RollbackException.class, () -> LockDao.saveNew(lock2));
|
||||
assertThat(thrown.getCause().getCause().getCause().getMessage())
|
||||
.contains("duplicate key value violates unique constraint");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void save_worksSuccesfullyGlobalLock() {
|
||||
Lock lock =
|
||||
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.saveNew(lock);
|
||||
Optional<Lock> returnedLock = LockDao.load("testResource");
|
||||
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
|
||||
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void load_worksSuccessfully() {
|
||||
Lock lock =
|
||||
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.saveNew(lock);
|
||||
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
|
||||
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
|
||||
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void load_worksSuccessfullyGlobalLock() {
|
||||
Lock lock =
|
||||
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.saveNew(lock);
|
||||
Optional<Lock> returnedLock = LockDao.load("testResource");
|
||||
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
|
||||
assertThat(returnedLock.get().requestLogId).isEqualTo(lock.requestLogId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void load_worksSuccesfullyLockDoesNotExist() {
|
||||
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
|
||||
assertThat(returnedLock.isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete_worksSuccesfully() {
|
||||
Lock lock =
|
||||
Lock.create("testResource", "tld", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.saveNew(lock);
|
||||
Optional<Lock> returnedLock = LockDao.load("testResource", "tld");
|
||||
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
|
||||
LockDao.delete(lock);
|
||||
returnedLock = LockDao.load("testResource", "tld");
|
||||
assertThat(returnedLock.isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete_worksSuccessfullyGlobalLock() {
|
||||
Lock lock =
|
||||
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.saveNew(lock);
|
||||
Optional<Lock> returnedLock = LockDao.load("testResource");
|
||||
assertThat(returnedLock.get().expirationTime).isEqualTo(lock.expirationTime);
|
||||
LockDao.delete(lock);
|
||||
returnedLock = LockDao.load("testResource");
|
||||
assertThat(returnedLock.isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete_succeedsLockDoesntExist() {
|
||||
Lock lock =
|
||||
Lock.createGlobal("testResource", "testLogId", fakeClock.nowUtc(), Duration.millis(2));
|
||||
LockDao.delete(lock);
|
||||
}
|
||||
}
|
22
db/src/main/resources/sql/flyway/V18__create_lock.sql
Normal file
22
db/src/main/resources/sql/flyway/V18__create_lock.sql
Normal file
|
@ -0,0 +1,22 @@
|
|||
-- Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
create table "Lock" (
|
||||
resource_name text not null,
|
||||
tld text not null,
|
||||
acquired_time timestamptz not null,
|
||||
expiration_time timestamptz not null,
|
||||
request_log_id text not null,
|
||||
primary key (resource_name, tld)
|
||||
);
|
|
@ -77,6 +77,15 @@
|
|||
primary key (id)
|
||||
);
|
||||
|
||||
create table "Lock" (
|
||||
resource_name text not null,
|
||||
tld text not null,
|
||||
acquired_time timestamptz not null,
|
||||
expiration_time timestamptz not null,
|
||||
request_log_id text not null,
|
||||
primary key (resource_name, tld)
|
||||
);
|
||||
|
||||
create table "PremiumEntry" (
|
||||
revision_id int8 not null,
|
||||
domain_label text not null,
|
||||
|
|
|
@ -116,6 +116,19 @@ CREATE TABLE public."Domain" (
|
|||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Lock; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public."Lock" (
|
||||
resource_name text NOT NULL,
|
||||
tld text NOT NULL,
|
||||
acquired_time timestamp with time zone NOT NULL,
|
||||
expiration_time timestamp with time zone NOT NULL,
|
||||
request_log_id text NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: PremiumEntry; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -374,6 +387,14 @@ ALTER TABLE ONLY public."Domain"
|
|||
ADD CONSTRAINT "Domain_pkey" PRIMARY KEY (repo_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Lock Lock_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."Lock"
|
||||
ADD CONSTRAINT "Lock_pkey" PRIMARY KEY (resource_name, tld);
|
||||
|
||||
|
||||
--
|
||||
-- Name: PremiumEntry PremiumEntry_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue