CE-847 Main implementation of fix for missing insert automations, by updating status to pending-update-automations when a record is still pending insert automations. added memoization of areThereTableTriggersForTableMemoization; small cleanup (remove session & instance params, pass transaction

This commit is contained in:
2024-02-09 19:38:00 -06:00
parent efa84d03e6
commit bbba43ef80
7 changed files with 344 additions and 30 deletions

View File

@ -22,30 +22,41 @@
package com.kingsrook.qqq.backend.core.actions.automation; package com.kingsrook.qqq.backend.core.actions.automation;
import java.io.Serializable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.automation.TableTrigger; import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
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.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Speaker;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -55,19 +66,37 @@ public class RecordAutomationStatusUpdater
{ {
private static final QLogger LOG = QLogger.getLogger(RecordAutomationStatusUpdater.class); private static final QLogger LOG = QLogger.getLogger(RecordAutomationStatusUpdater.class);
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// feature flag - by default, will be false - and before setting records to PENDING_UPDATE_AUTOMATIONS, //
// we will fetch them, to check their current automationStatus - and if they are currently PENDING //
// or RUNNING inserts or updates, we won't update them. This is added to fix cases where an update that //
// comes in before insert-automations have run, will cause the pending-insert status to be missed. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
private static boolean skipPreUpdateFetch = new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.recordAutomationStatusUpdater.skipPreUpdateFetch", "QQQ_RECORD_AUTOMATION_STATUS_UPDATER_SKIP_PRE_UPDATE_FETCH", false);
///////////////////////////////////////////////////////////////////////////////////////////////
// feature flag - by default, we'll memoize the check for triggers - but we can turn it off. //
///////////////////////////////////////////////////////////////////////////////////////////////
private static boolean memoizeCheckForTriggers = new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.recordAutomationStatusUpdater.memoizeCheckForTriggers", "QQQ_RECORD_AUTOMATION_STATUS_UPDATER_MEMOIZE_CHECK_FOR_TRIGGERS", true);
private static Memoization<Key, Boolean> areThereTableTriggersForTableMemoization = new Memoization<Key, Boolean>().withTimeout(Duration.of(60, ChronoUnit.SECONDS));
/******************************************************************************* /*******************************************************************************
** for a list of records from a table, set their automation status - based on ** for a list of records from a table, set their automation status - based on
** how the table is configured. ** how the table is configured.
*******************************************************************************/ *******************************************************************************/
public static boolean setAutomationStatusInRecords(QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus) public static boolean setAutomationStatusInRecords(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus, QBackendTransaction transaction)
{ {
if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records)) if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records))
{ {
return (false); return (false);
} }
QTableAutomationDetails automationDetails = table.getAutomationDetails();
Set<Serializable> pkeysWeMayNotUpdate = new HashSet<>();
/////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////
// In case an automation is running, and it updates records - don't let those records be marked // // In case an automation is running, and it updates records - don't let those records be marked //
// as PENDING_UPDATE_AUTOMATIONS... this is meant to avoid having a record's automation update // // as PENDING_UPDATE_AUTOMATIONS... this is meant to avoid having a record's automation update //
@ -81,12 +110,50 @@ public class RecordAutomationStatusUpdater
for(StackTraceElement stackTraceElement : e.getStackTrace()) for(StackTraceElement stackTraceElement : e.getStackTrace())
{ {
String className = stackTraceElement.getClassName(); String className = stackTraceElement.getClassName();
if(className.contains("com.kingsrook.qqq.backend.core.actions.automation") && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test")) if(className.contains(RecordAutomationStatusUpdater.class.getPackageName()) && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test") && !className.contains("Test$"))
{ {
LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation"); LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
return (false); return (false);
} }
} }
////////////////////////////////////////////////////////////////////////////////
// if table uses field-in-table status tracking (and feature flag allows it) //
// then look the records up before we set them to pending-updates, to avoid //
// losing other pending or running status information. We will allow moving //
// from OK or the 2 failed statuses into pending-updates - which seems right. //
////////////////////////////////////////////////////////////////////////////////
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()) && !skipPreUpdateFetch)
{
try
{
List<Serializable> pkeysToLookup = records.stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
List<QRecord> freshRecords = new QueryAction().execute(new QueryInput(table.getName())
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, pkeysToLookup)))
.withTransaction(transaction)
).getRecords();
for(QRecord freshRecord : freshRecords)
{
Serializable recordStatus = freshRecord.getValue(automationDetails.getStatusTracking().getFieldName());
if(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(recordStatus)
|| AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(recordStatus)
|| AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId().equals(recordStatus)
|| AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(recordStatus))
{
Serializable primaryKey = freshRecord.getValue(table.getPrimaryKeyField());
LOG.debug("May not update automation status", logPair("table", table.getName()), logPair("id", primaryKey), logPair("currentStatus", recordStatus), logPair("requestedStatus", automationStatus.getId()));
pkeysWeMayNotUpdate.add(primaryKey);
}
}
}
catch(QException qe)
{
LOG.error("Error checking existing automation status before setting new automation status - more records will be updated than maybe should be...", qe);
Speaker.say("Avoid re-set todd update during automation");
}
}
} }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -98,21 +165,17 @@ public class RecordAutomationStatusUpdater
automationStatus = AutomationStatus.OK; automationStatus = AutomationStatus.OK;
} }
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType())) if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{ {
for(QRecord record : records) for(QRecord record : records)
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if(!pkeysWeMayNotUpdate.contains(record.getValue(table.getPrimaryKeyField())))
// todo - seems like there's some case here, where if an order was in PENDING_INSERT, but then some other job updated the record, that we'd // {
// lose that pending status, which would be a Bad Thing™... //
// problem is - we may not have the full record in here, so we can't necessarily check the record to see what status it's currently in... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId()); record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
// todo - another field - for the automation timestamp?? // todo - another field - for the automation timestamp??
} }
} }
}
return (true); return (true);
} }
@ -188,11 +251,29 @@ public class RecordAutomationStatusUpdater
return (false); return (false);
} }
if(memoizeCheckForTriggers)
{
///////////////////////////////////////////////////////////////////////////////////////
// as within the lookup method, error on the side of "yes, maybe there are triggers" //
///////////////////////////////////////////////////////////////////////////////////////
Optional<Boolean> result = areThereTableTriggersForTableMemoization.getResult(new Key(table, triggerEvent), key -> lookupIfThereAreTriggersForTable(table, triggerEvent));
return result.orElse(true);
}
else
{
return lookupIfThereAreTriggersForTable(table, triggerEvent);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Boolean lookupIfThereAreTriggersForTable(QTableMetaData table, TriggerEvent triggerEvent)
{
try try
{ {
///////////////////
// todo - cache? //
///////////////////
CountInput countInput = new CountInput(); CountInput countInput = new CountInput();
countInput.setTableName(TableTrigger.TABLE_NAME); countInput.setTableName(TableTrigger.TABLE_NAME);
countInput.setFilter(new QQueryFilter( countInput.setFilter(new QQueryFilter(
@ -207,6 +288,7 @@ public class RecordAutomationStatusUpdater
/////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the count query failed, we're a bit safer to err on the side of "yeah, there might be automations" // // if the count query failed, we're a bit safer to err on the side of "yeah, there might be automations" //
/////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.warn("Error looking if there are triggers for table", e, logPair("tableName", table.getName()));
return (true); return (true);
} }
} }
@ -217,12 +299,12 @@ public class RecordAutomationStatusUpdater
** for a list of records, update their automation status and actually Update the ** for a list of records, update their automation status and actually Update the
** backend as well. ** backend as well.
*******************************************************************************/ *******************************************************************************/
public static void setAutomationStatusInRecordsAndUpdate(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus) throws QException public static void setAutomationStatusInRecordsAndUpdate(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus, QBackendTransaction transaction) throws QException
{ {
QTableAutomationDetails automationDetails = table.getAutomationDetails(); QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType())) if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{ {
boolean didSetStatusField = setAutomationStatusInRecords(session, table, records, automationStatus); boolean didSetStatusField = setAutomationStatusInRecords(table, records, automationStatus, transaction);
if(didSetStatusField) if(didSetStatusField)
{ {
UpdateInput updateInput = new UpdateInput(); UpdateInput updateInput = new UpdateInput();
@ -237,6 +319,7 @@ public class RecordAutomationStatusUpdater
.withValue(table.getPrimaryKeyField(), r.getValue(table.getPrimaryKeyField())) .withValue(table.getPrimaryKeyField(), r.getValue(table.getPrimaryKeyField()))
.withValue(automationDetails.getStatusTracking().getFieldName(), r.getValue(automationDetails.getStatusTracking().getFieldName()))).toList()); .withValue(automationDetails.getStatusTracking().getFieldName(), r.getValue(automationDetails.getStatusTracking().getFieldName()))).toList());
updateInput.setAreAllValuesBeingUpdatedTheSame(true); updateInput.setAreAllValuesBeingUpdatedTheSame(true);
updateInput.setTransaction(transaction);
updateInput.setOmitDmlAudit(true); updateInput.setOmitDmlAudit(true);
new UpdateAction().execute(updateInput); new UpdateAction().execute(updateInput);
@ -250,4 +333,8 @@ public class RecordAutomationStatusUpdater
} }
} }
private record Key(QTableMetaData table, TriggerEvent triggerEvent) {}
} }

View File

@ -251,8 +251,7 @@ public class PollingAutomationPerTableRunner implements Runnable
try try
{ {
QSession session = sessionSupplier != null ? sessionSupplier.get() : new QSession(); processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), tableActions.status());
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), session, tableActions.status());
} }
catch(Exception e) catch(Exception e)
{ {
@ -270,7 +269,7 @@ public class PollingAutomationPerTableRunner implements Runnable
/******************************************************************************* /*******************************************************************************
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table. ** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
*******************************************************************************/ *******************************************************************************/
public void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus) throws QException public void processTableInsertOrUpdate(QTableMetaData table, AutomationStatus automationStatus) throws QException
{ {
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
// get the actions to run against this table in this automation status // // get the actions to run against this table in this automation status //
@ -321,7 +320,7 @@ public class PollingAutomationPerTableRunner implements Runnable
}, () -> }, () ->
{ {
List<QRecord> records = recordPipe.consumeAvailableRecords(); List<QRecord> records = recordPipe.consumeAvailableRecords();
applyActionsToRecords(session, table, records, actions, automationStatus); applyActionsToRecords(table, records, actions, automationStatus);
return (records.size()); return (records.size());
} }
); );
@ -427,7 +426,7 @@ public class PollingAutomationPerTableRunner implements Runnable
** table's actions against them - IF they are found to match the action's filter ** table's actions against them - IF they are found to match the action's filter
** (assuming it has one - if it doesn't, then all records match). ** (assuming it has one - if it doesn't, then all records match).
*******************************************************************************/ *******************************************************************************/
private void applyActionsToRecords(QSession session, QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException private void applyActionsToRecords(QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException
{ {
if(CollectionUtils.nullSafeIsEmpty(records)) if(CollectionUtils.nullSafeIsEmpty(records))
{ {
@ -437,7 +436,7 @@ public class PollingAutomationPerTableRunner implements Runnable
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
// mark the records as RUNNING their automations // // mark the records as RUNNING their automations //
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToRunningStatusMap.get(automationStatus)); RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, pendingToRunningStatusMap.get(automationStatus), null);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// foreach action - run it against the records (but only if they match the action's filter, if there is one) // // foreach action - run it against the records (but only if they match the action's filter, if there is one) //
@ -457,11 +456,11 @@ public class PollingAutomationPerTableRunner implements Runnable
//////////////////////////////////////// ////////////////////////////////////////
if(anyActionsFailed) if(anyActionsFailed)
{ {
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToFailedStatusMap.get(automationStatus)); RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, pendingToFailedStatusMap.get(automationStatus), null);
} }
else else
{ {
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, AutomationStatus.OK); RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, AutomationStatus.OK, null);
} }
} }

View File

@ -444,7 +444,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
*******************************************************************************/ *******************************************************************************/
private void setAutomationStatusField(InsertInput insertInput) private void setAutomationStatusField(InsertInput insertInput)
{ {
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getSession(), insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS); RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS, insertInput.getTransaction());
} }

View File

@ -563,7 +563,7 @@ public class UpdateAction
*******************************************************************************/ *******************************************************************************/
private void setAutomationStatusField(UpdateInput updateInput) private void setAutomationStatusField(UpdateInput updateInput)
{ {
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getSession(), updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS); RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS, updateInput.getTransaction());
} }
} }

View File

@ -0,0 +1,220 @@
/*
* 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.actions.automation.polling;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
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.context.CapturedContext;
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.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.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunnerTest.runAllTableActions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/*******************************************************************************
** Test for the case where:
** - inserting into a main table and a child table, and the child table has a
** post-insert customizer, which mo
*******************************************************************************/
public class PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfiniteLoopTest extends BaseTest
{
private static boolean didFailInThread = false;
static
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// we can set this property to revert to the behavior that existed before this test was written. //
///////////////////////////////////////////////////////////////////////////////////////////////////
// System.setProperty("qqq.recordAutomationStatusUpdater.skipPreUpdateFetch", "true");
}
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
didFailInThread = false;
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
////////////////////////////////////
// add automations to order table //
////////////////////////////////////
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_ORDER)
.withField(TestUtils.standardQqqAutomationStatusField())
.withAutomationDetails(TestUtils.defineStandardAutomationDetails()
.withAction(new TableAutomationAction()
.withName("orderPostInsertAction")
.withTriggerEvent(TriggerEvent.POST_INSERT)
.withCodeReference(new QCodeReference(OrderPostInsertAndUpdateAction.class)))
.withAction(new TableAutomationAction()
.withName("orderPostUpdateAction")
.withTriggerEvent(TriggerEvent.POST_UPDATE)
.withCodeReference(new QCodeReference(OrderPostInsertAndUpdateAction.class))));
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord().withValue("orderNo", "10101").withValue("total", new BigDecimal(1))));
new InsertAction().execute(insertInput);
//////////////////////////////////////////////////////
// make sure the order is in pending-inserts status //
//////////////////////////////////////////////////////
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(1), order.getValueBigDecimal("total"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
// run automations - that should update the order via the automation - but leave status as OK //
////////////////////////////////////////////////////////////////////////////////////////////////
runAllTableActions(QContext.getQInstance());
assertFalse(didFailInThread, "A failure condition happened in the automation sub-thread. Check System.out for message.");
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
}
//////////////////////////////////////////////////////////////////
// now update the order, verify status moves to pending-updates //
//////////////////////////////////////////////////////////////////
new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_ORDER).withRecord(new QRecord()
.withValue("id", 1)
.withValue("storeId", "x")));
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
assertEquals("x", order.getValueString("storeId"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
// run automations - that should update the order via the automation - but leave status as OK //
////////////////////////////////////////////////////////////////////////////////////////////////
runAllTableActions(QContext.getQInstance());
assertFalse(didFailInThread, "A failure condition happened in the automation sub-thread. Check System.out for message.");
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(3), order.getValueBigDecimal("total"));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class OrderPostInsertAndUpdateAction extends RecordAutomationHandler
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// launch a new thread, to make sure we avoid the "stack contains automations" check in RecordAutomationStatusUpdater //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
CapturedContext capturedContext = QContext.capture();
for(QRecord record : recordAutomationInput.getRecordList())
{
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
Future<?> submit = service.submit(() ->
{
QContext.init(capturedContext);
try
{
new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_ORDER).withRecord(new QRecord()
.withValue("id", record.getValue("id"))
.withValue("total", record.getValueBigDecimal("total").add(new BigDecimal(1)))
));
///////////////////////////////////////////////////////////////////
// make sure that update action didn't change the order's status //
///////////////////////////////////////////////////////////////////
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
if(Objects.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName())))
{
System.out.println("Failing test - expected status to not be [PENDING_UPDATE_AUTOMATIONS], but it was.");
didFailInThread = true;
}
assertNotEquals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
}
catch(QException e)
{
e.printStackTrace();
}
finally
{
QContext.clear();
}
});
while(!submit.isDone())
{
}
}
}
}
}

View File

@ -76,6 +76,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class PollingAutomationPerTableRunnerChildPostInsertCustomizerTest extends BaseTest public class PollingAutomationPerTableRunnerChildPostInsertCustomizerTest extends BaseTest
{ {
static
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// we can set this property to revert to the behavior that existed before this test was written. //
///////////////////////////////////////////////////////////////////////////////////////////////////
// System.setProperty("qqq.recordAutomationStatusUpdater.skipPreUpdateFetch", "true");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -195,7 +195,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
///////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////
// note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. // // note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. //
///////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), QContext.getQSession(), tableAction.status()); pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
} }
} }
@ -497,7 +497,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
///////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////
// note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. // // note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. //
///////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), QContext.getQSession(), tableAction.status()); pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
} }
}).hasMessage(PollingAutomationPerTableRunnerThatShouldSimulateServerShutdownMidRun.EXCEPTION_MESSAGE); }).hasMessage(PollingAutomationPerTableRunnerThatShouldSimulateServerShutdownMidRun.EXCEPTION_MESSAGE);