mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-21 14:38:43 +00:00
Merged feature/CE-938-order-release-automation into integration/sprint-43
This commit is contained in:
@ -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<T extends TopLevelMetaDataInterface>
|
||||
public interface MetaDataProducerInterface<T extends MetaDataProducerOutput>
|
||||
{
|
||||
int DEFAULT_SORT_ORDER = 500;
|
||||
|
||||
|
@ -49,6 +49,11 @@ public @interface QField
|
||||
*******************************************************************************/
|
||||
String backendName() default "";
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
boolean isPrimaryKey() default false;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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<T extends TopLevelMetaDataInterface> implements MetaDataProducerInterface<T>
|
||||
public abstract class MetaDataProducer<T extends MetaDataProducerOutput> implements MetaDataProducerInterface<T>
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ public class MetaDataProducerHelper
|
||||
{
|
||||
try
|
||||
{
|
||||
TopLevelMetaDataInterface metaData = producer.produce(instance);
|
||||
MetaDataProducerOutput metaData = producer.produce(instance);
|
||||
if(metaData != null)
|
||||
{
|
||||
metaData.addSelfToInstance(instance);
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<MetaDataProducerOutput> 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 <T extends MetaDataProducerOutput> List<T> getEach(Class<T> c)
|
||||
{
|
||||
List<T> 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** call the appropriate methods on a QInstance to add ourselves to it.
|
||||
*******************************************************************************/
|
||||
void addSelfToInstance(QInstance instance);
|
||||
|
||||
}
|
@ -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
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
|
@ -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<? extends QRecordEntity> entityClass) throws QException
|
||||
{
|
||||
List<QRecordEntityField> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<MetaDataProducerMultiOutput>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String, ProcessLockType> getProcessLockTypeByNameMemoization = new Memoization<String, ProcessLockType>()
|
||||
.withTimeout(Duration.ofHours(1))
|
||||
.withMayStoreNullValues(false);
|
||||
|
||||
private static Memoization<Integer, ProcessLockType> getProcessLockTypeByIdMemoization = new Memoization<Integer, ProcessLockType>()
|
||||
.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<ProcessLockType> 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<ProcessLockType> 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));
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ public class Memoization<K, V>
|
||||
|
||||
private Duration timeout = Duration.ofSeconds(600);
|
||||
private Integer maxSize = 1000;
|
||||
private boolean mayStoreNullValues = true;
|
||||
|
||||
|
||||
|
||||
@ -58,6 +59,40 @@ public class Memoization<K, V>
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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<K, V>
|
||||
*******************************************************************************/
|
||||
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<K, V>
|
||||
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<K, V> withMayStoreNullValues(boolean mayStoreNullValues)
|
||||
{
|
||||
this.mayStoreNullValues = mayStoreNullValues;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
@ -119,6 +119,57 @@ class MemoizationTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testMayNotStoreNull()
|
||||
{
|
||||
Memoization<String, String> memoization = new Memoization<>();
|
||||
memoization.setMayStoreNullValues(false);
|
||||
|
||||
AtomicInteger callCounter = new AtomicInteger();
|
||||
callCounter.set(0);
|
||||
UnsafeFunction<String, String, Exception> 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
Reference in New Issue
Block a user