From 485bc618e0113683d7eb442a47d74ac4969fb8bc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 19 May 2024 20:21:03 -0500 Subject: [PATCH 1/6] CE-938 update memoization to say if it should store null values or not --- .../core/utils/memoization/Memoization.java | 78 ++++++++++++++++++- .../utils/memoization/MemoizationTest.java | 51 ++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java index ae373024..4ed6cbce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java @@ -43,8 +43,9 @@ public class Memoization private final Map> map = Collections.synchronizedMap(new LinkedHashMap<>()); - private Duration timeout = Duration.ofSeconds(600); - private Integer maxSize = 1000; + private Duration timeout = Duration.ofSeconds(600); + private Integer maxSize = 1000; + private boolean mayStoreNullValues = true; @@ -58,6 +59,40 @@ public class Memoization + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Memoization(Integer maxSize) + { + this.maxSize = maxSize; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Memoization(Duration timeout) + { + this.timeout = timeout; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public Memoization(Duration timeout, Integer maxSize) + { + this.timeout = timeout; + this.maxSize = maxSize; + } + + + /******************************************************************************* ** Get the memoized Value for a given input Key - computing it if it wasn't previously ** memoized (or expired). @@ -153,6 +188,14 @@ public class Memoization *******************************************************************************/ public void storeResult(K key, V value) { + ////////////////////////////////////////////////////////////////////////////////////////// + // if the value is null, and we're not supposed to store nulls, then return w/o storing // + ////////////////////////////////////////////////////////////////////////////////////////// + if(value == null && !mayStoreNullValues) + { + return; + } + map.put(key, new MemoizedResult<>(value)); ////////////////////////////////////// @@ -277,4 +320,35 @@ public class Memoization return (this); } + + + /******************************************************************************* + ** Getter for mayStoreNullValues + *******************************************************************************/ + public boolean getMayStoreNullValues() + { + return (this.mayStoreNullValues); + } + + + + /******************************************************************************* + ** Setter for mayStoreNullValues + *******************************************************************************/ + public void setMayStoreNullValues(boolean mayStoreNullValues) + { + this.mayStoreNullValues = mayStoreNullValues; + } + + + + /******************************************************************************* + ** Fluent setter for mayStoreNullValues + *******************************************************************************/ + public Memoization withMayStoreNullValues(boolean mayStoreNullValues) + { + this.mayStoreNullValues = mayStoreNullValues; + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java index 5d999353..9bc9c367 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java @@ -119,6 +119,57 @@ class MemoizationTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMayNotStoreNull() + { + Memoization memoization = new Memoization<>(); + memoization.setMayStoreNullValues(false); + + AtomicInteger callCounter = new AtomicInteger(); + callCounter.set(0); + UnsafeFunction supplier = name -> + { + callCounter.getAndIncrement(); + if(name.equals("throw")) + { + throw (new Exception("You asked me to throw")); + } + else if(name.equals("null")) + { + return (null); + } + else + { + return (name); + } + }; + + assertThat(memoization.getResult("null", supplier)).isEmpty(); + assertEquals(1, callCounter.get()); + + assertThat(memoization.getResult("null", supplier)).isEmpty(); + assertEquals(2, callCounter.get()); // should re-run the supplier, incrementing the counter + + assertThat(memoization.getResult("throw", supplier)).isEmpty(); + assertEquals(3, callCounter.get()); + + assertThat(memoization.getResult("throw", supplier)).isEmpty(); + assertEquals(4, callCounter.get()); // should re-run the supplier, incrementing the counter + + //noinspection AssertBetweenInconvertibleTypes + assertThat(memoization.getResult("foo", supplier)).isPresent().get().isEqualTo("foo"); + assertEquals(5, callCounter.get()); + + //noinspection AssertBetweenInconvertibleTypes + assertThat(memoization.getResult("foo", supplier)).isPresent().get().isEqualTo("foo"); + assertEquals(5, callCounter.get()); // should NOT re-run the supplier, NOT incrementing the counter + } + + + /******************************************************************************* ** *******************************************************************************/ From 85eae36c288224e542e8e70493be745925e83900 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 19 May 2024 20:22:58 -0500 Subject: [PATCH 2/6] CE-938 Add concept of MetaDataProducerMultiOutput --- .../core/model/MetaDataProducerInterface.java | 4 +- .../core/model/metadata/MetaDataProducer.java | 2 +- .../metadata/MetaDataProducerHelper.java | 2 +- .../metadata/MetaDataProducerMultiOutput.java | 101 ++++++++++++++++++ .../metadata/MetaDataProducerOutput.java | 35 ++++++ .../metadata/TopLevelMetaDataInterface.java | 2 +- 6 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerMultiOutput.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java index 45bd2ca6..9faf3dc4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/MetaDataProducerInterface.java @@ -23,8 +23,8 @@ package com.kingsrook.qqq.backend.core.model; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; /******************************************************************************* @@ -42,7 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; ** implement this interface. or, same idea for a QRecordEntity that provides ** its own TableMetaData. *******************************************************************************/ -public interface MetaDataProducerInterface +public interface MetaDataProducerInterface { int DEFAULT_SORT_ORDER = 500; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java index 4207a132..2756715f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducer.java @@ -30,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; ** MetaDataProducerHelper, to put point at a package full of these, and populate ** your whole QInstance. *******************************************************************************/ -public abstract class MetaDataProducer implements MetaDataProducerInterface +public abstract class MetaDataProducer implements MetaDataProducerInterface { } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java index e8165a41..54c7ae4b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java @@ -157,7 +157,7 @@ public class MetaDataProducerHelper { try { - TopLevelMetaDataInterface metaData = producer.produce(instance); + MetaDataProducerOutput metaData = producer.produce(instance); if(metaData != null) { metaData.addSelfToInstance(instance); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerMultiOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerMultiOutput.java new file mode 100644 index 00000000..d6797a2a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerMultiOutput.java @@ -0,0 +1,101 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Output object for a MetaDataProducer, which contains multiple meta-data + ** objects. + *******************************************************************************/ +public class MetaDataProducerMultiOutput implements MetaDataProducerOutput +{ + private List contents; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void addSelfToInstance(QInstance instance) + { + for(MetaDataProducerOutput metaDataProducerOutput : CollectionUtils.nonNullList(contents)) + { + metaDataProducerOutput.addSelfToInstance(instance); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void add(MetaDataProducerOutput metaDataProducerOutput) + { + if(contents == null) + { + contents = new ArrayList<>(); + } + contents.add(metaDataProducerOutput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public MetaDataProducerMultiOutput with(MetaDataProducerOutput metaDataProducerOutput) + { + add(metaDataProducerOutput); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public List getEach(Class c) + { + List rs = new ArrayList<>(); + + for(MetaDataProducerOutput content : contents) + { + if(content instanceof MetaDataProducerMultiOutput multiOutput) + { + rs.addAll(multiOutput.getEach(c)); + } + else if(c.isInstance(content)) + { + rs.add(c.cast(content)); + } + } + + return (rs); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java new file mode 100644 index 00000000..09310523 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java @@ -0,0 +1,35 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata; + + +/******************************************************************************* + ** Interface to mark objects that can be produced by a MetaDataProducer. + ** + ** These would usually be TopLevelMetaData objects (a table, a process, etc) + ** but can also be a MetaDataProducerMultiOutput, to produce multiple objects + ** from one producer. + *******************************************************************************/ +public interface MetaDataProducerOutput +{ + void addSelfToInstance(QInstance instance); +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java index e3dc117f..116bae9e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java @@ -26,7 +26,7 @@ package com.kingsrook.qqq.backend.core.model.metadata; ** Interface for meta-data classes that can be added directly (e.g, at the top ** level) to a QInstance (such as a QTableMetaData - not a QFieldMetaData). *******************************************************************************/ -public interface TopLevelMetaDataInterface +public interface TopLevelMetaDataInterface extends MetaDataProducerOutput { /******************************************************************************* From 9c79ce3272fef8f86ec94288dae2e55a5ed3db57 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 19 May 2024 20:24:06 -0500 Subject: [PATCH 3/6] CE-938 add isPrimaryKey to @QField --- .../qqq/backend/core/model/data/QField.java | 5 +++++ .../model/metadata/tables/QTableMetaData.java | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java index c13b87f5..13ead81c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java @@ -49,6 +49,11 @@ public @interface QField *******************************************************************************/ String backendName() default ""; + /******************************************************************************* + ** + *******************************************************************************/ + boolean isPrimaryKey() default false; + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index a14fc566..99e52602 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -111,6 +111,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData private ShareableTableMetaData shareableTableMetaData; + /******************************************************************************* ** Default constructor. *******************************************************************************/ @@ -158,11 +159,26 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData public QTableMetaData withFieldsFromEntity(Class entityClass) throws QException { List recordEntityFieldList = QRecordEntity.getFieldList(entityClass); + + boolean setPrimaryKey = false; + for(QRecordEntityField recordEntityField : recordEntityFieldList) { QFieldMetaData field = new QFieldMetaData(recordEntityField.getGetter()); addField(field); + + if(recordEntityField.getFieldAnnotation().isPrimaryKey()) + { + if(setPrimaryKey) + { + throw (new QException("Attempt to set more than one field as primary key (" + primaryKeyField + "," + field.getName() + ").")); + } + + setPrimaryKeyField(field.getName()); + setPrimaryKey = true; + } } + return (this); } @@ -1388,6 +1404,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData } + /******************************************************************************* ** Getter for shareableTableMetaData *******************************************************************************/ @@ -1417,5 +1434,4 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData return (this); } - } From 82f0f177fb3120cabe54c85dc1cedf87e6d45c3f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 19 May 2024 20:24:25 -0500 Subject: [PATCH 4/6] CE-938 add overload that takes a Duration --- .../kingsrook/qqq/backend/core/utils/SleepUtils.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java index 4487821a..5d374257 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.utils; +import java.time.Duration; import java.util.concurrent.TimeUnit; @@ -55,4 +56,14 @@ public class SleepUtils } } + + + /******************************************************************************* + ** overload for sleep that takes duration object + *******************************************************************************/ + public static void sleep(Duration sleepDuration) + { + sleep(sleepDuration.toMillis(), TimeUnit.MILLISECONDS); + } + } From 522dafca69a8d8283027c8d69ced925995b26167 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 19 May 2024 20:26:05 -0500 Subject: [PATCH 5/6] CE-938 Initial checkin of ProcessLocks --- .../core/processes/locks/ProcessLock.java | 330 ++++++++++++++++++ .../locks/ProcessLockMetaDataProducer.java | 104 ++++++ .../core/processes/locks/ProcessLockType.java | 262 ++++++++++++++ .../processes/locks/ProcessLockUtils.java | 291 +++++++++++++++ .../UnableToObtainProcessLockException.java | 52 +++ .../processes/locks/ProcessLockUtilsTest.java | 196 +++++++++++ 6 files changed, 1235 insertions(+) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockType.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtils.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/UnableToObtainProcessLockException.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java new file mode 100644 index 00000000..157b0dd7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLock.java @@ -0,0 +1,330 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.locks; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; + + +/******************************************************************************* + ** QRecord Entity for ProcessLock table + *******************************************************************************/ +public class ProcessLock extends QRecordEntity +{ + public static final String TABLE_NAME = "processLock"; + + @QField(isEditable = false, isPrimaryKey = true) + private Integer id; + + @QField(isEditable = false) + private Instant createDate; + + @QField(isEditable = false) + private Instant modifyDate; + + @QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR) + private String key; + + @QField(possibleValueSourceName = ProcessLockType.TABLE_NAME) + private Integer processLockTypeId; + + @QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR) + private String holder; + + @QField() + private Instant checkInTimestamp; + + @QField() + private Instant expiresAtTimestamp; + + + + /******************************************************************************* + ** Default constructor + *******************************************************************************/ + public ProcessLock() + { + } + + + + /******************************************************************************* + ** Constructor that takes a QRecord + *******************************************************************************/ + public ProcessLock(QRecord record) + { + populateFromQRecord(record); + } + + + + /******************************************************************************* + ** Getter for id + *******************************************************************************/ + public Integer getId() + { + return (this.id); + } + + + + /******************************************************************************* + ** Setter for id + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + *******************************************************************************/ + public ProcessLock withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + *******************************************************************************/ + public Instant getCreateDate() + { + return (this.createDate); + } + + + + /******************************************************************************* + ** Setter for createDate + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + *******************************************************************************/ + public ProcessLock withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + *******************************************************************************/ + public Instant getModifyDate() + { + return (this.modifyDate); + } + + + + /******************************************************************************* + ** Setter for modifyDate + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + *******************************************************************************/ + public ProcessLock withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for key + *******************************************************************************/ + public String getKey() + { + return (this.key); + } + + + + /******************************************************************************* + ** Setter for key + *******************************************************************************/ + public void setKey(String key) + { + this.key = key; + } + + + + /******************************************************************************* + ** Fluent setter for key + *******************************************************************************/ + public ProcessLock withKey(String key) + { + this.key = key; + return (this); + } + + + + /******************************************************************************* + ** Getter for holder + *******************************************************************************/ + public String getHolder() + { + return (this.holder); + } + + + + /******************************************************************************* + ** Setter for holder + *******************************************************************************/ + public void setHolder(String holder) + { + this.holder = holder; + } + + + + /******************************************************************************* + ** Fluent setter for holder + *******************************************************************************/ + public ProcessLock withHolder(String holder) + { + this.holder = holder; + return (this); + } + + + + /******************************************************************************* + ** Getter for checkInTimestamp + *******************************************************************************/ + public Instant getCheckInTimestamp() + { + return (this.checkInTimestamp); + } + + + + /******************************************************************************* + ** Setter for checkInTimestamp + *******************************************************************************/ + public void setCheckInTimestamp(Instant checkInTimestamp) + { + this.checkInTimestamp = checkInTimestamp; + } + + + + /******************************************************************************* + ** Fluent setter for checkInTimestamp + *******************************************************************************/ + public ProcessLock withCheckInTimestamp(Instant checkInTimestamp) + { + this.checkInTimestamp = checkInTimestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for expiresAtTimestamp + *******************************************************************************/ + public Instant getExpiresAtTimestamp() + { + return (this.expiresAtTimestamp); + } + + + + /******************************************************************************* + ** Setter for expiresAtTimestamp + *******************************************************************************/ + public void setExpiresAtTimestamp(Instant expiresAtTimestamp) + { + this.expiresAtTimestamp = expiresAtTimestamp; + } + + + + /******************************************************************************* + ** Fluent setter for expiresAtTimestamp + *******************************************************************************/ + public ProcessLock withExpiresAtTimestamp(Instant expiresAtTimestamp) + { + this.expiresAtTimestamp = expiresAtTimestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for processLockTypeId + *******************************************************************************/ + public Integer getProcessLockTypeId() + { + return (this.processLockTypeId); + } + + + + /******************************************************************************* + ** Setter for processLockTypeId + *******************************************************************************/ + public void setProcessLockTypeId(Integer processLockTypeId) + { + this.processLockTypeId = processLockTypeId; + } + + + + /******************************************************************************* + ** Fluent setter for processLockTypeId + *******************************************************************************/ + public ProcessLock withProcessLockTypeId(Integer processLockTypeId) + { + this.processLockTypeId = processLockTypeId; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java new file mode 100644 index 00000000..3f10f86f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockMetaDataProducer.java @@ -0,0 +1,104 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.locks; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; +import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; + + +/******************************************************************************* + ** MetaData producer for Process Locks "system" + *******************************************************************************/ +public class ProcessLockMetaDataProducer implements MetaDataProducerInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException + { + MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput(); + + //////////////////////// + // process lock table // + //////////////////////// + output.add(new QTableMetaData() + .withName(ProcessLock.TABLE_NAME) + .withFieldsFromEntity(ProcessLock.class) + .withIcon(new QIcon().withName("sync_lock")) + .withUniqueKey(new UniqueKey("processLockTypeId", "key")) + .withRecordLabelFormat("%s %s") + .withRecordLabelFields("processLockTypeId", "key") + .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "processLockTypeId", "key"))) + .withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("holder", "checkInTimestamp", "expiresAtTimestamp"))) + .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))) + ); + + ///////////////////////////// + // process lock type table // + ///////////////////////////// + output.add(new QTableMetaData() + .withName(ProcessLockType.TABLE_NAME) + .withFieldsFromEntity(ProcessLockType.class) + .withIcon(new QIcon().withName("lock")) + .withUniqueKey(new UniqueKey("name")) + .withRecordLabelFormat("%s") + .withRecordLabelFields("label") + .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name", "label"))) + .withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("defaultExpirationSeconds"))) + .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))) + ); + + /////////////////////////// + // process lock type PVS // + /////////////////////////// + output.add(QPossibleValueSource.newForTable(ProcessLockType.TABLE_NAME)); + + ///////////////////////////////////////////////////// + // join between process lock type and process lock // + ///////////////////////////////////////////////////// + output.add(new QJoinMetaData() + .withLeftTable(ProcessLockType.TABLE_NAME) + .withRightTable(ProcessLock.TABLE_NAME) + .withInferredName() + .withType(JoinType.ONE_TO_MANY) + .withJoinOn(new JoinOn("name", "processLockTypeId")) + ); + + return output; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockType.java new file mode 100644 index 00000000..2a9e6209 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockType.java @@ -0,0 +1,262 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.locks; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; + + +/******************************************************************************* + ** QRecord Entity for ProcessLockType table + *******************************************************************************/ +public class ProcessLockType extends QRecordEntity +{ + public static final String TABLE_NAME = "processLockType"; + + @QField(isEditable = false, isPrimaryKey = true) + private Integer id; + + @QField(isEditable = false) + private Instant createDate; + + @QField(isEditable = false) + private Instant modifyDate; + + @QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR) + private String name; + + @QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR) + private String label; + + @QField() + private Integer defaultExpirationSeconds; + + + + /******************************************************************************* + ** Default constructor + *******************************************************************************/ + public ProcessLockType() + { + } + + + + /******************************************************************************* + ** Constructor that takes a QRecord + *******************************************************************************/ + public ProcessLockType(QRecord record) + { + populateFromQRecord(record); + } + + + + /******************************************************************************* + ** Getter for id + *******************************************************************************/ + public Integer getId() + { + return (this.id); + } + + + + /******************************************************************************* + ** Setter for id + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + *******************************************************************************/ + public ProcessLockType withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + *******************************************************************************/ + public Instant getCreateDate() + { + return (this.createDate); + } + + + + /******************************************************************************* + ** Setter for createDate + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + *******************************************************************************/ + public ProcessLockType withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + *******************************************************************************/ + public Instant getModifyDate() + { + return (this.modifyDate); + } + + + + /******************************************************************************* + ** Setter for modifyDate + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + *******************************************************************************/ + public ProcessLockType withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public ProcessLockType withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + *******************************************************************************/ + public String getLabel() + { + return (this.label); + } + + + + /******************************************************************************* + ** Setter for label + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + *******************************************************************************/ + public ProcessLockType withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for defaultExpirationSeconds + *******************************************************************************/ + public Integer getDefaultExpirationSeconds() + { + return (this.defaultExpirationSeconds); + } + + + + /******************************************************************************* + ** Setter for defaultExpirationSeconds + *******************************************************************************/ + public void setDefaultExpirationSeconds(Integer defaultExpirationSeconds) + { + this.defaultExpirationSeconds = defaultExpirationSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for defaultExpirationSeconds + *******************************************************************************/ + public ProcessLockType withDefaultExpirationSeconds(Integer defaultExpirationSeconds) + { + this.defaultExpirationSeconds = defaultExpirationSeconds; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtils.java new file mode 100644 index 00000000..3307eb5e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtils.java @@ -0,0 +1,291 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.locks; + + +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Utility class for working with ProcessLock table - creating, checking-in, + ** and releasing process locks. + *******************************************************************************/ +public class ProcessLockUtils +{ + private static final QLogger LOG = QLogger.getLogger(ProcessLockUtils.class); + + private static Memoization getProcessLockTypeByNameMemoization = new Memoization() + .withTimeout(Duration.ofHours(1)) + .withMayStoreNullValues(false); + + private static Memoization getProcessLockTypeByIdMemoization = new Memoization() + .withTimeout(Duration.ofHours(1)) + .withMayStoreNullValues(false); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ProcessLock create(String key, String typeName, String holder) throws UnableToObtainProcessLockException, QException + { + ProcessLockType lockType = getProcessLockTypeByName(typeName); + if(lockType == null) + { + throw (new QException("Unrecognized process lock type: " + typeName)); + } + + QSession qSession = QContext.getQSession(); + holder = qSession.getUser().getIdReference() + "-" + qSession.getUuid() + "-" + holder; + + Instant now = Instant.now(); + ProcessLock processLock = new ProcessLock() + .withKey(key) + .withProcessLockTypeId(lockType.getId()) + .withHolder(holder) + .withCheckInTimestamp(now); + + Integer defaultExpirationSeconds = lockType.getDefaultExpirationSeconds(); + if(defaultExpirationSeconds != null) + { + processLock.setExpiresAtTimestamp(now.plusSeconds(defaultExpirationSeconds)); + } + + QRecord insertOutputRecord = tryToInsert(processLock); + + //////////////////////////////////////////////////////////// + // if inserting failed... see if we can get existing lock // + //////////////////////////////////////////////////////////// + StringBuilder existingLockDetails = new StringBuilder(); + if(CollectionUtils.nullSafeHasContents(insertOutputRecord.getErrors())) + { + QRecord existingLockRecord = new GetAction().executeForRecord(new GetInput(ProcessLock.TABLE_NAME).withUniqueKey(Map.of("key", key, "processLockTypeId", lockType.getId()))); + if(existingLockRecord != null) + { + existingLockDetails.append("Held by: ").append(existingLockRecord.getValueString("holder")); + Instant expiresAtTimestamp = existingLockRecord.getValueInstant("expiresAtTimestamp"); + if(expiresAtTimestamp != null) + { + ZonedDateTime zonedExpiresAt = expiresAtTimestamp.atZone(ValueUtils.getSessionOrInstanceZoneId()); + existingLockDetails.append("; Expires at: ").append(QValueFormatter.formatDateTimeWithZone(zonedExpiresAt)); + } + + if(expiresAtTimestamp != null && expiresAtTimestamp.isBefore(now)) + { + ///////////////////////////////////////////////////////////////////////////////// + // if existing lock has expired, then we can delete it and try to insert again // + ///////////////////////////////////////////////////////////////////////////////// + Serializable id = existingLockRecord.getValue("id"); + LOG.info("Existing lock has expired - deleting it and trying again.", logPair("id", id), + logPair("key", key), logPair("type", typeName), logPair("holder", holder), logPair("expiresAtTimestamp", expiresAtTimestamp)); + new DeleteAction().execute(new DeleteInput(ProcessLock.TABLE_NAME).withPrimaryKey(id)); + insertOutputRecord = tryToInsert(processLock); + } + } + else + { + ///////////////////////////////////////////////////////// + // if existing lock doesn't exist, try to insert again // + ///////////////////////////////////////////////////////// + insertOutputRecord = tryToInsert(processLock); + } + } + + if(CollectionUtils.nullSafeHasContents(insertOutputRecord.getErrors())) + { + ///////////////////////////////////////////////////////////////////////////////// + // if at this point, we have errors on the last attempted insert, then give up // + ///////////////////////////////////////////////////////////////////////////////// + LOG.info("Errors in process lock record after attempted insert", logPair("errors", insertOutputRecord.getErrors()), + logPair("key", key), logPair("type", typeName), logPair("holder", holder)); + throw (new UnableToObtainProcessLockException("A Process Lock already exists for key [" + key + "] of type [" + typeName + "], " + existingLockDetails)); + } + + LOG.info("Created process lock", logPair("id", processLock.getId()), + logPair("key", key), logPair("type", typeName), logPair("holder", holder), logPair("expiresAtTimestamp", processLock.getExpiresAtTimestamp())); + return new ProcessLock(insertOutputRecord); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QRecord tryToInsert(ProcessLock processLock) throws QException + { + return new InsertAction().execute(new InsertInput(ProcessLock.TABLE_NAME).withRecordEntity(processLock)).getRecords().get(0); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ProcessLock create(String key, String type, String holderId, Duration sleepBetweenTries, Duration maxWait) throws UnableToObtainProcessLockException, QException + { + Instant giveUpTime = Instant.now().plus(maxWait); + + while(true) + { + try + { + ProcessLock processLock = create(key, type, holderId); + return (processLock); + } + catch(UnableToObtainProcessLockException e) + { + if(Instant.now().plus(sleepBetweenTries).isBefore(giveUpTime)) + { + SleepUtils.sleep(sleepBetweenTries); + } + else + { + break; + } + } + } + + throw (new UnableToObtainProcessLockException("Unable to obtain process lock for key [" + key + "] in type [" + type + "] after [" + maxWait + "]")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static ProcessLock getById(Integer id) throws QException + { + QRecord existingLockRecord = new GetAction().executeForRecord(new GetInput(ProcessLock.TABLE_NAME).withPrimaryKey(id)); + if(existingLockRecord != null) + { + return (new ProcessLock(existingLockRecord)); + } + return null; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkIn(ProcessLock processLock) throws QException + { + ProcessLockType lockType = getProcessLockTypeById(processLock.getProcessLockTypeId()); + if(lockType == null) + { + throw (new QException("Unrecognized process lock type id: " + processLock.getProcessLockTypeId())); + } + + Instant now = Instant.now(); + QRecord recordToUpdate = new QRecord() + .withValue("id", processLock.getId()) + .withValue("checkInTimestamp", now); + + Integer defaultExpirationSeconds = lockType.getDefaultExpirationSeconds(); + if(defaultExpirationSeconds != null) + { + recordToUpdate.setValue("expiresAtTimestamp", now.plusSeconds(defaultExpirationSeconds)); + } + + new UpdateAction().execute(new UpdateInput(ProcessLock.TABLE_NAME).withRecord(recordToUpdate)); + LOG.debug("Updated processLock checkInTimestamp", logPair("id", processLock.getId()), logPair("checkInTimestamp", now)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void release(ProcessLock processLock) throws QException + { + DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(ProcessLock.TABLE_NAME).withPrimaryKey(processLock.getId())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ProcessLockType getProcessLockTypeByName(String name) + { + Optional result = getProcessLockTypeByNameMemoization.getResult(name, n -> + { + QRecord qRecord = new GetAction().executeForRecord(new GetInput(ProcessLockType.TABLE_NAME).withUniqueKey(Map.of("name", name))); + + if(qRecord != null) + { + return (new ProcessLockType(qRecord)); + } + + return (null); + }); + + return (result.orElse(null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ProcessLockType getProcessLockTypeById(Integer id) + { + Optional result = getProcessLockTypeByIdMemoization.getResult(id, i -> + { + QRecord qRecord = new GetAction().executeForRecord(new GetInput(ProcessLockType.TABLE_NAME).withPrimaryKey(id)); + + if(qRecord != null) + { + return (new ProcessLockType(qRecord)); + } + + return (null); + }); + + return (result.orElse(null)); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/UnableToObtainProcessLockException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/UnableToObtainProcessLockException.java new file mode 100644 index 00000000..a14f9f02 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/locks/UnableToObtainProcessLockException.java @@ -0,0 +1,52 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.locks; + + +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; + + +/******************************************************************************* + ** Lock thrown by ProcessLockUtils when you can't get the lock. + *******************************************************************************/ +public class UnableToObtainProcessLockException extends QUserFacingException +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public UnableToObtainProcessLockException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public UnableToObtainProcessLockException(String message, Throwable cause) + { + super(message, cause); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java new file mode 100644 index 00000000..b665fed0 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/locks/ProcessLockUtilsTest.java @@ -0,0 +1,196 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.locks; + + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for ProcessLockUtils + *******************************************************************************/ +class ProcessLockUtilsTest extends BaseTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws QException + { + QInstance qInstance = QContext.getQInstance(); + MetaDataProducerMultiOutput metaData = new ProcessLockMetaDataProducer().produce(qInstance); + + for(QTableMetaData table : metaData.getEach(QTableMetaData.class)) + { + table.setBackendName(TestUtils.MEMORY_BACKEND_NAME); + } + + metaData.addSelfToInstance(qInstance); + + new InsertAction().execute(new InsertInput(ProcessLockType.TABLE_NAME).withRecordEntities(List.of( + new ProcessLockType() + .withName("typeA") + .withLabel("Type A"), + new ProcessLockType() + .withName("typeB") + .withLabel("Type B") + .withDefaultExpirationSeconds(1), + new ProcessLockType() + .withName("typeC") + .withLabel("Type C") + .withDefaultExpirationSeconds(10) + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + ///////////////////////////////////////// + // make sure that we can create a lock // + ///////////////////////////////////////// + ProcessLock processLock = ProcessLockUtils.create("1", "typeA", "me"); + assertNotNull(processLock.getId()); + assertNotNull(processLock.getCheckInTimestamp()); + assertNull(processLock.getExpiresAtTimestamp()); + + ///////////////////////////////////////////////////////// + // make sure we can't create a second for the same key // + ///////////////////////////////////////////////////////// + assertThatThrownBy(() -> ProcessLockUtils.create("1", "typeA", "you")) + .isInstanceOf(UnableToObtainProcessLockException.class); + + ///////////////////////////////////////////////////////// + // make sure we can create another for a different key // + ///////////////////////////////////////////////////////// + ProcessLockUtils.create("2", "typeA", "him"); + + ///////////////////////////////////////////////////////////////////// + // make sure we can create another for a different type (same key) // + ///////////////////////////////////////////////////////////////////// + ProcessLockUtils.create("1", "typeB", "her"); + + ////////////////////////////// + // make sure we can release // + ////////////////////////////// + ProcessLockUtils.release(processLock); + + ////////////////////// + // and then you can // + ////////////////////// + processLock = ProcessLockUtils.create("1", "typeA", "you"); + assertNotNull(processLock.getId()); + assertThat(processLock.getHolder()).endsWith("you"); + + assertThatThrownBy(() -> ProcessLockUtils.create("1", "notAType", "you")) + .isInstanceOf(QException.class) + .hasMessageContaining("Unrecognized process lock type"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSucceedWaitingForExpiration() throws QException + { + ProcessLock processLock = ProcessLockUtils.create("1", "typeB", "me"); + assertNotNull(processLock.getId()); + assertNotNull(processLock.getCheckInTimestamp()); + assertNotNull(processLock.getExpiresAtTimestamp()); + + ///////////////////////////////////////////////////////////////////////// + // make sure someone else can, if they wait longer than the expiration // + ///////////////////////////////////////////////////////////////////////// + processLock = ProcessLockUtils.create("1", "typeB", "you", Duration.of(1, ChronoUnit.SECONDS), Duration.of(3, ChronoUnit.SECONDS)); + assertNotNull(processLock.getId()); + assertThat(processLock.getHolder()).endsWith("you"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFailWaitingForExpiration() throws QException + { + ProcessLock processLock = ProcessLockUtils.create("1", "typeC", "me"); + assertNotNull(processLock.getId()); + assertNotNull(processLock.getCheckInTimestamp()); + assertNotNull(processLock.getExpiresAtTimestamp()); + + ////////////////////////////////////////////////////////////////// + // make sure someone else fails, if they don't wait long enough // + ////////////////////////////////////////////////////////////////// + assertThatThrownBy(() -> ProcessLockUtils.create("1", "typeC", "you", Duration.of(1, ChronoUnit.SECONDS), Duration.of(3, ChronoUnit.SECONDS))) + .isInstanceOf(UnableToObtainProcessLockException.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCheckInUpdatesExpiration() throws QException + { + ProcessLock processLock = ProcessLockUtils.create("1", "typeB", "me"); + assertNotNull(processLock.getId()); + Instant originalCheckIn = processLock.getCheckInTimestamp(); + Instant originalExpiration = processLock.getExpiresAtTimestamp(); + + SleepUtils.sleep(5, TimeUnit.MILLISECONDS); + ProcessLockUtils.checkIn(processLock); + + ProcessLock freshLock = ProcessLockUtils.getById(processLock.getId()); + assertNotEquals(originalCheckIn, freshLock.getCheckInTimestamp()); + assertNotEquals(originalExpiration, freshLock.getExpiresAtTimestamp()); + } + +} \ No newline at end of file From b91da938586be12681f0f46ab18b3eaaf4812718 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 19 May 2024 20:29:25 -0500 Subject: [PATCH 6/6] CE-938 Add missing javadoc --- .../backend/core/model/metadata/MetaDataProducerOutput.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java index 09310523..74504e72 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerOutput.java @@ -31,5 +31,10 @@ package com.kingsrook.qqq.backend.core.model.metadata; *******************************************************************************/ public interface MetaDataProducerOutput { + + /******************************************************************************* + ** call the appropriate methods on a QInstance to add ourselves to it. + *******************************************************************************/ void addSelfToInstance(QInstance instance); + }