CE-938 Initial checkin of ProcessLocks

This commit is contained in:
2024-05-19 20:26:05 -05:00
parent 82f0f177fb
commit 522dafca69
6 changed files with 1235 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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