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:
sarahcaseybot 2020-02-13 10:22:10 -05:00 committed by GitHub
parent 62433c2238
commit 22a879e655
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 392 additions and 0 deletions

View 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;
}
}
}

View file

@ -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());
}
});
}
}

View file

@ -25,6 +25,7 @@
<class>google.registry.schema.domain.RegistryLock</class> <class>google.registry.schema.domain.RegistryLock</class>
<class>google.registry.schema.tmch.ClaimsList</class> <class>google.registry.schema.tmch.ClaimsList</class>
<class>google.registry.schema.cursor.Cursor</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.PremiumList</class>
<class>google.registry.schema.tld.PremiumEntry</class> <class>google.registry.schema.tld.PremiumEntry</class>
<class>google.registry.schema.tld.ReservedList</class> <class>google.registry.schema.tld.ReservedList</class>

View file

@ -19,6 +19,7 @@ import google.registry.model.domain.DomainBaseSqlTest;
import google.registry.model.registry.RegistryLockDaoTest; import google.registry.model.registry.RegistryLockDaoTest;
import google.registry.persistence.transaction.JpaEntityCoverage; import google.registry.persistence.transaction.JpaEntityCoverage;
import google.registry.schema.cursor.CursorDaoTest; import google.registry.schema.cursor.CursorDaoTest;
import google.registry.schema.server.LockDaoTest;
import google.registry.schema.tld.PremiumListDaoTest; import google.registry.schema.tld.PremiumListDaoTest;
import google.registry.schema.tld.ReservedListDaoTest; import google.registry.schema.tld.ReservedListDaoTest;
import google.registry.schema.tmch.ClaimsListDaoTest; import google.registry.schema.tmch.ClaimsListDaoTest;
@ -56,6 +57,7 @@ import org.junit.runners.Suite.SuiteClasses;
CreateReservedListCommandTest.class, CreateReservedListCommandTest.class,
CursorDaoTest.class, CursorDaoTest.class,
DomainLockUtilsTest.class, DomainLockUtilsTest.class,
LockDaoTest.class,
LockDomainCommandTest.class, LockDomainCommandTest.class,
DomainBaseSqlTest.class, DomainBaseSqlTest.class,
PremiumListDaoTest.class, PremiumListDaoTest.class,

View file

@ -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);
}
}

View 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)
);

View file

@ -77,6 +77,15 @@
primary key (id) 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" ( create table "PremiumEntry" (
revision_id int8 not null, revision_id int8 not null,
domain_label text not null, domain_label text not null,

View file

@ -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: - -- Name: PremiumEntry; Type: TABLE; Schema: public; Owner: -
-- --
@ -374,6 +387,14 @@ ALTER TABLE ONLY public."Domain"
ADD CONSTRAINT "Domain_pkey" PRIMARY KEY (repo_id); 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: - -- Name: PremiumEntry PremiumEntry_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --