From 58c21fda28628edb59c7001e88b51dc9069d3188 Mon Sep 17 00:00:00 2001 From: Deepak Dixit Date: Thu, 2 Feb 2023 11:21:41 +0530 Subject: [PATCH 01/61] Added missing package name in entity relationship --- framework/entity/ResourceEntities.xml | 2 +- framework/entity/ServerEntities.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/entity/ResourceEntities.xml b/framework/entity/ResourceEntities.xml index 349e5139a..f9ac9b8cc 100644 --- a/framework/entity/ResourceEntities.xml +++ b/framework/entity/ResourceEntities.xml @@ -145,7 +145,7 @@ along with this software (see the LICENSE.md file). If not, see - + diff --git a/framework/entity/ServerEntities.xml b/framework/entity/ServerEntities.xml index f6f0481c9..17ec6556d 100644 --- a/framework/entity/ServerEntities.xml +++ b/framework/entity/ServerEntities.xml @@ -116,9 +116,9 @@ along with this software (see the LICENSE.md file). If not, see - + - + From 24f1c2230e4e7b6007ad5d601621a11a98a41537 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Fri, 3 Feb 2023 15:35:29 -0800 Subject: [PATCH 02/61] Add StatusFlowTransitionFromAndTo view-entity --- framework/entity/BasicEntities.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/framework/entity/BasicEntities.xml b/framework/entity/BasicEntities.xml index 25615b2da..3ca7a10e4 100644 --- a/framework/entity/BasicEntities.xml +++ b/framework/entity/BasicEntities.xml @@ -373,6 +373,16 @@ along with this software (see the LICENSE.md file). If not, see + + + + + + + + + + From 5cb82477de6e85b40afd8ddd992e68025631983c Mon Sep 17 00:00:00 2001 From: David E Jones Date: Sat, 4 Feb 2023 04:46:22 -0800 Subject: [PATCH 03/61] In ServiceFacadeImpl add classes for Service LoadRunner, used for load testing of service calls with some profiling info, some basics for a first pass --- .../moqui/impl/context/ContextJavaUtil.java | 2 +- .../elastic/ElasticEntityListIterator.java | 1 + .../impl/service/ServiceCallAsyncImpl.groovy | 38 +++- .../impl/service/ServiceFacadeImpl.groovy | 199 ++++++++++++++++++ 4 files changed, 230 insertions(+), 10 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java b/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java index ea72322e4..7c9b62c90 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java +++ b/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java @@ -735,7 +735,7 @@ static class CustomScheduledTask implements RunnableScheduledFuture { return "CustomScheduledTask " + (runnable != null ? runnable.getClass().getName() : (callable != null ? callable.getClass().getName() : "[no Runnable or Callable!]")); } } - static class CustomScheduledExecutor extends ScheduledThreadPoolExecutor { + public static class CustomScheduledExecutor extends ScheduledThreadPoolExecutor { public CustomScheduledExecutor(int coreThreads) { super(coreThreads, new ScheduledThreadFactory()); } diff --git a/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticEntityListIterator.java b/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticEntityListIterator.java index bf2c594d0..947193eaf 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticEntityListIterator.java +++ b/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticEntityListIterator.java @@ -125,6 +125,7 @@ boolean nextResult() { // logger.warn("nextResult end resultCount " + resultCount + " overallIndex " + overallIndex + " currentListStartIndex " + currentListStartIndex + " currentDocList.size() " + currentDocList.size()); return hasCurrentValue(); } + @SuppressWarnings("unchecked") void fetchNext() { if (this.closed) throw new IllegalStateException("EntityListIterator is closed, cannot fetch next results"); diff --git a/framework/src/main/groovy/org/moqui/impl/service/ServiceCallAsyncImpl.groovy b/framework/src/main/groovy/org/moqui/impl/service/ServiceCallAsyncImpl.groovy index edf7903bb..3d4ddceeb 100644 --- a/framework/src/main/groovy/org/moqui/impl/service/ServiceCallAsyncImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/service/ServiceCallAsyncImpl.groovy @@ -102,6 +102,12 @@ class ServiceCallAsyncImpl extends ServiceCallImpl implements ServiceCallAsync { this.serviceName = serviceName this.parameters = new HashMap<>(parameters) } + AsyncServiceInfo(ExecutionContextFactoryImpl ecfi, String username, String serviceName, Map parameters) { + ecfiLocal = ecfi + threadUsername = username + this.serviceName = serviceName + this.parameters = new HashMap<>(parameters) + } @Override void writeExternal(ObjectOutput out) throws IOException { @@ -123,6 +129,9 @@ class ServiceCallAsyncImpl extends ServiceCallImpl implements ServiceCallAsync { } Map runInternal() throws Exception { + return runInternal(null, false) + } + Map runInternal(Map parameters, boolean skipEcCheck) throws Exception { ExecutionContextImpl threadEci = (ExecutionContextImpl) null try { // check for active Transaction @@ -135,22 +144,33 @@ class ServiceCallAsyncImpl extends ServiceCallImpl implements ServiceCallAsync { } } // check for active ExecutionContext - ExecutionContextImpl activeEc = getEcfi().activeContext.get() - if (activeEc != null) { - logger.error("In ServiceCallAsync service ${serviceName} there is already an ExecutionContext for user ${activeEc.user.username} (from ${activeEc.forThreadId}:${activeEc.forThreadName}) in this thread ${Thread.currentThread().id}:${Thread.currentThread().name}, destroying") - try { - activeEc.destroy() - } catch (Throwable t) { - logger.error("Error destroying ExecutionContext already in place in ServiceCallAsync in thread ${Thread.currentThread().id}:${Thread.currentThread().name}", t) + if (!skipEcCheck) { + ExecutionContextImpl activeEc = getEcfi().activeContext.get() + if (activeEc != null) { + logger.error("In ServiceCallAsync service ${serviceName} there is already an ExecutionContext for user ${activeEc.user.username} (from ${activeEc.forThreadId}:${activeEc.forThreadName}) in this thread ${Thread.currentThread().id}:${Thread.currentThread().name}, destroying") + try { + activeEc.destroy() + } catch (Throwable t) { + logger.error("Error destroying ExecutionContext already in place in ServiceCallAsync in thread ${Thread.currentThread().id}:${Thread.currentThread().name}", t) + } } } threadEci = getEcfi().getEci() - if (threadUsername != null && threadUsername.length() > 0) + if (threadUsername != null && threadUsername.length() > 0) { threadEci.userFacade.internalLoginUser(threadUsername, false) + } else { + threadEci.userFacade.loginAnonymousIfNoUser() + } + + Map parmsToUse = this.parameters + if (parameters != null) { + parmsToUse = new HashMap<>(this.parameters) + parmsToUse.putAll(parameters) + } // NOTE: authz is disabled because authz is checked before queueing - Map result = threadEci.serviceFacade.sync().name(serviceName).parameters(parameters).disableAuthz().call() + Map result = threadEci.serviceFacade.sync().name(serviceName).parameters(parmsToUse).disableAuthz().call() return result } catch (Throwable t) { logger.error("Error in async service", t) diff --git a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy index f28eca2cc..167cc2461 100644 --- a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy @@ -15,6 +15,7 @@ package org.moqui.impl.service import groovy.transform.CompileStatic import org.moqui.impl.context.ContextJavaUtil +import org.moqui.impl.context.ContextJavaUtil.CustomScheduledExecutor import org.moqui.resource.ResourceReference import org.moqui.context.ToolFactory import org.moqui.impl.context.ExecutionContextFactoryImpl @@ -33,6 +34,7 @@ import org.slf4j.LoggerFactory import javax.cache.Cache import javax.mail.internet.MimeMessage import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @CompileStatic @@ -52,6 +54,7 @@ class ServiceFacadeImpl implements ServiceFacade { private ScheduledJobRunner jobRunner = null public final ThreadPoolExecutor jobWorkerPool + private LoadRunner loadRunner = null /** Distributed ExecutorService for async services, etc */ protected ExecutorService distributedExecutorService = null @@ -599,4 +602,200 @@ class ServiceFacadeImpl implements ServiceFacade { if (callbackList != null && callbackList.size() > 0) for (ServiceCallback scb in callbackList) scb.receiveEvent(context, t) } + + // ========================== + // Service LoadRunner Classes + // ========================== + + synchronized LoadRunner getLoadRunner() { + if (loadRunner == null) loadRunner = new LoadRunner(ecfi) + return loadRunner + } + + static class LoadRunnerServiceRunnable extends ServiceCallAsyncImpl.AsyncServiceInfo implements Runnable, Externalizable { + LoadRunner loadRunner + LoadRunnerServiceInfo serviceInfo + LoadRunnerServiceRunnable(String username, String serviceName, Map parameters, + LoadRunner loadRunner, LoadRunnerServiceInfo serviceInfo) { + super(loadRunner.ecfi, username, serviceName, parameters) + this.loadRunner = loadRunner + this.serviceInfo = serviceInfo + } + @Override void run() { + // TODO other parameters, maybe configurable with expanded strings? + String parametersExpr = serviceInfo.parametersExpr + Map parameters = [execIndex:loadRunner.execIndex.getAndIncrement()] as Map + if (parametersExpr != null && !parametersExpr.isEmpty()) { + try { + Map exprMap = (Map) loadRunner.ecfi.getEci().resourceFacade + .expression(parametersExpr, null, parameters) + if (exprMap != null) parameters.putAll(exprMap) + } catch (Throwable t) { + logger.error("Error in Service LoadRunner parameter expression: ${parametersExpr}", t) + } + } + + long startTime = System.currentTimeMillis() + try { + serviceInfo.lastResult = runInternal(parameters, true) + } catch (Throwable t) { + // logged elsewhere, just swallow and count + serviceInfo.errorCount++ + } + long endTime = System.currentTimeMillis() + long runTime = endTime - startTime + serviceInfo.runCount++ + serviceInfo.lastRunTime = endTime + serviceInfo.totalTime += runTime + serviceInfo.totalSquaredTime += runTime * runTime + } + } + static class LoadRunnerServiceInfo { + String serviceName, parametersExpr = null + int targetThreads, runDelayMs, rampDelayMs + AtomicInteger currentThreads = new AtomicInteger(0) + long runCount = 0, errorCount = 0, lastRunTime = 0, beginTime = 0, totalTime = 0, totalSquaredTime = 0 + Map lastResult = null + LoadRunnerServiceInfo(String serviceName, int targetThreads, int runDelayMs, int rampDelayMs) { + this.serviceName = serviceName + this.targetThreads = targetThreads + this.runDelayMs = runDelayMs + this.rampDelayMs = rampDelayMs + } + void addThread(LoadRunner loadRunner) { + LoadRunnerServiceRunnable runnable = + new LoadRunnerServiceRunnable(null, serviceName, [:], loadRunner, this) + // NOTE: use scheduleWithFixedDelay so delay is wait between terminate of one and start of another, better for both short and long running test runs + loadRunner.scheduledExecutor.scheduleWithFixedDelay(runnable, 1, runDelayMs, TimeUnit.MILLISECONDS) + } + void addRampThread(LoadRunner loadRunner) { + beginTime = System.currentTimeMillis() + LoadRunnerRamperRunnable runnable = new LoadRunnerRamperRunnable(loadRunner, this) + // NOTE: use scheduleAtFixedRate so one is added each delay period regardless of how long it takes (generally not long) + loadRunner.scheduledExecutor.scheduleAtFixedRate(runnable, 1, rampDelayMs, TimeUnit.MILLISECONDS) + } + } + static class LoadRunnerRamperRunnable implements Runnable { + LoadRunner loadRunner + LoadRunnerServiceInfo serviceInfo + LoadRunnerRamperRunnable(LoadRunner loadRunner, LoadRunnerServiceInfo serviceInfo) { + this.loadRunner = loadRunner + this.serviceInfo = serviceInfo + } + @Override void run() { + if (serviceInfo.currentThreads < serviceInfo.targetThreads) { + serviceInfo.addThread(loadRunner) + // may not actually need AtomicInteger here, but for ramp down will need a list of ScheduledFuture objects and might be useful there + serviceInfo.currentThreads.incrementAndGet() + } + // TODO add delayed ramp-down, useful for some performance behavior patterns but usually redundant with delayed ramp up to look for elbows in the response time over time + } + } + static class LoadRunner { + ExecutionContextFactoryImpl ecfi + CustomScheduledExecutor scheduledExecutor = null + ArrayList serviceInfos = new ArrayList<>() + int corePoolSize = 4, maxPoolSize = 8 + AtomicInteger execIndex = new AtomicInteger(1) + ReentrantLock mutateLock = new ReentrantLock() + + LoadRunner(ExecutionContextFactoryImpl ecfi) { + this.ecfi = ecfi + } + + void setServiceInfo(String serviceName, String parametersExpr, int targetThreads, int runDelayMs, int rampDelayMs) { + mutateLock.lock() + try { + LoadRunnerServiceInfo serviceInfo = null + for (int i = 0; i < serviceInfos.size(); i++) { + LoadRunnerServiceInfo curInfo = (LoadRunnerServiceInfo) serviceInfos.get(i) + if (curInfo.serviceName == serviceName) serviceInfo = curInfo + } + if (serviceInfo == null) { + serviceInfo = new LoadRunnerServiceInfo(serviceName, targetThreads, runDelayMs, rampDelayMs) + serviceInfo.parametersExpr = parametersExpr + + serviceInfos.add(serviceInfo) + + if (scheduledExecutor != null) { + // begin() already called, get this started + serviceInfo.addRampThread(this) + } + } else { + serviceInfo.parametersExpr = parametersExpr + serviceInfo.targetThreads = targetThreads + serviceInfo.runDelayMs = runDelayMs + serviceInfo.rampDelayMs = rampDelayMs + } + } finally { + mutateLock.unlock() + } + } + void begin() { + mutateLock.lock() + try { + // TODO set maxPoolSize to CPU count x2 or something if needed to scale the load runner itself; probably not much as services running on same system... + if (scheduledExecutor == null) { + // restart index + execIndex = new AtomicInteger(1) + + for (int i = 0; i < serviceInfos.size(); i++) { + LoadRunnerServiceInfo curInfo = (LoadRunnerServiceInfo) serviceInfos.get(i) + // clear out stats before start + curInfo.currentThreads = new AtomicInteger(0) + curInfo.runCount = 0 + curInfo.errorCount = 0 + curInfo.lastRunTime = 0 + curInfo.beginTime = 0 + curInfo.totalTime = 0 + curInfo.totalSquaredTime = 0 + curInfo.lastResult = null + } + + scheduledExecutor = new CustomScheduledExecutor(corePoolSize) + scheduledExecutor.setMaximumPoolSize(maxPoolSize) + + for (int i = 0; i < serviceInfos.size(); i++) { + LoadRunnerServiceInfo curInfo = (LoadRunnerServiceInfo) serviceInfos.get(i) + // do this once here + curInfo.addThread(this) + // add a schedule for the rest + curInfo.addRampThread(this) + } + } + } finally { + mutateLock.unlock() + } + } + void stopNow() { + mutateLock.lock() + try { + if (scheduledExecutor != null) { + logger.info("Shutting down LoadRunner ScheduledExecutorService now") + scheduledExecutor.shutdownNow() + scheduledExecutor = null + } + } finally { + mutateLock.unlock() + } + } + void stopWait() { + mutateLock.lock() + try { + if (scheduledExecutor != null) { + logger.info("Shutting down LoadRunner ScheduledExecutorService") + scheduledExecutor.shutdown() + } + + if (scheduledExecutor != null) { + scheduledExecutor.awaitTermination(30, TimeUnit.SECONDS) + if (scheduledExecutor.isTerminated()) logger.info("LoadRunner Scheduled executor shut down and terminated") + else logger.warn("LoadRunner Scheduled executor NOT YET terminated, waited 30 seconds") + scheduledExecutor = null + } + } finally { + mutateLock.unlock() + } + } + } } From 63b510d9c5e5ba86edb165a9da56e85fa2461e1c Mon Sep 17 00:00:00 2001 From: David E Jones Date: Sat, 4 Feb 2023 13:30:19 -0800 Subject: [PATCH 04/61] In Service LoadRunner add stats broken down by artifact type with detail, part of code is more generic addition to ArtifactExecutionInfoImpl --- .../ArtifactExecutionFacadeImpl.groovy | 4 + .../context/ArtifactExecutionInfoImpl.java | 118 ++++++++++++++++++ .../impl/service/ServiceFacadeImpl.groovy | 81 +++++++++--- .../java/org/moqui/util/ObjectUtilities.java | 46 +++++++ 4 files changed, 229 insertions(+), 20 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionFacadeImpl.groovy index 82e9acb3d..5edf7ab96 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionFacadeImpl.groovy @@ -172,6 +172,10 @@ class ArtifactExecutionFacadeImpl implements ArtifactExecutionFacade { return sw.toString() } + ArtifactExecutionInfoImpl.ArtifactTypeStats getArtifactTypeStats() { + return ArtifactExecutionInfoImpl.getArtifactTypeStats(artifactExecutionInfoHistory) + } + void logProfilingDetail() { if (!logger.isInfoEnabled()) return diff --git a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java index 5880cf390..084d2e04b 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java +++ b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java @@ -17,6 +17,8 @@ import org.moqui.impl.entity.EntityValueBase; import org.moqui.util.CollectionUtilities; import org.moqui.util.StringUtilities; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.StringWriter; @@ -26,6 +28,7 @@ import java.util.*; public class ArtifactExecutionInfoImpl implements ArtifactExecutionInfo { + protected final static Logger logger = LoggerFactory.getLogger(ArtifactExecutionInfoImpl.class); // NOTE: these need to be in a Map instead of the DB because Enumeration records may not yet be loaded private final static Map artifactTypeDescriptionMap = new EnumMap<>(ArtifactType.class); @@ -153,6 +156,7 @@ public void copyAuthorizedInfo(ArtifactExecutionInfoImpl aeii) { @Override public long getRunningTime() { return endTimeNanos != 0 ? endTimeNanos - startTimeNanos : 0; } public double getRunningTimeMillisDouble() { return (endTimeNanos != 0 ? endTimeNanos - startTimeNanos : 0) / 1000000.0; } + public long getRunningTimeMillisLong() { return Math.round((endTimeNanos != 0 ? endTimeNanos - startTimeNanos : 0) / 1000000.0); } private void calcChildTime(boolean recurse) { childrenRunningTime = 0; if (childList != null) for (ArtifactExecutionInfoImpl aeii: childList) { @@ -211,6 +215,120 @@ public void print(Writer writer, int level, boolean children) { } private String getKeyString() { return nameInternal + ":" + internalTypeEnum.name() + ":" + internalActionEnum.name() + ":" + actionDetail; } + private String getKeyStringNoName() { return internalTypeEnum.name() + ":" + internalActionEnum.name() + ":" + actionDetail; } + + public static class ArtifactTypeStats { + public int screenCount = 0, screenTransCount = 0, screenContentCount = 0, restPathCount = 0, + serviceViewCount = 0, serviceOtherCount = 0, + entityFindOneCount = 0, entityFindListCount = 0, entityFindIteratorCount = 0, entityFindCountCount = 0, + entityCreateCount = 0, entityUpdateCount = 0, entityDeleteCount = 0; + public long screenTime = 0, screenTransTime = 0, screenContentTime = 0, restPathTime = 0, + serviceViewTime = 0, serviceOtherTime = 0, + entityFindOneTime = 0, entityFindListTime = 0, entityFindIteratorTime = 0, entityFindCountTime = 0, + entityCreateTime = 0, entityUpdateTime = 0, entityDeleteTime = 0; + public void add(ArtifactTypeStats that) { + screenCount += that.screenCount; screenTransCount += that.screenTransCount; + screenContentCount += that.screenContentCount; restPathCount += that.restPathCount; + serviceViewCount += that.serviceViewCount; serviceOtherCount += that.serviceOtherCount; + entityFindOneCount += that.entityFindOneCount; entityFindListCount += that.entityFindListCount; + entityFindIteratorCount += that.entityFindIteratorCount; entityFindCountCount += that.entityFindCountCount; + entityCreateCount += that.entityCreateCount; entityUpdateCount += that.entityUpdateCount; + entityDeleteCount += that.entityDeleteCount; + + screenTime += that.screenTime; screenTransTime += that.screenTransTime; + screenContentTime += that.screenContentTime; restPathTime += that.restPathTime; + serviceViewTime += that.serviceViewTime; serviceOtherTime += that.serviceOtherTime; + entityFindOneTime += that.entityFindOneTime; entityFindListTime += that.entityFindListTime; + entityFindIteratorTime += that.entityFindIteratorTime; entityFindCountTime += that.entityFindCountTime; + entityCreateTime += that.entityCreateTime; entityUpdateTime += that.entityUpdateTime; + entityDeleteTime += that.entityDeleteTime; + } + } + static ArtifactTypeStats getArtifactTypeStats(ArrayList aeiiList) { + ArtifactTypeStats stats = new ArtifactTypeStats(); + addArtifactTypeStats(aeiiList, stats); + return stats; + } + static void addArtifactTypeStats(ArrayList aeiiList, ArtifactTypeStats stats) { + if (aeiiList == null) return; + int aeiiListSize = aeiiList.size(); + for (int i = 0; i < aeiiListSize; i++) { + ArtifactExecutionInfoImpl aeii = aeiiList.get(i); + // tight loop, use switch instead of if on these enums for much better performance; run fast for use in on the fly accumulators + switch (aeii.internalTypeEnum) { + case AT_ENTITY: + switch (aeii.internalActionEnum) { + case AUTHZA_VIEW: + if (aeii.actionDetail != null && !aeii.actionDetail.isEmpty()) { + char first = aeii.actionDetail.charAt(0); + switch (first) { + case 'o': // one + case 'r': // refresh + stats.entityFindOneCount++; + stats.entityFindOneTime += aeii.getRunningTime(); + break; + case 'l': // list + stats.entityFindListCount++; + stats.entityFindListTime += aeii.getRunningTime(); + break; + case 'i': // iterator + stats.entityFindIteratorCount++; + stats.entityFindIteratorTime += aeii.getRunningTime(); + break; + case 'c': // count + stats.entityFindCountCount++; + stats.entityFindCountTime += aeii.getRunningTime(); + break; + } + } else { + logger.warn("entity view with no detail " + aeii.toBasicString()); + } + break; + case AUTHZA_CREATE: + stats.entityCreateCount++; + stats.entityCreateTime += aeii.getRunningTime(); + break; + case AUTHZA_UPDATE: + stats.entityUpdateCount++; + stats.entityUpdateTime += aeii.getRunningTime(); + break; + case AUTHZA_DELETE: + stats.entityDeleteCount++; + stats.entityDeleteTime += aeii.getRunningTime(); + break; + } + break; + case AT_SERVICE: + if (aeii.internalActionEnum == AUTHZA_VIEW) { + stats.serviceViewCount++; + stats.serviceViewTime += aeii.getRunningTime(); + } else { + stats.serviceOtherCount++; + stats.serviceOtherTime += aeii.getRunningTime(); + } + break; + case AT_XML_SCREEN: + stats.screenCount++; + stats.screenTime += aeii.getRunningTime(); + break; + case AT_XML_SCREEN_TRANS: + stats.screenTransCount++; + stats.screenTransTime += aeii.getRunningTime(); + break; + case AT_XML_SCREEN_CONTENT: + stats.screenContentCount++; + stats.screenContentTime += aeii.getRunningTime(); + break; + case AT_REST_PATH: + stats.restPathCount++; + stats.restPathTime += aeii.getRunningTime(); + break; + } + + // this aeii is done, how about children? + addArtifactTypeStats(aeii.childList, stats); + } + } @SuppressWarnings("unchecked") static List> hotSpotByTime(List aeiiList, boolean ownTime, String orderBy) { diff --git a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy index 167cc2461..c7b0087f3 100644 --- a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy @@ -14,6 +14,8 @@ package org.moqui.impl.service import groovy.transform.CompileStatic +import org.moqui.impl.context.ArtifactExecutionInfoImpl +import org.moqui.impl.context.ArtifactExecutionInfoImpl.ArtifactTypeStats import org.moqui.impl.context.ContextJavaUtil import org.moqui.impl.context.ContextJavaUtil.CustomScheduledExecutor import org.moqui.resource.ResourceReference @@ -612,19 +614,41 @@ class ServiceFacadeImpl implements ServiceFacade { return loadRunner } - static class LoadRunnerServiceRunnable extends ServiceCallAsyncImpl.AsyncServiceInfo implements Runnable, Externalizable { + static class LoadRunnerServiceRunnable implements Runnable { + ExecutionContextFactoryImpl ecfi + String serviceName LoadRunner loadRunner LoadRunnerServiceInfo serviceInfo - LoadRunnerServiceRunnable(String username, String serviceName, Map parameters, - LoadRunner loadRunner, LoadRunnerServiceInfo serviceInfo) { - super(loadRunner.ecfi, username, serviceName, parameters) + + LoadRunnerServiceRunnable(String serviceName, LoadRunner loadRunner, LoadRunnerServiceInfo serviceInfo) { + this.ecfi = loadRunner.ecfi + this.serviceName = serviceName this.loadRunner = loadRunner this.serviceInfo = serviceInfo } @Override void run() { - // TODO other parameters, maybe configurable with expanded strings? + // check for active Transaction + if (getEcfi().transactionFacade.isTransactionInPlace()) { + logger.error("In LoadRunner service ${serviceName} a transaction is in place for thread ${Thread.currentThread().getName()}, trying to commit") + try { + getEcfi().transactionFacade.destroyAllInThread() + } catch (Exception e) { + logger.error("LoadRunner commit in place transaction failed for thread ${Thread.currentThread().getName()}", e) + } + } + // check for active ExecutionContext + ExecutionContextImpl activeEc = getEcfi().activeContext.get() + if (activeEc != null) { + logger.error("In LoadRunner service ${serviceName} there is already an ExecutionContext for user ${activeEc.user.username} (from ${activeEc.forThreadId}:${activeEc.forThreadName}) in this thread ${Thread.currentThread().id}:${Thread.currentThread().name}, destroying") + try { + activeEc.destroy() + } catch (Throwable t) { + logger.error("Error destroying LoadRunner already in place in ServiceCallAsync in thread ${Thread.currentThread().id}:${Thread.currentThread().name}", t) + } + } + String parametersExpr = serviceInfo.parametersExpr - Map parameters = [execIndex:loadRunner.execIndex.getAndIncrement()] as Map + Map parameters = [index:loadRunner.execIndex.getAndIncrement()] as Map if (parametersExpr != null && !parametersExpr.isEmpty()) { try { Map exprMap = (Map) loadRunner.ecfi.getEci().resourceFacade @@ -636,26 +660,44 @@ class ServiceFacadeImpl implements ServiceFacade { } long startTime = System.currentTimeMillis() + ExecutionContextImpl threadEci = ecfi.getEci() try { - serviceInfo.lastResult = runInternal(parameters, true) - } catch (Throwable t) { - // logged elsewhere, just swallow and count - serviceInfo.errorCount++ + // always login anonymous, disable authz below + threadEci.userFacade.loginAnonymousIfNoUser() + + // run the service + try { + serviceInfo.lastResult = threadEci.serviceFacade.sync().name(serviceName).parameters(parameters).disableAuthz().call() + } catch (Throwable t) { + // logged elsewhere, just count and swallow + serviceInfo.errorCount++ + } + + serviceInfo.artifactTypeStats.add(threadEci.artifactExecutionFacade.getArtifactTypeStats()) + } finally { + if (threadEci != null) threadEci.destroy() } + long endTime = System.currentTimeMillis() long runTime = endTime - startTime serviceInfo.runCount++ serviceInfo.lastRunTime = endTime serviceInfo.totalTime += runTime serviceInfo.totalSquaredTime += runTime * runTime + if (runTime < serviceInfo.minTime) serviceInfo.minTime = runTime + if (runTime > serviceInfo.maxTime) serviceInfo.maxTime = runTime } } static class LoadRunnerServiceInfo { String serviceName, parametersExpr = null int targetThreads, runDelayMs, rampDelayMs AtomicInteger currentThreads = new AtomicInteger(0) - long runCount = 0, errorCount = 0, lastRunTime = 0, beginTime = 0, totalTime = 0, totalSquaredTime = 0 + + long lastRunTime = 0, beginTime = 0, totalTime = 0, totalSquaredTime = 0, minTime = Long.MAX_VALUE, maxTime = 0 + int runCount = 0, errorCount = 0 + ArtifactTypeStats artifactTypeStats = new ArtifactTypeStats() Map lastResult = null + LoadRunnerServiceInfo(String serviceName, int targetThreads, int runDelayMs, int rampDelayMs) { this.serviceName = serviceName this.targetThreads = targetThreads @@ -663,8 +705,7 @@ class ServiceFacadeImpl implements ServiceFacade { this.rampDelayMs = rampDelayMs } void addThread(LoadRunner loadRunner) { - LoadRunnerServiceRunnable runnable = - new LoadRunnerServiceRunnable(null, serviceName, [:], loadRunner, this) + LoadRunnerServiceRunnable runnable = new LoadRunnerServiceRunnable(serviceName, loadRunner, this) // NOTE: use scheduleWithFixedDelay so delay is wait between terminate of one and start of another, better for both short and long running test runs loadRunner.scheduledExecutor.scheduleWithFixedDelay(runnable, 1, runDelayMs, TimeUnit.MILLISECONDS) } @@ -674,6 +715,12 @@ class ServiceFacadeImpl implements ServiceFacade { // NOTE: use scheduleAtFixedRate so one is added each delay period regardless of how long it takes (generally not long) loadRunner.scheduledExecutor.scheduleAtFixedRate(runnable, 1, rampDelayMs, TimeUnit.MILLISECONDS) } + void resetStats() { + lastRunTime = 0; beginTime = 0; totalTime = 0; totalSquaredTime = 0; minTime = Long.MAX_VALUE; maxTime = 0 + runCount = 0; errorCount = 0 + artifactTypeStats = new ArtifactTypeStats() + lastResult = null + } } static class LoadRunnerRamperRunnable implements Runnable { LoadRunner loadRunner @@ -743,13 +790,7 @@ class ServiceFacadeImpl implements ServiceFacade { LoadRunnerServiceInfo curInfo = (LoadRunnerServiceInfo) serviceInfos.get(i) // clear out stats before start curInfo.currentThreads = new AtomicInteger(0) - curInfo.runCount = 0 - curInfo.errorCount = 0 - curInfo.lastRunTime = 0 - curInfo.beginTime = 0 - curInfo.totalTime = 0 - curInfo.totalSquaredTime = 0 - curInfo.lastResult = null + curInfo.resetStats() } scheduledExecutor = new CustomScheduledExecutor(corePoolSize) diff --git a/framework/src/main/java/org/moqui/util/ObjectUtilities.java b/framework/src/main/java/org/moqui/util/ObjectUtilities.java index 1be291e92..f2b794603 100644 --- a/framework/src/main/java/org/moqui/util/ObjectUtilities.java +++ b/framework/src/main/java/org/moqui/util/ObjectUtilities.java @@ -18,7 +18,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.math.BigDecimal; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -53,6 +60,45 @@ public class ObjectUtilities { temporalUnitByUomId = tum; } + /** Populate a Map with public fields and Java Bean style properties (using java.beans.BeanInfo) */ + public static Map objectToMap(Object bean) { + if (bean == null) return null; + Map map = new HashMap<>(); + + Class clazz = bean.getClass(); + Field[] fields = clazz.getFields(); + for (int fi = 0; fi < fields.length; fi++) { + Field field = fields[fi]; + try { + map.put(field.getName(), field.get(bean)); + } catch (IllegalAccessException e) { + // do nothing, maybe log at some point if we care enough and are okay with the potential performance hit + } + } + + try { + BeanInfo beanInfo = Introspector.getBeanInfo(clazz); + PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); + for (int pi = 0; pi < propertyDescriptors.length; pi++) { + PropertyDescriptor propertyDescriptor = propertyDescriptors[pi]; + Method readMethod = propertyDescriptor.getReadMethod(); + if (readMethod != null) { + try { + map.put(propertyDescriptor.getName(), readMethod.invoke(bean)); + } catch (IllegalAccessException | InvocationTargetException e) { + // nothing again + } + } + } + } catch (IntrospectionException e) { + // nothing again + } + + // this gets picked up automatically, just remove at the end, faster than checking along the way + map.remove("class"); + + return map; + } @SuppressWarnings("unchecked") public static Object basicConvert(Object value, final String javaType) { From f7471752cf85e8ffb64d0fb270531afdf4f42f65 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Sat, 4 Feb 2023 19:40:39 -0800 Subject: [PATCH 05/61] Service LoadRunner improved stats gathering with time bin based stats in addition to entire run stats, prep work for performance charts and such --- .../context/ArtifactExecutionInfoImpl.java | 8 + .../moqui/impl/context/ContextJavaUtil.java | 9 +- .../impl/service/ServiceFacadeImpl.groovy | 198 ++++++++++++++---- .../java/org/moqui/util/ObjectUtilities.java | 2 + 4 files changed, 171 insertions(+), 46 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java index 084d2e04b..67fe40d54 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java +++ b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java @@ -218,6 +218,7 @@ public void print(Writer writer, int level, boolean children) { private String getKeyStringNoName() { return internalTypeEnum.name() + ":" + internalActionEnum.name() + ":" + actionDetail; } public static class ArtifactTypeStats { + public int screenCount = 0, screenTransCount = 0, screenContentCount = 0, restPathCount = 0, serviceViewCount = 0, serviceOtherCount = 0, entityFindOneCount = 0, entityFindListCount = 0, entityFindIteratorCount = 0, entityFindCountCount = 0, @@ -227,6 +228,8 @@ public static class ArtifactTypeStats { entityFindOneTime = 0, entityFindListTime = 0, entityFindIteratorTime = 0, entityFindCountTime = 0, entityCreateTime = 0, entityUpdateTime = 0, entityDeleteTime = 0; public void add(ArtifactTypeStats that) { + if (that == null) return; + screenCount += that.screenCount; screenTransCount += that.screenTransCount; screenContentCount += that.screenContentCount; restPathCount += that.restPathCount; serviceViewCount += that.serviceViewCount; serviceOtherCount += that.serviceOtherCount; @@ -243,6 +246,11 @@ public void add(ArtifactTypeStats that) { entityCreateTime += that.entityCreateTime; entityUpdateTime += that.entityUpdateTime; entityDeleteTime += that.entityDeleteTime; } + public ArtifactTypeStats cloneStats(ArtifactTypeStats that) { + ArtifactTypeStats newStats = new ArtifactTypeStats(); + newStats.add(that); + return newStats; + } } static ArtifactTypeStats getArtifactTypeStats(ArrayList aeiiList) { ArtifactTypeStats stats = new ArtifactTypeStats(); diff --git a/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java b/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java index 7c9b62c90..9f2321e38 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java +++ b/framework/src/main/groovy/org/moqui/impl/context/ContextJavaUtil.java @@ -695,17 +695,17 @@ static class ScheduledThreadFactory implements ThreadFactory { private final AtomicInteger threadNumber = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(workerGroup, r, "MoquiScheduled-" + threadNumber.getAndIncrement()); } } - static class CustomScheduledTask implements RunnableScheduledFuture { + public static class CustomScheduledTask implements RunnableScheduledFuture { public final Runnable runnable; public final Callable callable; public final RunnableScheduledFuture future; - CustomScheduledTask(Runnable runnable, RunnableScheduledFuture future) { + public CustomScheduledTask(Runnable runnable, RunnableScheduledFuture future) { this.runnable = runnable; this.callable = null; this.future = future; } - CustomScheduledTask(Callable callable, RunnableScheduledFuture future) { + public CustomScheduledTask(Callable callable, RunnableScheduledFuture future) { this.runnable = null; this.callable = callable; this.future = future; @@ -739,6 +739,9 @@ public static class CustomScheduledExecutor extends ScheduledThreadPoolExecutor public CustomScheduledExecutor(int coreThreads) { super(coreThreads, new ScheduledThreadFactory()); } + public CustomScheduledExecutor(int coreThreads, ThreadFactory threadFactory) { + super(coreThreads, threadFactory); + } protected RunnableScheduledFuture decorateTask(Runnable r, RunnableScheduledFuture task) { return new CustomScheduledTask(r, task); } diff --git a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy index c7b0087f3..b6c5fb52f 100644 --- a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy @@ -14,6 +14,7 @@ package org.moqui.impl.service import groovy.transform.CompileStatic +import org.moqui.Moqui import org.moqui.impl.context.ArtifactExecutionInfoImpl import org.moqui.impl.context.ArtifactExecutionInfoImpl.ArtifactTypeStats import org.moqui.impl.context.ContextJavaUtil @@ -28,6 +29,7 @@ import org.moqui.impl.service.runner.RemoteJsonRpcServiceRunner import org.moqui.service.* import org.moqui.util.CollectionUtilities import org.moqui.util.MNode +import org.moqui.util.ObjectUtilities import org.moqui.util.RestClient import org.moqui.util.StringUtilities import org.slf4j.Logger @@ -35,6 +37,7 @@ import org.slf4j.LoggerFactory import javax.cache.Cache import javax.mail.internet.MimeMessage +import java.sql.Timestamp import java.util.concurrent.* import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -614,19 +617,43 @@ class ServiceFacadeImpl implements ServiceFacade { return loadRunner } - static class LoadRunnerServiceRunnable implements Runnable { - ExecutionContextFactoryImpl ecfi - String serviceName - LoadRunner loadRunner - LoadRunnerServiceInfo serviceInfo + static class LoadRunnerServiceRunnable implements Runnable, Externalizable { + volatile ExecutionContextFactoryImpl ecfi + volatile LoadRunner loadRunner + String serviceName, parametersExpr - LoadRunnerServiceRunnable(String serviceName, LoadRunner loadRunner, LoadRunnerServiceInfo serviceInfo) { + LoadRunnerServiceRunnable() { + // init the other objects that can't be serialized + ecfi = (ExecutionContextFactoryImpl) Moqui.getExecutionContextFactory() + loadRunner = ecfi.serviceFacade.getLoadRunner() + } + LoadRunnerServiceRunnable(String serviceName, String parametersExpr, LoadRunner loadRunner) { + this.loadRunner = loadRunner this.ecfi = loadRunner.ecfi this.serviceName = serviceName - this.loadRunner = loadRunner - this.serviceInfo = serviceInfo + this.parametersExpr = parametersExpr + } + + @Override + void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(serviceName) // never null + out.writeObject(parametersExpr) // may be null } + + @Override + void readExternal(ObjectInput objectInput) throws IOException, ClassNotFoundException { + serviceName = objectInput.readUTF() + parametersExpr = objectInput.readObject() + } + @Override void run() { + try { + runInternal() + } catch (Throwable t) { + logger.error("Error in LoadRunner service run", t) + } + } + void runInternal() { // check for active Transaction if (getEcfi().transactionFacade.isTransactionInPlace()) { logger.error("In LoadRunner service ${serviceName} a transaction is in place for thread ${Thread.currentThread().getName()}, trying to commit") @@ -647,6 +674,11 @@ class ServiceFacadeImpl implements ServiceFacade { } } + LoadRunnerServiceInfo serviceInfo = loadRunner.getServiceInfo(serviceName, parametersExpr) + if (serviceInfo == null) { + logger.error("Service Info not found for ${serviceName} ${parametersExpr}, not running") + return + } String parametersExpr = serviceInfo.parametersExpr Map parameters = [index:loadRunner.execIndex.getAndIncrement()] as Map if (parametersExpr != null && !parametersExpr.isEmpty()) { @@ -673,53 +705,112 @@ class ServiceFacadeImpl implements ServiceFacade { serviceInfo.errorCount++ } - serviceInfo.artifactTypeStats.add(threadEci.artifactExecutionFacade.getArtifactTypeStats()) + // count the run and accumulate stats + serviceInfo.countRun(loadRunner, startTime, System.currentTimeMillis(), + threadEci.artifactExecutionFacade.getArtifactTypeStats()) } finally { if (threadEci != null) threadEci.destroy() } - - long endTime = System.currentTimeMillis() - long runTime = endTime - startTime - serviceInfo.runCount++ - serviceInfo.lastRunTime = endTime - serviceInfo.totalTime += runTime - serviceInfo.totalSquaredTime += runTime * runTime - if (runTime < serviceInfo.minTime) serviceInfo.minTime = runTime - if (runTime > serviceInfo.maxTime) serviceInfo.maxTime = runTime } } - static class LoadRunnerServiceInfo { - String serviceName, parametersExpr = null - int targetThreads, runDelayMs, rampDelayMs - AtomicInteger currentThreads = new AtomicInteger(0) - + static class LoadRunnerServiceStats { long lastRunTime = 0, beginTime = 0, totalTime = 0, totalSquaredTime = 0, minTime = Long.MAX_VALUE, maxTime = 0 int runCount = 0, errorCount = 0 ArtifactTypeStats artifactTypeStats = new ArtifactTypeStats() + Map getMap() { + Map newMap = [lastRunTime:lastRunTime, beginTime:beginTime, totalTime:totalTime, + totalSquaredTime:totalSquaredTime, minTime:minTime, maxTime:maxTime, runCount:runCount, errorCount:errorCount] as Map + newMap.put("artifactTypeStats", ObjectUtilities.objectToMap(artifactTypeStats)) + return newMap + } + } + static class LoadRunnerServiceInfo extends LoadRunnerServiceStats { + String serviceName, parametersExpr + int targetThreads, runDelayMs, rampDelayMs, timeBinLength, timeBinsKeep + AtomicInteger currentThreads = new AtomicInteger(0) + Map lastResult = null + ConcurrentLinkedDeque timeBinList = new ConcurrentLinkedDeque<>() + ArrayList runFutures = new ArrayList<>() + ScheduledFuture rampFuture = null - LoadRunnerServiceInfo(String serviceName, int targetThreads, int runDelayMs, int rampDelayMs) { - this.serviceName = serviceName + LoadRunnerServiceInfo(String serviceName, String parametersExpr, int targetThreads, + int runDelayMs, int rampDelayMs, int timeBinLength, int timeBinsKeep) { + this.serviceName = serviceName; this.parametersExpr = parametersExpr this.targetThreads = targetThreads - this.runDelayMs = runDelayMs - this.rampDelayMs = rampDelayMs + this.runDelayMs = runDelayMs; this.rampDelayMs = rampDelayMs + this.timeBinLength = timeBinLength; this.timeBinsKeep = timeBinsKeep } + + void countRun(LoadRunner loadRunner, long startTime, long endTime, ArtifactTypeStats stats) { + long runTime = endTime - startTime + // logger.info("count run ${serviceName} ${runTime} ${Thread.currentThread().name}") + LoadRunnerServiceStats curBin = null + + // find the current time bin in a semaphore locked section, the rest is increments and can be run multithreaded + loadRunner.mutateLock.lock() + // logger.info("count run after lock ${serviceName} ${runTime} ${Thread.currentThread().name}") + try { + if (beginTime == 0) beginTime = startTime + + curBin = timeBinList.isEmpty() ? null : timeBinList.getLast() + // create and add a new bin if there are none or if this hit is after the bin's end time (need to advance the bin) + if (curBin == null || curBin.beginTime + timeBinLength < startTime) { + curBin = new LoadRunnerServiceStats() + curBin.beginTime = startTime + timeBinList.add(curBin) + if (timeBinList.size() > timeBinsKeep) { + LoadRunnerServiceStats removeBin = timeBinList.removeFirst() + // logger.info("Removed time bin starting ${new Timestamp(removeBin.beginTime)} count ${removeBin.runCount}") + } + } + + // some exceptions, these are multiple operations and need to be in the locked section to be accurate, maybe don't need to be... + if (runTime < this.minTime) this.minTime = runTime + if (runTime > this.maxTime) this.maxTime = runTime + if (runTime < curBin.minTime) curBin.minTime = runTime + if (runTime > curBin.maxTime) curBin.maxTime = runTime + } finally { + loadRunner.mutateLock.unlock() + // logger.info("count run after unlock ${serviceName} ${runTime} ${Thread.currentThread().name}") + // loadRunner.logFutures() + } + + // for all runs + this.runCount++ + this.lastRunTime = endTime + this.totalTime += runTime + this.totalSquaredTime += runTime * runTime + this.artifactTypeStats.add(stats) + + // same thing for just this bin + curBin.runCount++ + curBin.lastRunTime = endTime + curBin.totalTime += runTime + curBin.totalSquaredTime += runTime * runTime + curBin.artifactTypeStats.add(stats) + } + void addThread(LoadRunner loadRunner) { - LoadRunnerServiceRunnable runnable = new LoadRunnerServiceRunnable(serviceName, loadRunner, this) + LoadRunnerServiceRunnable runnable = new LoadRunnerServiceRunnable(serviceName, parametersExpr, loadRunner) // NOTE: use scheduleWithFixedDelay so delay is wait between terminate of one and start of another, better for both short and long running test runs - loadRunner.scheduledExecutor.scheduleWithFixedDelay(runnable, 1, runDelayMs, TimeUnit.MILLISECONDS) + ScheduledFuture future = loadRunner.scheduledExecutor.scheduleWithFixedDelay(runnable, 1, runDelayMs, TimeUnit.MILLISECONDS) + // logger.info("Added run thread runDelayMs ${runDelayMs} ${runDelayMs?.class?.name} done ${future.done} ${future.toString()}") + runFutures.add(future) } void addRampThread(LoadRunner loadRunner) { + if (rampFuture != null && !rampFuture) beginTime = System.currentTimeMillis() LoadRunnerRamperRunnable runnable = new LoadRunnerRamperRunnable(loadRunner, this) // NOTE: use scheduleAtFixedRate so one is added each delay period regardless of how long it takes (generally not long) - loadRunner.scheduledExecutor.scheduleAtFixedRate(runnable, 1, rampDelayMs, TimeUnit.MILLISECONDS) + rampFuture = loadRunner.scheduledExecutor.scheduleAtFixedRate(runnable, 1, rampDelayMs, TimeUnit.MILLISECONDS) } void resetStats() { lastRunTime = 0; beginTime = 0; totalTime = 0; totalSquaredTime = 0; minTime = Long.MAX_VALUE; maxTime = 0 runCount = 0; errorCount = 0 artifactTypeStats = new ArtifactTypeStats() lastResult = null + timeBinList = new ConcurrentLinkedDeque<>() } } static class LoadRunnerRamperRunnable implements Runnable { @@ -738,6 +829,12 @@ class ServiceFacadeImpl implements ServiceFacade { // TODO add delayed ramp-down, useful for some performance behavior patterns but usually redundant with delayed ramp up to look for elbows in the response time over time } } + static class LoadRunnerThreadFactory implements ThreadFactory { + private final ThreadGroup workerGroup = new ThreadGroup("LoadRunner") + private final AtomicInteger threadNumber = new AtomicInteger(1) + Thread newThread(Runnable r) { return new Thread(workerGroup, r, "LoadRunner-" + threadNumber.getAndIncrement()) } + } + static class LoadRunner { ExecutionContextFactoryImpl ecfi CustomScheduledExecutor scheduledExecutor = null @@ -750,17 +847,22 @@ class ServiceFacadeImpl implements ServiceFacade { this.ecfi = ecfi } - void setServiceInfo(String serviceName, String parametersExpr, int targetThreads, int runDelayMs, int rampDelayMs) { + LoadRunnerServiceInfo getServiceInfo(String serviceName, String parametersExpr) { + for (int i = 0; i < serviceInfos.size(); i++) { + LoadRunnerServiceInfo curInfo = (LoadRunnerServiceInfo) serviceInfos.get(i) + if (curInfo.serviceName == serviceName && curInfo.parametersExpr == parametersExpr) + return curInfo + } + return null + } + void setServiceInfo(String serviceName, String parametersExpr, int targetThreads, int runDelayMs, + int rampDelayMs, int timeBinLength, int timeBinsKeep) { mutateLock.lock() try { - LoadRunnerServiceInfo serviceInfo = null - for (int i = 0; i < serviceInfos.size(); i++) { - LoadRunnerServiceInfo curInfo = (LoadRunnerServiceInfo) serviceInfos.get(i) - if (curInfo.serviceName == serviceName) serviceInfo = curInfo - } + LoadRunnerServiceInfo serviceInfo = getServiceInfo(serviceName, parametersExpr) if (serviceInfo == null) { - serviceInfo = new LoadRunnerServiceInfo(serviceName, targetThreads, runDelayMs, rampDelayMs) - serviceInfo.parametersExpr = parametersExpr + serviceInfo = new LoadRunnerServiceInfo(serviceName, parametersExpr, targetThreads, + runDelayMs, rampDelayMs, timeBinLength, timeBinsKeep) serviceInfos.add(serviceInfo) @@ -769,10 +871,11 @@ class ServiceFacadeImpl implements ServiceFacade { serviceInfo.addRampThread(this) } } else { - serviceInfo.parametersExpr = parametersExpr serviceInfo.targetThreads = targetThreads serviceInfo.runDelayMs = runDelayMs serviceInfo.rampDelayMs = rampDelayMs + serviceInfo.timeBinLength = timeBinLength + serviceInfo.timeBinsKeep = timeBinsKeep } } finally { mutateLock.unlock() @@ -793,14 +896,12 @@ class ServiceFacadeImpl implements ServiceFacade { curInfo.resetStats() } - scheduledExecutor = new CustomScheduledExecutor(corePoolSize) + scheduledExecutor = new CustomScheduledExecutor(corePoolSize, new LoadRunnerThreadFactory()) scheduledExecutor.setMaximumPoolSize(maxPoolSize) for (int i = 0; i < serviceInfos.size(); i++) { LoadRunnerServiceInfo curInfo = (LoadRunnerServiceInfo) serviceInfos.get(i) - // do this once here - curInfo.addThread(this) - // add a schedule for the rest + // get the ramp thread started curInfo.addRampThread(this) } } @@ -838,5 +939,16 @@ class ServiceFacadeImpl implements ServiceFacade { mutateLock.unlock() } } + + void logFutures() { + for (int si = 0; si < serviceInfos.size(); si++) { + LoadRunnerServiceInfo serviceInfo = serviceInfos.get(si) + logger.info("LoadRunner RAMP Future done ${serviceInfo.rampFuture?.done} canceled ${serviceInfo.rampFuture?.cancelled} ${serviceInfo.rampFuture?.toString()}") + for (int i = 0; i < serviceInfo.runFutures.size(); i++) { + ScheduledFuture future = serviceInfo.runFutures.get(i) + logger.info("LoadRunner RUN Future done ${future.done} canceled ${future.cancelled} ${future.toString()}") + } + } + } } } diff --git a/framework/src/main/java/org/moqui/util/ObjectUtilities.java b/framework/src/main/java/org/moqui/util/ObjectUtilities.java index f2b794603..16a9c2754 100644 --- a/framework/src/main/java/org/moqui/util/ObjectUtilities.java +++ b/framework/src/main/java/org/moqui/util/ObjectUtilities.java @@ -76,6 +76,7 @@ public static Map objectToMap(Object bean) { } } + /* commenting for now, seems to call a bunch of undesired methods, will need some work to filter them out: try { BeanInfo beanInfo = Introspector.getBeanInfo(clazz); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); @@ -93,6 +94,7 @@ public static Map objectToMap(Object bean) { } catch (IntrospectionException e) { // nothing again } + */ // this gets picked up automatically, just remove at the end, faster than checking along the way map.remove("class"); From d850f11536ff7dcf1fd68b7716c39576a0d658b5 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Sun, 5 Feb 2023 17:27:46 -0800 Subject: [PATCH 06/61] In Service LoadRunner add small random delay support, use available threads * 4 for max load pool size; add permission for SERVICE_LOAD_RUNNER for screens --- framework/data/SecurityTypeData.xml | 2 ++ .../impl/service/ServiceFacadeImpl.groovy | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/framework/data/SecurityTypeData.xml b/framework/data/SecurityTypeData.xml index 0906d9648..33544305b 100644 --- a/framework/data/SecurityTypeData.xml +++ b/framework/data/SecurityTypeData.xml @@ -54,4 +54,6 @@ along with this software (see the LICENSE.md file). If not, see + + diff --git a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy index b6c5fb52f..da7bf9357 100644 --- a/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/service/ServiceFacadeImpl.groovy @@ -691,6 +691,10 @@ class ServiceFacadeImpl implements ServiceFacade { } } + // before starting, and tracking the startTime, do a small random delay for variation in run times + if (serviceInfo.runDelayVaryMs != 0) + Thread.sleep(ThreadLocalRandom.current().nextInt(serviceInfo.runDelayVaryMs)) + long startTime = System.currentTimeMillis() ExecutionContextImpl threadEci = ecfi.getEci() try { @@ -699,7 +703,8 @@ class ServiceFacadeImpl implements ServiceFacade { // run the service try { - serviceInfo.lastResult = threadEci.serviceFacade.sync().name(serviceName).parameters(parameters).disableAuthz().call() + serviceInfo.lastResult = threadEci.serviceFacade.sync().name(serviceName) + .parameters(parameters).disableAuthz().call() } catch (Throwable t) { // logged elsewhere, just count and swallow serviceInfo.errorCount++ @@ -726,7 +731,7 @@ class ServiceFacadeImpl implements ServiceFacade { } static class LoadRunnerServiceInfo extends LoadRunnerServiceStats { String serviceName, parametersExpr - int targetThreads, runDelayMs, rampDelayMs, timeBinLength, timeBinsKeep + int targetThreads, runDelayMs, runDelayVaryMs, rampDelayMs, timeBinLength, timeBinsKeep AtomicInteger currentThreads = new AtomicInteger(0) Map lastResult = null @@ -735,10 +740,10 @@ class ServiceFacadeImpl implements ServiceFacade { ScheduledFuture rampFuture = null LoadRunnerServiceInfo(String serviceName, String parametersExpr, int targetThreads, - int runDelayMs, int rampDelayMs, int timeBinLength, int timeBinsKeep) { + int runDelayMs, int runDelayVaryMs, int rampDelayMs, int timeBinLength, int timeBinsKeep) { this.serviceName = serviceName; this.parametersExpr = parametersExpr this.targetThreads = targetThreads - this.runDelayMs = runDelayMs; this.rampDelayMs = rampDelayMs + this.runDelayMs = runDelayMs; this.runDelayVaryMs = runDelayVaryMs; this.rampDelayMs = rampDelayMs this.timeBinLength = timeBinLength; this.timeBinsKeep = timeBinsKeep } @@ -839,7 +844,7 @@ class ServiceFacadeImpl implements ServiceFacade { ExecutionContextFactoryImpl ecfi CustomScheduledExecutor scheduledExecutor = null ArrayList serviceInfos = new ArrayList<>() - int corePoolSize = 4, maxPoolSize = 8 + Integer corePoolSize = 4, maxPoolSize = null AtomicInteger execIndex = new AtomicInteger(1) ReentrantLock mutateLock = new ReentrantLock() @@ -856,13 +861,13 @@ class ServiceFacadeImpl implements ServiceFacade { return null } void setServiceInfo(String serviceName, String parametersExpr, int targetThreads, int runDelayMs, - int rampDelayMs, int timeBinLength, int timeBinsKeep) { + int runDelayVaryMs, int rampDelayMs, int timeBinLength, int timeBinsKeep) { mutateLock.lock() try { LoadRunnerServiceInfo serviceInfo = getServiceInfo(serviceName, parametersExpr) if (serviceInfo == null) { serviceInfo = new LoadRunnerServiceInfo(serviceName, parametersExpr, targetThreads, - runDelayMs, rampDelayMs, timeBinLength, timeBinsKeep) + runDelayMs, runDelayVaryMs, rampDelayMs, timeBinLength, timeBinsKeep) serviceInfos.add(serviceInfo) @@ -884,7 +889,6 @@ class ServiceFacadeImpl implements ServiceFacade { void begin() { mutateLock.lock() try { - // TODO set maxPoolSize to CPU count x2 or something if needed to scale the load runner itself; probably not much as services running on same system... if (scheduledExecutor == null) { // restart index execIndex = new AtomicInteger(1) @@ -897,6 +901,7 @@ class ServiceFacadeImpl implements ServiceFacade { } scheduledExecutor = new CustomScheduledExecutor(corePoolSize, new LoadRunnerThreadFactory()) + if (maxPoolSize == null) maxPoolSize = Runtime.getRuntime().availableProcessors() * 4 scheduledExecutor.setMaximumPoolSize(maxPoolSize) for (int i = 0; i < serviceInfos.size(); i++) { From 3775dbe2c65c6633ec9b457c8fb57ccbcd55ae5e Mon Sep 17 00:00:00 2001 From: David E Jones Date: Mon, 6 Feb 2023 15:19:16 -0800 Subject: [PATCH 07/61] In build.gradle also delete the SaveOpenSearch.zip file in cleanLoadSave, also called by cleanAll, was missing in the prior OpenSearch changes --- build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1ab9e682b..3b591d4c6 100644 --- a/build.gradle +++ b/build.gradle @@ -75,8 +75,9 @@ task cleanDb { doLast { } } task cleanLog(type: Delete) { delete fileTree(dir: moquiRuntime+'/log', include: '*') } task cleanSessions(type: Delete) { delete fileTree(dir: moquiRuntime+'/sessions', include: '*') } -task cleanLoadSave(type: Delete) { delete file('SaveH2.zip'); delete file('SaveDEFAULT.zip'); - delete file('SaveTransactional.zip'); delete file('SaveAnalytical.zip'); delete file('SaveOrientDb.zip'); delete file('SaveElasticSearch.zip') } +task cleanLoadSave(type: Delete) { delete file('SaveH2.zip'); delete file('SaveDEFAULT.zip') + delete file('SaveTransactional.zip'); delete file('SaveAnalytical.zip'); delete file('SaveOrientDb.zip') + delete file('SaveElasticSearch.zip'); delete file('SaveOpenSearch.zip') } task cleanPlusRuntime(type: Delete) { delete file(plusRuntimeName) } task cleanOther(type: Delete) { delete fileTree(dir: '.', includes: ['**/.nbattrs', '**/*~', '**/.#*', '**/.DS_Store', '**/*.rej', '**/*.orig']) } From 74f493525d5d34800e0530a9b7681b1d516dccb2 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Mon, 6 Feb 2023 15:22:16 -0800 Subject: [PATCH 08/61] Fix PIT testing against ElasticSearch 7.10.2, the last open source version; the docs are horrible with 3 differences between what actually ended working from a bunch of random experimentation; PIT support is necessary for the Elastic Entity implementation (alternative to a DB cursor); this ended up being needed because more recent OpenSearch that supports PIT also uses more memory than ElasticSearch, enough that the anemic demo.moqui.org server with 2GB RAM can't run Moqui + OpenSearch 2.4 --- .../org/moqui/impl/context/ElasticFacadeImpl.groovy | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy index 2d30bdd1d..d9971b5ef 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy @@ -493,8 +493,14 @@ class ElasticFacadeImpl implements ElasticFacade { // requires 2.4.0 or later response = makeRestClient(Method.POST, index, "_search/point_in_time", [keep_alive:keepAlive]).call() } else { - // see: https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#scroll-search-results - response = makeRestClient(Method.POST, index, "_pit", [keep_alive:keepAlive]).call() + // see: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/paginate-search-results.html#scroll-search-results + // whatever the docs say: + // - it doesn't work with the keep_alive parameter at all "contains unrecognized parameter: [keep_alive]" + // - does not work with no body "request body is required" + // - and it doesn't work without the doc type _doc before _pit in the path "mapping type name [_pit] can't start with '_' unless it is called [_doc]" + // in other words, the docs are completely wrong for ES 7.10.2 + // response = makeRestClient(Method.POST, index, "_pit", [keep_alive:keepAlive]).call() + response = makeRestClient(Method.POST, index, "_doc/_pit", null).text(objectToJson([keep_alive:keepAlive])).call() } // System.out.println("Get PIT Response: ${response.statusCode} ${response.reasonPhrase}\n${response.text()}") checkResponse(response, "PIT", index) @@ -510,7 +516,7 @@ class ElasticFacadeImpl implements ElasticFacade { response = makeRestClient(Method.DELETE, null, "_search/point_in_time", null) .text(objectToJson([pit_id:[pitId]])).call() } else { - // see: https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#scroll-search-results + // see: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/paginate-search-results.html#scroll-search-results response = makeRestClient(Method.DELETE, null, "_pit", null).text(objectToJson([id:pitId])).call() } // System.out.println("Delete PIT Response: ${response.statusCode} ${response.reasonPhrase}\n${response.text()}") From 1947553c759110693759e26fe90a82aa66e17a41 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Mon, 6 Feb 2023 15:53:47 -0800 Subject: [PATCH 09/61] Add new moqui-demo component repo to addons.xml --- addons.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons.xml b/addons.xml index e5a926a14..8e7e0e3de 100644 --- a/addons.xml +++ b/addons.xml @@ -77,7 +77,9 @@ + + From f8a51c26e70d3a417f4817f09e2d331117d58532 Mon Sep 17 00:00:00 2001 From: Acetousk Date: Wed, 15 Feb 2023 11:24:00 -0700 Subject: [PATCH 10/61] Add getScreenPathHasTransition in ScreenRenderImpl --- .../org/moqui/impl/screen/ScreenRenderImpl.groovy | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy index f3e8b8ee6..183c22cfd 100644 --- a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy @@ -1105,6 +1105,19 @@ class ScreenRenderImpl implements ScreenRender { return activePath } + // TODO: This may not be the actual place we decided on, but due to lost work this is my best guess + // Get the first screen path of the parent screens with a transition specified of the currently rendered screen + String getScreenPathHasTransition(String transitionName) { + int screenPathDefListSize = screenUrlInfo.screenPathDefList.size() + for (int i = 0; i < screenPathDefListSize; i++) { + ScreenDefinition screenDef = (ScreenDefinition) screenUrlInfo.screenPathDefList.get(i) + if (screenDef.hasTransition(transitionName)) { + return '/' + screenUrlInfo.fullPathNameList.subList(0,i).join('/') + (i == 0 ? '' : '/') + } + } + return null + } + String renderSubscreen() { // first see if there is another screen def in the list if (!getActiveScreenHasNext()) { From 47f00153b57585a08648be321a4f82291c529bf9 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Sun, 19 Feb 2023 13:50:48 -0800 Subject: [PATCH 11/61] In ElasticDatasourceFactory add better exceptions than NPE when no ElasticClient is found --- .../impl/entity/elastic/ElasticDatasourceFactory.groovy | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticDatasourceFactory.groovy b/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticDatasourceFactory.groovy index 4567832c8..a532332c0 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticDatasourceFactory.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/elastic/ElasticDatasourceFactory.groovy @@ -161,6 +161,7 @@ class ElasticDatasourceFactory implements EntityDatasourceFactory { if (checkedEntityIndexSet.contains(indexName)) return ElasticFacade.ElasticClient elasticClient = efi.ecfi.elasticFacade.getClient(clusterName) + if (elasticClient == null) throw new IllegalStateException("No ElasticClient found for cluster name " + clusterName) if (!elasticClient.indexExists(indexName)) { Map mapping = makeElasticEntityMapping(ed) // logger.warn("Creating ES Index ${indexName} with mapping: ${JsonOutput.prettyPrint(JsonOutput.toJson(mapping))}") @@ -170,7 +171,11 @@ class ElasticDatasourceFactory implements EntityDatasourceFactory { checkedEntityIndexSet.add(indexName) } - ElasticFacade.ElasticClient getElasticClient() { efi.ecfi.elasticFacade.getClient(clusterName) } + ElasticFacade.ElasticClient getElasticClient() { + ElasticFacade.ElasticClient client = efi.ecfi.elasticFacade.getClient(clusterName) + if (client == null) throw new IllegalStateException("No ElasticClient found for cluster name " + clusterName) + return client + } String getIndexName(EntityDefinition ed) { return indexPrefix + ed.getTableNameLowerCase() } From 1e753417ae5e8c37a2284925f04beb422de6c358 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Sun, 19 Feb 2023 21:33:44 -0800 Subject: [PATCH 12/61] Increase default worker-pool-core to 16 threads, and max to 32 or CPU * 3 threads (instead of *2); increasing after tests and production results showing typical low CPU time per thread (much of it is deferred database or search with lots of I/O wait); also improve logging when waiting for worker pool to empty; note that this also improves automated test performance because many of the tests get to hundreds of async services queued up and the ThreadPoolExecutor seems to add threads slowly even if the pool is fully utilized --- .../moqui/impl/context/ExecutionContextFactoryImpl.groovy | 8 ++++---- framework/src/main/resources/MoquiDefaultConf.xml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy index f42d6f8f2..e6c93a26f 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy @@ -452,10 +452,10 @@ class ExecutionContextFactoryImpl implements ExecutionContextFactory { BlockingQueue workQueue = new LinkedBlockingQueue<>(workerQueueSize) int coreSize = (toolsNode.attribute("worker-pool-core") ?: "16") as int - int maxSize = (toolsNode.attribute("worker-pool-max") ?: "24") as int - int availableProcessorsSize = Runtime.getRuntime().availableProcessors() * 2 + int maxSize = (toolsNode.attribute("worker-pool-max") ?: "32") as int + int availableProcessorsSize = Runtime.getRuntime().availableProcessors() * 3 if (availableProcessorsSize > maxSize) { - logger.info("Setting worker pool size to ${availableProcessorsSize} based on available processors * 2") + logger.info("Setting worker pool size to ${availableProcessorsSize} based on available processors * 3") maxSize = availableProcessorsSize } long aliveTime = (toolsNode.attribute("worker-pool-alive") ?: "60") as long @@ -467,9 +467,9 @@ class ExecutionContextFactoryImpl implements ExecutionContextFactory { boolean waitWorkerPoolEmpty(int retryLimit) { ThreadPoolExecutor jobWorkerPool = serviceFacade.jobWorkerPool int count = 0 - logger.warn("Wait for workerPool and jobWorkerPool empty: worker queue size ${workerPool.getQueue().size()} active ${workerPool.getActiveCount()}; service job queue size ${jobWorkerPool.getQueue().size()} active ${jobWorkerPool.getActiveCount()}") while (count < retryLimit && (workerPool.getQueue().size() > 0 || workerPool.getActiveCount() > 0 || jobWorkerPool.getQueue().size() > 0 || jobWorkerPool.getActiveCount() > 0)) { + if (count % 10 == 0) logger.warn("Wait for workerPool and jobWorkerPool empty: worker queue size ${workerPool.getQueue().size()} active ${workerPool.getActiveCount()} max threads ${workerPool.getMaximumPoolSize()}; service job queue size ${jobWorkerPool.getQueue().size()} active ${jobWorkerPool.getActiveCount()}") Thread.sleep(100) count++ } diff --git a/framework/src/main/resources/MoquiDefaultConf.xml b/framework/src/main/resources/MoquiDefaultConf.xml index d2ad0edee..e44aeb3f3 100644 --- a/framework/src/main/resources/MoquiDefaultConf.xml +++ b/framework/src/main/resources/MoquiDefaultConf.xml @@ -81,7 +81,7 @@ - + From c994d1aedc40c7bfc516a9052b0028d0ba6a34e8 Mon Sep 17 00:00:00 2001 From: aabiabdallah Date: Tue, 21 Feb 2023 17:35:01 -0600 Subject: [PATCH 13/61] In ScreenRenderImpl remove token created requirement when checking for session token --- .../main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy index f3e8b8ee6..69ad1ce71 100644 --- a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy @@ -425,7 +425,6 @@ class ScreenRenderImpl implements ScreenRender { // require a moquiSessionToken parameter for all but get if (request.getMethod().toLowerCase() != "get" && webappInfo != null && webappInfo.requireSessionToken && targetTransition.getRequireSessionToken() && - !"true".equals(request.getAttribute("moqui.session.token.created")) && !"true".equals(request.getAttribute("moqui.request.authenticated"))) { String passedToken = (String) ec.web.getParameters().get("moquiSessionToken") if (!passedToken) passedToken = request.getHeader("moquiSessionToken") ?: From cc4d9efae74a6301a33a27d03b27a1180bd0a5a8 Mon Sep 17 00:00:00 2001 From: "David E. Jones" Date: Tue, 21 Feb 2023 17:25:14 -0800 Subject: [PATCH 14/61] Revert "Update to session token condition" --- .../main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy index 69ad1ce71..f3e8b8ee6 100644 --- a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy @@ -425,6 +425,7 @@ class ScreenRenderImpl implements ScreenRender { // require a moquiSessionToken parameter for all but get if (request.getMethod().toLowerCase() != "get" && webappInfo != null && webappInfo.requireSessionToken && targetTransition.getRequireSessionToken() && + !"true".equals(request.getAttribute("moqui.session.token.created")) && !"true".equals(request.getAttribute("moqui.request.authenticated"))) { String passedToken = (String) ec.web.getParameters().get("moquiSessionToken") if (!passedToken) passedToken = request.getHeader("moquiSessionToken") ?: From a6f5ee5e7b689f79effb3a9fd005c63dd5ba0bf6 Mon Sep 17 00:00:00 2001 From: Wei Zhang Date: Thu, 23 Feb 2023 07:03:27 +0800 Subject: [PATCH 15/61] Fixed a runtime error if Currency is BTC (#555) * Fixed the problem that moqui cannot be deployed as non-root webapp in Tomcat * Fixed a runtime error if Currency is BTC * Fixed a runtime error if Currency is BTC * Fixed the retries of Elastic Client --- AUTHORS | 2 ++ .../org/moqui/impl/context/ElasticFacadeImpl.groovy | 2 +- .../groovy/org/moqui/impl/context/L10nFacadeImpl.java | 10 +++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 566d88618..27dbd1a34 100644 --- a/AUTHORS +++ b/AUTHORS @@ -60,6 +60,7 @@ Written in 2020 by Jacob Barnes - Tellan Written in 2020 by Amir Anjomshoaa - amiranjom Written in 2021 by Deepak Dixit - dixitdeepak Written in 2021 by Taher Alkhateeb - pythys +Written in 2022 by Zhang Wei - hellozhangwei =========================================================================== @@ -106,5 +107,6 @@ Written in 2020 by Jacob Barnes - Tellan Written in 2020 by Amir Anjomshoaa - amiranjom Written in 2021 by Deepak Dixit - dixitdeepak Written in 2021 by Taher Alkhateeb - pythys +Written in 2022 by Zhang Wei - hellozhangwei =========================================================================== diff --git a/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy index d9971b5ef..3155ec965 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ElasticFacadeImpl.groovy @@ -196,7 +196,7 @@ class ElasticFacadeImpl implements ElasticFacade { requestFactory.init() // try connecting and get server info - int retries = clusterHost == 'localhost' && !"true".equals(System.getProperty("moqui.elasticsearch.started")) ? 1 : 20 + int retries = ((clusterHost == 'localhost' || clusterHost == '127.0.0.1') && !"true".equals(System.getProperty("moqui.elasticsearch.started"))) ? 1 : 20 for (int i = 1; i <= retries; i++) { try { serverInfo = getServerInfo() diff --git a/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java b/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java index 65b4511b9..acec7f13f 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java +++ b/framework/src/main/groovy/org/moqui/impl/context/L10nFacadeImpl.java @@ -116,7 +116,15 @@ public String formatCurrency(Object amount, String uomId, Integer fractionDigits } } - Currency currency = uomId != null && uomId.length() > 0 ? Currency.getInstance(uomId) : null; + Currency currency = null; + if (uomId != null && uomId.length() > 0) { + try { + currency = Currency.getInstance(uomId); + } catch (Exception e) { + if (logger.isTraceEnabled()) logger.trace("Ignoring IllegalArgumentException for Currency parse: " + e.toString()); + } + } + if (locale == null) locale = getLocale(); if (currency != null) { NumberFormat nf = NumberFormat.getCurrencyInstance(locale); From 4666aa6c0f53a7cc682663690de87d9c792b0d5f Mon Sep 17 00:00:00 2001 From: Jens Hardings Date: Wed, 22 Feb 2023 20:22:34 -0300 Subject: [PATCH 16/61] Anonymous usage of screens with transitions (#541) * fix: consider transitions in screens or parent screens defined with require-authentication in "anonymous-view" or "anonymous-all" as permitted * fix: consider transitions defined with authenticate in "anonymous-view" or "anonymous-all" as permitted * fix: consider any of service.@authenticate and screen.@require-authentication to determine permission on transition * fix: considering verb-based execution actions of single-service when determining transition permission, correctly determining whether view or all allowed by screen --- .../moqui/impl/screen/ScreenUrlInfo.groovy | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy b/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy index 3ce33c96e..ada1939f4 100644 --- a/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy +++ b/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy @@ -271,6 +271,9 @@ class ScreenUrlInfo { ArrayDeque artifactExecutionInfoStack = new ArrayDeque() int screenPathDefListSize = screenPathDefList.size() + boolean allowedByScreenDefinitionView = false + boolean allowedByScreenDefinitionAll = false + boolean allowedByScreenDefinition = false for (int i = 0; i < screenPathDefListSize; i++) { AuthzAction curActionEnum = (i == (screenPathDefListSize - 1)) ? actionEnum : ArtifactExecutionInfo.AUTHZA_VIEW ScreenDefinition screenDef = (ScreenDefinition) screenPathDefList.get(i) @@ -285,9 +288,15 @@ class ScreenUrlInfo { MNode screenNode = screenDef.getScreenNode() String requireAuthentication = screenNode.attribute('require-authentication') + allowedByScreenDefinitionView = "anonymous-view".equals(requireAuthentication) + allowedByScreenDefinitionAll = "anonymous-all".equals(requireAuthentication) + if (actionEnum == ArtifactExecutionInfo.AUTHZA_VIEW) { + allowedByScreenDefinition = allowedByScreenDefinition || allowedByScreenDefinitionView || allowedByScreenDefinitionAll + } else if (actionEnum == ArtifactExecutionInfo.AUTHZA_ALL) + allowedByScreenDefinition = allowedByScreenDefinition || allowedByScreenDefinitionAll if (!aefi.isPermitted(aeii, lastAeii, isLast ? (!requireAuthentication || "true".equals(requireAuthentication)) : false, false, false, artifactExecutionInfoStack)) { - // logger.warn("TOREMOVE user ${username} is NOT allowed to view screen at path ${this.fullPathNameList} because of screen at ${screenDef.location}") + //logger.warn("TOREMOVE user ${userId} is NOT allowed to view screen at path ${this.fullPathNameList} because of screen at ${screenDef.location}") if (permittedCacheKey != null) aefi.screenPermittedCache.put(permittedCacheKey, false) return false } @@ -296,7 +305,7 @@ class ScreenUrlInfo { } // see if the transition is permitted - if (transitionItem != null) { + if (!allowedByScreenDefinition && transitionItem != null) { ScreenDefinition lastScreenDef = (ScreenDefinition) screenPathDefList.get(screenPathDefList.size() - 1) ArtifactExecutionInfoImpl aeii = new ArtifactExecutionInfoImpl("${lastScreenDef.location}/${transitionItem.name}", ArtifactExecutionInfo.AT_XML_SCREEN_TRANS, ArtifactExecutionInfo.AUTHZA_VIEW, null) @@ -317,10 +326,15 @@ class ScreenUrlInfo { if (authzAction == null) authzAction = ServiceDefinition.verbAuthzActionEnumMap.get(ServiceDefinition.getVerbFromName(serviceName)) if (authzAction == null) authzAction = ArtifactExecutionInfo.AUTHZA_ALL + boolean allowedByServiceDefinition = false + if (authzAction == ArtifactExecutionInfo.AUTHZA_VIEW) { + allowedByServiceDefinition = allowedByScreenDefinitionView || "anonymous-view".equals(sd.authenticate) || "anonymous-all".equals(sd.authenticate) + } else if (authzAction in [ArtifactExecutionInfo.AUTHZA_ALL, ArtifactExecutionInfo.AUTHZA_CREATE, ArtifactExecutionInfo.AUTHZA_UPDATE, ArtifactExecutionInfo.AUTHZA_DELETE]) + allowedByServiceDefinition = allowedByScreenDefinitionAll || "anonymous-all".equals(sd.authenticate) ArtifactExecutionInfoImpl aeii = new ArtifactExecutionInfoImpl(serviceName, ArtifactExecutionInfo.AT_SERVICE, authzAction, null) ArtifactExecutionInfoImpl lastAeii = (ArtifactExecutionInfoImpl) artifactExecutionInfoStack.peekFirst() - if (!aefi.isPermitted(aeii, lastAeii, true, false, false, null)) { + if (!aefi.isPermitted(aeii, lastAeii, !allowedByServiceDefinition, false, false, null)) { // logger.warn("TOREMOVE user ${username} is NOT allowed to run transition at path ${this.fullPathNameList} because of screen at ${screenDef.location}") if (permittedCacheKey != null) aefi.screenPermittedCache.put(permittedCacheKey, false) return false From 080a2323c4fe59e78c8863190fd72ed20ea17b4f Mon Sep 17 00:00:00 2001 From: David E Jones Date: Thu, 23 Feb 2023 02:25:10 -0800 Subject: [PATCH 17/61] Small fix for new authz changes for anonymous-view/-all, handle no screen definition instead of NPE --- .../main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy b/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy index ada1939f4..ec31ef405 100644 --- a/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy +++ b/framework/src/main/groovy/org/moqui/impl/screen/ScreenUrlInfo.groovy @@ -328,9 +328,10 @@ class ScreenUrlInfo { boolean allowedByServiceDefinition = false if (authzAction == ArtifactExecutionInfo.AUTHZA_VIEW) { - allowedByServiceDefinition = allowedByScreenDefinitionView || "anonymous-view".equals(sd.authenticate) || "anonymous-all".equals(sd.authenticate) - } else if (authzAction in [ArtifactExecutionInfo.AUTHZA_ALL, ArtifactExecutionInfo.AUTHZA_CREATE, ArtifactExecutionInfo.AUTHZA_UPDATE, ArtifactExecutionInfo.AUTHZA_DELETE]) - allowedByServiceDefinition = allowedByScreenDefinitionAll || "anonymous-all".equals(sd.authenticate) + allowedByServiceDefinition = allowedByScreenDefinitionView || (sd != null && ("anonymous-view".equals(sd.authenticate) || "anonymous-all".equals(sd.authenticate))) + } else if (authzAction in [ArtifactExecutionInfo.AUTHZA_ALL, ArtifactExecutionInfo.AUTHZA_CREATE, ArtifactExecutionInfo.AUTHZA_UPDATE, ArtifactExecutionInfo.AUTHZA_DELETE]) { + allowedByServiceDefinition = allowedByScreenDefinitionAll || (sd != null && "anonymous-all".equals(sd.authenticate)) + } ArtifactExecutionInfoImpl aeii = new ArtifactExecutionInfoImpl(serviceName, ArtifactExecutionInfo.AT_SERVICE, authzAction, null) ArtifactExecutionInfoImpl lastAeii = (ArtifactExecutionInfoImpl) artifactExecutionInfoStack.peekFirst() From 9fc07786d3cdb8fb29101b02fc40098e9b4958bc Mon Sep 17 00:00:00 2001 From: Jens Hardings Date: Thu, 23 Feb 2023 11:30:39 -0300 Subject: [PATCH 18/61] Handle usernames with different casing in session data --- .../groovy/org/moqui/impl/context/UserFacadeImpl.groovy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy index 0a37ad7aa..abd9f7b23 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy @@ -113,8 +113,10 @@ class UserFacadeImpl implements UserFacade { // user found in session so no login needed, but make sure hasLoggedOut != "Y" EntityValue userAccount = (EntityValue) null if (sesUsername != null && !sesUsername.isEmpty()) { + EntityCondition usernameCond = eci.entityFacade.getConditionFactory() + .makeCondition("username", EntityCondition.ComparisonOperator.EQUALS, username).ignoreCase() userAccount = eci.getEntity().find("moqui.security.UserAccount") - .condition("username", sesUsername).useCache(false).disableAuthz().one() + .condition(usernameCond).useCache(false).disableAuthz().one() } if (userAccount != null && "Y".equals(userAccount.getNoCheckSimple("hasLoggedOut"))) { @@ -1051,8 +1053,10 @@ class UserFacadeImpl implements UserFacade { EntityValueBase ua = (EntityValueBase) null if (username != null && username.length() > 0) { + EntityCondition usernameCond = ufi.eci.entityFacade.getConditionFactory() + .makeCondition("username", EntityCondition.ComparisonOperator.EQUALS, username).ignoreCase() ua = (EntityValueBase) ufi.eci.getEntity().find("moqui.security.UserAccount") - .condition("username", username).useCache(false).disableAuthz().one() + .condition(usernameCond).useCache(false).disableAuthz().one() } if (ua != null) { userAccount = ua From 64d92247d79dd03d3d303fa721343d475b41bc02 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 7 Mar 2023 12:55:31 -0600 Subject: [PATCH 19/61] Add common java includes to the xml actions ftl file --- framework/template/XmlActions.groovy.ftl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/template/XmlActions.groovy.ftl b/framework/template/XmlActions.groovy.ftl index 66a235934..39236c122 100644 --- a/framework/template/XmlActions.groovy.ftl +++ b/framework/template/XmlActions.groovy.ftl @@ -15,6 +15,8 @@ import static org.moqui.util.ObjectUtilities.* import static org.moqui.util.CollectionUtilities.* import static org.moqui.util.StringUtilities.* import java.sql.Timestamp +import java.sql.Time +import java.time.* // these are in the context by default: ExecutionContext ec, Map context, Map result <#visit xmlActionsRoot/> From 91ba9b7b91c084b72ea20f44fb247705b4212774 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Tue, 7 Mar 2023 13:11:43 -0800 Subject: [PATCH 20/61] Add subTopic field to NotificationMessage --- framework/entity/SecurityEntities.xml | 1 + .../moqui/impl/context/NotificationMessageImpl.groovy | 11 +++++++++-- .../java/org/moqui/context/NotificationMessage.java | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/framework/entity/SecurityEntities.xml b/framework/entity/SecurityEntities.xml index 71cc6405b..e5e7f8a26 100644 --- a/framework/entity/SecurityEntities.xml +++ b/framework/entity/SecurityEntities.xml @@ -513,6 +513,7 @@ along with this software (see the LICENSE.md file). If not, see + diff --git a/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy index b30563d2d..8c990c8c9 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy @@ -37,6 +37,7 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { private Set userIdSet = new HashSet() private String userGroupId = (String) null private String topic = (String) null + private String subTopic = (String) null private transient EntityValue notificationTopic = (EntityValue) null private String messageJson = (String) null private transient Map messageMap = (Map) null @@ -144,6 +145,9 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { @Override NotificationMessage topic(String topic) { this.topic = topic; notificationTopic = null; return this } @Override String getTopic() { topic } + @Override String getSubTopic() { subTopic } + @Override NotificationMessage subTopic(String st) { subTopic = st; return this } + @Override NotificationMessage message(String messageJson) { this.messageJson = messageJson; messageMap = null; return this } @Override NotificationMessage message(Map message) { this.messageMap = Collections.unmodifiableMap(message) as Map @@ -305,7 +309,7 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { boolean beganTransaction = tfi.begin(null) try { Map createResult = ecfi.service.sync().name("create", "moqui.security.user.NotificationMessage") - .parameters([topic:this.topic, userGroupId:this.userGroupId, sentDate:this.sentDate, + .parameters([topic:this.topic, subTopic:this.subTopic, userGroupId:this.userGroupId, sentDate:this.sentDate, messageJson:this.getMessageJson(), titleText:this.getTitle(), linkText:this.getLink(), typeString:this.getType(), showAlert:(this.showAlert ? 'Y' : 'N')]) .disableAuthz().call() @@ -450,7 +454,7 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { @Override Map getWrappedMessageMap() { EntityValue localNotTopic = getNotificationTopic() - return [topic:topic, sentDate:sentDate, notificationMessageId:notificationMessageId, topicDescription:localNotTopic?.description, + return [topic:topic, subTopic:subTopic, sentDate:sentDate, notificationMessageId:notificationMessageId, topicDescription:localNotTopic?.description, message:getMessageMap(), title:getTitle(), link:getLink(), type:getType(), persistOnSend:isPersistOnSend(), showAlert:isShowAlert(), alertNoAutoHide:isAlertNoAutoHide()] } @@ -467,6 +471,7 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { void populateFromValue(EntityValue nmbu) { this.notificationMessageId = nmbu.notificationMessageId this.topic = nmbu.topic + this.subTopic = nmbu.subTopic this.sentDate = nmbu.getTimestamp("sentDate") this.userGroupId = nmbu.userGroupId this.messageJson = nmbu.messageJson @@ -486,6 +491,7 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { out.writeObject(userIdSet) out.writeObject(userGroupId) out.writeUTF(topic) + out.writeObject(subTopic) out.writeUTF(getMessageJson()) out.writeObject(notificationMessageId) out.writeObject(sentDate) @@ -500,6 +506,7 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { userIdSet = (Set) objectInput.readObject() userGroupId = (String) objectInput.readObject() topic = objectInput.readUTF() + subTopic = objectInput.readObject() messageJson = objectInput.readUTF() notificationMessageId = (String) objectInput.readObject() sentDate = (Timestamp) objectInput.readObject() diff --git a/framework/src/main/java/org/moqui/context/NotificationMessage.java b/framework/src/main/java/org/moqui/context/NotificationMessage.java index b478e6121..38cb9636e 100644 --- a/framework/src/main/java/org/moqui/context/NotificationMessage.java +++ b/framework/src/main/java/org/moqui/context/NotificationMessage.java @@ -38,6 +38,9 @@ enum NotificationType { info, success, warning, danger } NotificationMessage topic(String topic); String getTopic(); + NotificationMessage subTopic(String subTopic); + String getSubTopic(); + /** Set the message as a JSON String. The top-level should be a Map (JSON Object). * @param messageJson The message as a JSON string containing a Map (JSON Object) * @return Self-reference for convenience From e04b65f645b3b2ee12ed5d37413e240fedd81b9f Mon Sep 17 00:00:00 2001 From: Acetousk Date: Mon, 13 Mar 2023 14:30:12 -0600 Subject: [PATCH 21/61] Refactor getTitle to prioritize title() method call over data --- .../context/NotificationMessageImpl.groovy | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy index 8c990c8c9..43988372d 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/NotificationMessageImpl.groovy @@ -173,16 +173,18 @@ class NotificationMessageImpl implements NotificationMessage, Externalizable { @Override NotificationMessage title(String title) { titleTemplate = title; return this } @Override String getTitle() { if (titleText == null) { - EntityValue localNotTopic = getNotificationTopic() - if (localNotTopic != null) { - if (type == danger && localNotTopic.errorTitleTemplate) { - titleText = ecfi.resource.expand((String) localNotTopic.errorTitleTemplate, "", getMessageMap(), true) - } else if (localNotTopic.titleTemplate) { - titleText = ecfi.resource.expand((String) localNotTopic.titleTemplate, "", getMessageMap(), true) + if (titleTemplate != null && !titleTemplate.isEmpty()) + titleText = ecfi.resource.expand(titleTemplate, "", getMessageMap(), true) + if (titleText == null || titleText.isEmpty()) { + EntityValue localNotTopic = getNotificationTopic() + if (localNotTopic != null) { + if (type == danger && localNotTopic.errorTitleTemplate) { + titleText = ecfi.resource.expand((String) localNotTopic.errorTitleTemplate, "", getMessageMap(), true) + } else if (localNotTopic.titleTemplate) { + titleText = ecfi.resource.expand((String) localNotTopic.titleTemplate, "", getMessageMap(), true) + } } } - if ((titleText == null || titleText.isEmpty()) && titleTemplate != null && !titleTemplate.isEmpty()) - titleText = ecfi.resource.expand(titleTemplate, "", getMessageMap(), true) } return titleText } From 3a3089ad4e94ec58b1b2c765786ec15abe969d80 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Tue, 11 Apr 2023 16:14:24 -0500 Subject: [PATCH 22/61] In MoquiStart add support for webapp_session_cookie_max_age env var to set the cookie expire time based on this age in seconds, if not specified defaults to Session expire and cookie will be dropped by the browser when it quits --- framework/src/start/java/MoquiStart.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/framework/src/start/java/MoquiStart.java b/framework/src/start/java/MoquiStart.java index 031366779..73010a320 100644 --- a/framework/src/start/java/MoquiStart.java +++ b/framework/src/start/java/MoquiStart.java @@ -282,6 +282,20 @@ public static void main(String[] args) throws IOException { // NOTE DEJ20210520: now always using StartClassLoader because of breaking classloader changes in 9.4.37 (likely from https://github.com/eclipse/jetty.project/pull/5894) webappClass.getMethod("setClassLoader", ClassLoader.class).invoke(webapp, moquiStartLoader); + // handle webapp_session_cookie_max_age with setInitParameter (1209600 seconds is about 2 weeks 60 * 60 * 24 * 14) + String sessionMaxAge = System.getenv("webapp_session_cookie_max_age"); + if (sessionMaxAge != null && !sessionMaxAge.isEmpty()) { + Integer maxAgeInt = null; + try { maxAgeInt = Integer.parseInt(sessionMaxAge); } + catch (Exception e) { System.out.println("Found webapp_session_cookie_max_age env var with invalid number, ignoring: " + sessionMaxAge); } + + if (maxAgeInt != null) { + System.out.println("Setting Servlet Session Max Age based on webapp_session_cookie_max_age " + maxAgeInt); + webappClass.getMethod("setInitParameter", String.class, String.class) + .invoke(webapp, "org.eclipse.jetty.servlet.MaxAge", maxAgeInt.toString()); + } + } + // WebSocket Object wsContainer = wsInitializerClass.getMethod("configure", scHandlerClass, wsInitializerConfiguratorClass).invoke(null, webapp, null); webappClass.getMethod("setAttribute", String.class, Object.class).invoke(webapp, "javax.websocket.server.ServerContainer", wsContainer); From 1417b4466497b40e0dadca02d54155b3f4609e87 Mon Sep 17 00:00:00 2001 From: Yao Chunlin Date: Fri, 14 Apr 2023 14:16:27 +0800 Subject: [PATCH 23/61] BugFix for DataFeed could not find backward relationship for DataDocument --- .../src/main/groovy/org/moqui/impl/entity/EntityDataFeed.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityDataFeed.groovy b/framework/src/main/groovy/org/moqui/impl/entity/EntityDataFeed.groovy index 02e5e8d41..c46a00ca9 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityDataFeed.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityDataFeed.groovy @@ -633,7 +633,7 @@ class EntityDataFeed { // entity name alone String currentRelName = backwardRelList.get(i) String currentRelEntityName = currentRelName.contains("#") ? - currentRelName.substring(0, currentRelName.indexOf("#")) : + currentRelName.substring(currentRelName.indexOf("#") + 1) : currentRelName // all values should be for the same entity, so just use the first EntityDefinition prevRelValueEd = prevRelValueList.get(0).getEntityDefinition() From 498b947b99041e7ad466a739e932dd99bf5a5893 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Thu, 27 Apr 2023 12:05:27 -0500 Subject: [PATCH 24/61] Add new WeCreate application component to addons.xml --- addons.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons.xml b/addons.xml index 8e7e0e3de..01967527c 100644 --- a/addons.xml +++ b/addons.xml @@ -77,6 +77,7 @@ + From a650649f975faf3cb90bef9e099a9f850540fbe7 Mon Sep 17 00:00:00 2001 From: Adam Heath Date: Mon, 8 May 2023 13:45:37 -0500 Subject: [PATCH 25/61] Allow for properties to be marked "is-secret" so their values don't get printed into the log at startup. --- .../impl/context/ExecutionContextFactoryImpl.groovy | 13 ++++++------- framework/xsd/moqui-conf-3.xsd | 1 + 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy index e6c93a26f..d33a23d0e 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy @@ -390,28 +390,27 @@ class ExecutionContextFactoryImpl implements ExecutionContextFactory { // set default System properties now that all is merged for (MNode defPropNode in baseConfigNode.children("default-property")) { String propName = defPropNode.attribute("name") + boolean isSecret = "true".equals(defPropNode.attribute("is-secret")) || propName.contains("pass") || propName.contains("pw") || propName.contains("key") if (System.getProperty(propName)) { - if (propName.contains("pass") || propName.contains("pw") || propName.contains("key")) { + if (isSecret) { logger.info("Found pw/key property ${propName}, not setting from env var or default") } else { logger.info("Found property ${propName} with value [${System.getProperty(propName)}], not setting from env var or default") } continue - } - if (System.getenv(propName) && !System.getProperty(propName)) { + } else if (System.getenv(propName)) { // make env vars available as Java System properties System.setProperty(propName, System.getenv(propName)) - if (propName.contains("pass") || propName.contains("pw") || propName.contains("key")) { + if (isSecret) { logger.info("Setting pw/key property ${propName} from env var") } else { logger.info("Setting property ${propName} from env var with value [${System.getProperty(propName)}]") } - } - if (!System.getProperty(propName) && !System.getenv(propName)) { + } else { String valueAttr = defPropNode.attribute("value") if (valueAttr != null && !valueAttr.isEmpty()) { System.setProperty(propName, SystemBinding.expand(valueAttr)) - if (propName.contains("pass") || propName.contains("pw") || propName.contains("key")) { + if (isSecret) { logger.info("Setting pw/key property ${propName} from default") } else { logger.info("Setting property ${propName} from default with value [${System.getProperty(propName)}]") diff --git a/framework/xsd/moqui-conf-3.xsd b/framework/xsd/moqui-conf-3.xsd index fa88f3b45..78adec646 100644 --- a/framework/xsd/moqui-conf-3.xsd +++ b/framework/xsd/moqui-conf-3.xsd @@ -49,6 +49,7 @@ along with this software (see the LICENSE.md file). If not, see + From 9ec342ebc0afb88ec9764843e8223c503a399f9b Mon Sep 17 00:00:00 2001 From: Adam Heath Date: Mon, 8 May 2023 13:54:57 -0500 Subject: [PATCH 26/61] Add weight microgram weight unit of measure. --- framework/data/UnitData.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/data/UnitData.xml b/framework/data/UnitData.xml index badda117c..6bd05a501 100644 --- a/framework/data/UnitData.xml +++ b/framework/data/UnitData.xml @@ -331,6 +331,7 @@ along with this software (see the LICENSE.md file). If not, see + @@ -346,6 +347,7 @@ along with this software (see the LICENSE.md file). If not, see + From b2c8c78ddd3a910f81f2089b3bbdd57fb70047c3 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Tue, 16 May 2023 00:14:56 -0500 Subject: [PATCH 27/61] Small change to new default-property.@is-secret attribute so that a false value treats it as non-secret even if it contains one of the previously supported substrings (pass, pw, key); add explicit is-secret=true attrs to a few in MoquiDefaultConf.xml --- .../impl/context/ExecutionContextFactoryImpl.groovy | 11 ++++++----- framework/src/main/resources/MoquiDefaultConf.xml | 10 +++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy index d33a23d0e..99b1716fb 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy @@ -390,19 +390,20 @@ class ExecutionContextFactoryImpl implements ExecutionContextFactory { // set default System properties now that all is merged for (MNode defPropNode in baseConfigNode.children("default-property")) { String propName = defPropNode.attribute("name") - boolean isSecret = "true".equals(defPropNode.attribute("is-secret")) || propName.contains("pass") || propName.contains("pw") || propName.contains("key") + String isSecretAttr = defPropNode.attribute("is-secret") + boolean isSecret = !"false".equals(isSecretAttr) && + ("true".equals(isSecretAttr) || propName.contains("pass") || propName.contains("pw") || propName.contains("key")) if (System.getProperty(propName)) { if (isSecret) { - logger.info("Found pw/key property ${propName}, not setting from env var or default") + logger.info("Found secret property ${propName}, not setting from env var or default") } else { logger.info("Found property ${propName} with value [${System.getProperty(propName)}], not setting from env var or default") } - continue } else if (System.getenv(propName)) { // make env vars available as Java System properties System.setProperty(propName, System.getenv(propName)) if (isSecret) { - logger.info("Setting pw/key property ${propName} from env var") + logger.info("Setting secret property ${propName} from env var") } else { logger.info("Setting property ${propName} from env var with value [${System.getProperty(propName)}]") } @@ -411,7 +412,7 @@ class ExecutionContextFactoryImpl implements ExecutionContextFactory { if (valueAttr != null && !valueAttr.isEmpty()) { System.setProperty(propName, SystemBinding.expand(valueAttr)) if (isSecret) { - logger.info("Setting pw/key property ${propName} from default") + logger.info("Setting secret property ${propName} from default") } else { logger.info("Setting property ${propName} from default with value [${System.getProperty(propName)}]") } diff --git a/framework/src/main/resources/MoquiDefaultConf.xml b/framework/src/main/resources/MoquiDefaultConf.xml index e44aeb3f3..07f1e9bad 100644 --- a/framework/src/main/resources/MoquiDefaultConf.xml +++ b/framework/src/main/resources/MoquiDefaultConf.xml @@ -49,9 +49,9 @@ - - - + + + @@ -67,7 +67,7 @@ - + @@ -75,7 +75,7 @@ - + From e8e881a829a447b0e6f6ab544d60f18313e09e64 Mon Sep 17 00:00:00 2001 From: Yao Chunlin Date: Tue, 23 May 2023 11:37:03 +0800 Subject: [PATCH 28/61] Fix regression with partitioned tables in PostgreSQL PostgreSQL JDBC Driver introduced separating type for partitioned table from 40.2.12 pgjdbc/pgjdbc#1708 --- .../src/main/groovy/org/moqui/impl/entity/EntityDbMeta.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/entity/EntityDbMeta.groovy b/framework/src/main/groovy/org/moqui/impl/entity/EntityDbMeta.groovy index da1e61d50..d487e7de6 100644 --- a/framework/src/main/groovy/org/moqui/impl/entity/EntityDbMeta.groovy +++ b/framework/src/main/groovy/org/moqui/impl/entity/EntityDbMeta.groovy @@ -107,7 +107,7 @@ class EntityDbMeta { String schemaName = datasourceNode != null ? datasourceNode.attribute("schema-name") : null Set groupEntityNames = efi.getAllEntityNamesInGroup(groupName) - String[] types = ["TABLE", "VIEW", "ALIAS", "SYNONYM"] + String[] types = ["TABLE", "VIEW", "ALIAS", "SYNONYM", "PARTITIONED TABLE"] Set existingTableNames = new HashSet<>() boolean beganTx = useTxForMetaData ? efi.ecfi.transactionFacade.begin(300) : false @@ -458,7 +458,7 @@ class EntityDbMeta { con = efi.getConnection(groupName) DatabaseMetaData dbData = con.getMetaData() - String[] types = ["TABLE", "VIEW", "ALIAS", "SYNONYM"] + String[] types = ["TABLE", "VIEW", "ALIAS", "SYNONYM", "PARTITIONED TABLE"] tableSet1 = dbData.getTables(con.getCatalog(), ed.getSchemaName(), ed.getTableName(), types) if (tableSet1.next()) { dbResult = true From bf4e68fd85de002f9835648eced6727b5229455c Mon Sep 17 00:00:00 2001 From: David E Jones Date: Fri, 26 May 2023 16:41:15 -0500 Subject: [PATCH 29/61] In ScreenRenderImpl add methods needed for section-include to support pagination; in CollectionUtilities paginateList() and paginateParameters() add support for entity-find with search-form-inputs or other situations where paginate parameters are already in place, don't overwrite them and don't try to subList() a big list --- .../moqui/impl/screen/ScreenRenderImpl.groovy | 17 +++++++-- .../org/moqui/util/CollectionUtilities.java | 36 +++++++++++-------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy index 183c22cfd..780100970 100644 --- a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy @@ -1203,7 +1203,15 @@ class ScreenRenderImpl implements ScreenRender { return "" } - String renderSectionInclude(MNode sectionIncludeNode) { + MNode getSectionIncludedNode(MNode sectionIncludeNode) { + ScreenDefinition sd = getActiveScreenDef() + String sectionName = getSectionIncludeName(sectionIncludeNode) + ScreenSection section = sd.getSection(sectionName) + if (section == null) throw new BaseArtifactException("No section with name [${sectionName}] in screen [${sd.location}]") + return section.sectionNode + } + + String getSectionIncludeName(MNode sectionIncludeNode) { String sectionLocation = sectionIncludeNode.attribute("location") String sectionName = sectionIncludeNode.attribute("name") boolean isDynamic = (sectionLocation != null && sectionLocation.contains('${')) || (sectionName != null && sectionName.contains('${')) @@ -1214,11 +1222,14 @@ class ScreenRenderImpl implements ScreenRender { String cacheName = sectionLocation + "#" + sectionName if (sd.sectionByName.get(cacheName) == null) sd.pullSectionInclude(sectionIncludeNode) // logger.warn("sd.sectionByName ${sd.sectionByName}") - return renderSection(cacheName) + return cacheName } else { - return renderSection(sectionName) + return sectionName } } + String renderSectionInclude(MNode sectionIncludeNode) { + renderSection(getSectionIncludeName(sectionIncludeNode)) + } MNode getFormNode(String formName) { FormInstance fi = getFormInstance(formName) diff --git a/framework/src/main/java/org/moqui/util/CollectionUtilities.java b/framework/src/main/java/org/moqui/util/CollectionUtilities.java index 3dfae7570..3252956a8 100644 --- a/framework/src/main/java/org/moqui/util/CollectionUtilities.java +++ b/framework/src/main/java/org/moqui/util/CollectionUtilities.java @@ -593,6 +593,9 @@ public static void paginateList(String listName, String pageListName, Map context) { + // if this exists then was already paginated so don't do a subList() + if (context.containsKey(pageListName + "AlreadyPaginated")) return theList; + Integer pageRangeLow = (Integer) context.get(pageListName + "PageRangeLow"); Integer pageRangeHigh = (Integer) context.get(pageListName + "PageRangeHigh"); if (pageRangeLow == null || pageRangeHigh == null) { @@ -619,20 +622,25 @@ public static Map paginateParameters(int listSize, String pageListName, Map count) pageRangeLow = count + 1; - int pageRangeHigh = (pageIndex * pageSize) + pageSize; - if (pageRangeHigh > count) pageRangeHigh = count; - - context.put(pageListName + "Count", count); - context.put(pageListName + "PageIndex", pageIndex); - context.put(pageListName + "PageSize", pageSize); - context.put(pageListName + "PageMaxIndex", maxIndex); - context.put(pageListName + "PageRangeLow", pageRangeLow); - context.put(pageListName + "PageRangeHigh", pageRangeHigh); + // NOTE: if context has a *Count field don't calc and set values, assume are already in place + if (context.get(pageListName + "Count") == null) { + int count = listSize; + // calculate the pagination values + int maxIndex = (new BigDecimal(count - 1)).divide(new BigDecimal(pageSize), 0, RoundingMode.DOWN).intValue(); + int pageRangeLow = (pageIndex * pageSize) + 1; + if (pageRangeLow > count) pageRangeLow = count + 1; + int pageRangeHigh = (pageIndex * pageSize) + pageSize; + if (pageRangeHigh > count) pageRangeHigh = count; + + context.put(pageListName + "Count", count); + context.put(pageListName + "PageIndex", pageIndex); + context.put(pageListName + "PageSize", pageSize); + context.put(pageListName + "PageMaxIndex", maxIndex); + context.put(pageListName + "PageRangeLow", pageRangeLow); + context.put(pageListName + "PageRangeHigh", pageRangeHigh); + } else { + context.put(pageListName + "AlreadyPaginated", true); + } return context; } From e9d158fcd4f0256228bb3b887856a526948264db Mon Sep 17 00:00:00 2001 From: David E Jones Date: Mon, 12 Jun 2023 16:25:17 -0500 Subject: [PATCH 30/61] Add new moqui-image repo, fork of original work be acetousk --- addons.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons.xml b/addons.xml index 01967527c..789609c18 100644 --- a/addons.xml +++ b/addons.xml @@ -47,6 +47,7 @@ + From 0a06a904ff923bad5cd7f25eebe4141869ba9612 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Tue, 13 Jun 2023 15:08:50 -0500 Subject: [PATCH 31/61] Add Sales app from xolvegroup (third party component) --- addons.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons.xml b/addons.xml index 789609c18..d449211e1 100644 --- a/addons.xml +++ b/addons.xml @@ -94,6 +94,7 @@ + @@ -81,7 +83,9 @@ - + diff --git a/framework/xsd/moqui-conf-3.xsd b/framework/xsd/moqui-conf-3.xsd index 78adec646..d060a6176 100644 --- a/framework/xsd/moqui-conf-3.xsd +++ b/framework/xsd/moqui-conf-3.xsd @@ -61,6 +61,14 @@ along with this software (see the LICENSE.md file). If not, see table for moqui.basic.Enumeration). Empty or 'none' means load nothing, use 'all' to load all found data files regardless of type. + + Comma-separated list of data file types to load on start. Empty or 'none' means load nothing. + Does not run if empty-db-load runs. + + + Comma-separated list of component names to load on start, used with on-start-load-types. + Does not run if empty-db-load runs. + The maximum size of the worker queue. From d93b4eeee92498cc2ebfec2a41d9622b87db7f7f Mon Sep 17 00:00:00 2001 From: Wei Zhang Date: Wed, 26 Jul 2023 21:54:45 +0800 Subject: [PATCH 33/61] Add subscreensItem.menuInclude to menu data (#600) * Fixed the problem that moqui cannot be deployed as non-root webapp in Tomcat * Fixed a runtime error if Currency is BTC * Fixed a runtime error if Currency is BTC * Fixed the retries of Elastic Client * Add subscreensItem.menuInclude to menu data --- .../main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy index 780100970..41d3b8fcd 100644 --- a/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/screen/ScreenRenderImpl.groovy @@ -2347,6 +2347,7 @@ class ScreenRenderImpl implements ScreenRender { boolean active = (nextItem == subscreensItem.name) Map itemMap = [name:subscreensItem.name, title:ec.resource.expand(subscreensItem.menuTitle, ""), path:screenPath, pathWithParams:pathWithParams, image:image, imageType:imageType] + if (subscreensItem.menuInclude) itemMap.menuInclude = true if (active) itemMap.active = true if (screenUrlInstance.disableLink) itemMap.disableLink = true subscreensList.add(itemMap) From 86c3e0c431953ae7f6e8b7e44ec81880c58e7d2d Mon Sep 17 00:00:00 2001 From: Rohit pawar <72196393+rohitpawar2811@users.noreply.github.com> Date: Wed, 26 Jul 2023 19:30:13 +0530 Subject: [PATCH 34/61] Docker-Image-Pull Feature (#553) * Improvement: In Moqui-Multi-Instance added a Async-Pull-Image-Feature which pulls the image by Using Docker-Engine-Api from multiple registry like AWS, Azure, Docker-Hub * Update AUTHORS * Added: added the generic way to process cmd , by adding an extra field(authTokenPass) inside the entity InstanceImage , now user has separate field for cmd and password so all types of registries is easily configurable * Update ServerEntities.xml by adding description --- AUTHORS | 2 + framework/entity/ServerEntities.xml | 3 + .../org/moqui/impl/InstanceServices.xml | 60 ++++++++++++++++++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 27dbd1a34..77107bd60 100644 --- a/AUTHORS +++ b/AUTHORS @@ -61,6 +61,7 @@ Written in 2020 by Amir Anjomshoaa - amiranjom Written in 2021 by Deepak Dixit - dixitdeepak Written in 2021 by Taher Alkhateeb - pythys Written in 2022 by Zhang Wei - hellozhangwei +Written in 2023 by Rohit Pawar - rohitpawar2811 =========================================================================== @@ -108,5 +109,6 @@ Written in 2020 by Amir Anjomshoaa - amiranjom Written in 2021 by Deepak Dixit - dixitdeepak Written in 2021 by Taher Alkhateeb - pythys Written in 2022 by Zhang Wei - hellozhangwei +Written in 2023 by Rohit Pawar - rohitpawar2811 =========================================================================== diff --git a/framework/entity/ServerEntities.xml b/framework/entity/ServerEntities.xml index 17ec6556d..e9b63af25 100644 --- a/framework/entity/ServerEntities.xml +++ b/framework/entity/ServerEntities.xml @@ -196,6 +196,9 @@ along with this software (see the LICENSE.md file). If not, see + + + diff --git a/framework/service/org/moqui/impl/InstanceServices.xml b/framework/service/org/moqui/impl/InstanceServices.xml index 7071d77fa..dacbd18ca 100644 --- a/framework/service/org/moqui/impl/InstanceServices.xml +++ b/framework/service/org/moqui/impl/InstanceServices.xml @@ -214,8 +214,13 @@ along with this software (see the LICENSE.md file). If not, see - - + + + @@ -281,8 +286,9 @@ along with this software (see the LICENSE.md file). If not, see - https://docs.docker.com/engine/security/https/ --> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Authentication code is not valid + + + + + + + + + + + @@ -377,7 +422,7 @@ along with this software (see the LICENSE.md file). If not, see - A reset password was sent by email to ${userAccount.emailAddress}. This password may only be used to change your password. Your current password is still valid. + A reset password was sent to the email of username ${userAccount.username}. This password may only be used to change your password. Your current password is still valid. You must change your password before login. From f1f5f05169289ba7a79d1bb2daa1bf85af4111b2 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Wed, 10 Jan 2024 14:37:43 -0600 Subject: [PATCH 54/61] Follow up on PR #628: formatting changes only to reduce indentation and better match convention used elsewhere, no functional changes --- .../service/org/moqui/impl/UserServices.xml | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/framework/service/org/moqui/impl/UserServices.xml b/framework/service/org/moqui/impl/UserServices.xml index 47e154f65..a82c80322 100644 --- a/framework/service/org/moqui/impl/UserServices.xml +++ b/framework/service/org/moqui/impl/UserServices.xml @@ -33,36 +33,25 @@ along with this software (see the LICENSE.md file). If not, see - - - - - - - - - - - - - - - - - - - - - Authentication code is not valid - - - - - - - + + + + + + + + + + + + + + Authentication code is not valid + + + + From ba5fa64a9247aafd330b6e743003055f00ff2764 Mon Sep 17 00:00:00 2001 From: AyanF Date: Wed, 7 Feb 2024 12:02:10 +0530 Subject: [PATCH 55/61] Re-ordered Console Appender in log4j2.xml --- framework/src/main/resources/log4j2.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/src/main/resources/log4j2.xml b/framework/src/main/resources/log4j2.xml index 78692f3bc..c31011e18 100644 --- a/framework/src/main/resources/log4j2.xml +++ b/framework/src/main/resources/log4j2.xml @@ -35,15 +35,15 @@ https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout + + + - - - From d750346d92f77c2f50ed1ee7926732187382a61b Mon Sep 17 00:00:00 2001 From: acetousk Date: Fri, 16 Feb 2024 16:00:41 -0700 Subject: [PATCH 56/61] Add screen resource type for a footer script --- framework/entity/ScreenEntities.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/entity/ScreenEntities.xml b/framework/entity/ScreenEntities.xml index 7f04eb37f..cc296a3e1 100644 --- a/framework/entity/ScreenEntities.xml +++ b/framework/entity/ScreenEntities.xml @@ -139,6 +139,7 @@ along with this software (see the LICENSE.md file). If not, see + From f264e9e2b7ba2d20b8b25052b148fc5df4c09685 Mon Sep 17 00:00:00 2001 From: David E Jones Date: Sat, 17 Feb 2024 13:37:25 -0600 Subject: [PATCH 57/61] Add xolvegroup/WorkManagement, corrected branch on xolvegroup/Sales in addons.xml --- addons.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons.xml b/addons.xml index f5d4f2aac..2e40d7711 100644 --- a/addons.xml +++ b/addons.xml @@ -95,7 +95,8 @@ - + +