diff --git a/README.md b/README.md index 5cc83b57..4afac0a8 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ The following example demonstrates how to create a user and a group, assign perm ```groovy boolean canRun() { - return conditions.idleSelf() && conditions.everyHour() + return conditions.notQueuedSelf() && conditions.everyHour() } void doRun() { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java b/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java index 36f35068..45e18bb5 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java @@ -61,32 +61,40 @@ public Stream passedExecutions() { return executionHistory.findAll(query); } + public boolean idle() { + return notQueued(); + } + public boolean idleSelf() { - if (executionContext.getExecutor().isLocking() && lockedSelf()) { + return notQueuedSelf(); + } + + public boolean notQueued() { + if (executionContext.getExecutor().isLocking() && lockedAny()) { return false; } - return !queuedSelfExecutions().findAny().isPresent(); + return !queuedExecutions().findAny().isPresent(); } - public boolean notRunningSelf() { + public boolean notQueuedSelf() { if (executionContext.getExecutor().isLocking() && lockedSelf()) { return false; } - return noneRunning(queuedSelfExecutions()); + return !queuedSelfExecutions().findAny().isPresent(); } - public boolean idle() { + public boolean notRunning() { if (executionContext.getExecutor().isLocking() && lockedAny()) { return false; } - return !queuedExecutions().findAny().isPresent(); + return noneRunning(queuedExecutions()); } - public boolean notRunning() { + public boolean notRunningSelf() { if (executionContext.getExecutor().isLocking() && lockedAny()) { return false; } - return noneRunning(queuedExecutions()); + return noneRunning(queuedSelfExecutions()); } private boolean noneRunning(Stream queuedExecutions) { @@ -627,7 +635,7 @@ public boolean retry(long count) { if (count < 1) { throw new IllegalArgumentException("Retry count must be greater than zero!"); } - if (!idleSelf()) { + if (!notQueuedSelf()) { return false; } Execution passedExecution = passedExecution(); diff --git a/core/src/main/java/dev/vml/es/acm/core/code/ExecutionQueue.java b/core/src/main/java/dev/vml/es/acm/core/code/ExecutionQueue.java index 4fa60fa0..e5418a70 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/ExecutionQueue.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/ExecutionQueue.java @@ -67,7 +67,7 @@ protected void deactivate() { } } - public Optional submit(ExecutionContextOptions contextOptions, Executable executable) + public Execution submit(ExecutionContextOptions contextOptions, Executable executable) throws AcmException { Map jobProps = new HashMap<>(); jobProps.putAll(ExecutionContextOptions.toJobProps(contextOptions)); @@ -76,9 +76,9 @@ public Optional submit(ExecutionContextOptions contextOptions, Execut Job job = jobManager.addJob(TOPIC, jobProps); if (job == null) { - return Optional.empty(); + throw new AcmException(String.format("Execution of executable '%s' cannot be queued because manager refused to add a job!", executable.getId())); } - return Optional.of(new QueuedExecution(executor, job)); + return new QueuedExecution(executor, job); } public Stream findAll() { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Executor.java b/core/src/main/java/dev/vml/es/acm/core/code/Executor.java index d8359485..e4513ce9 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Executor.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Executor.java @@ -82,7 +82,7 @@ public Execution execute(Executable executable, ExecutionContextOptions contextO return execute(executionContext); } catch (LoginException e) { throw new AcmException( - String.format("Cannot access repository while executing '%s'", executable.getId()), e); + String.format("Cannot access repository while executing '%s'!", executable.getId()), e); } } diff --git a/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java b/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java index bfffc447..43608f28 100644 --- a/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java +++ b/core/src/main/java/dev/vml/es/acm/core/script/ScriptScheduler.java @@ -1,10 +1,7 @@ package dev.vml.es.acm.core.script; import dev.vml.es.acm.core.AcmException; -import dev.vml.es.acm.core.code.ExecutionContextOptions; -import dev.vml.es.acm.core.code.ExecutionMode; -import dev.vml.es.acm.core.code.ExecutionQueue; -import dev.vml.es.acm.core.code.Executor; +import dev.vml.es.acm.core.code.*; import dev.vml.es.acm.core.instance.HealthChecker; import dev.vml.es.acm.core.instance.HealthStatus; import dev.vml.es.acm.core.util.ResourceUtils; @@ -114,12 +111,34 @@ public void run() { scriptRepository.clean(); scriptRepository.findAll(ScriptType.ENABLED).forEach(script -> { - queue.submit(contextOptions, script); + if (checkScript(script, resourceResolver)) { + submitScript(script, contextOptions); + } }); runCount.incrementAndGet(); } catch (Exception e) { - LOG.error("Failed to access repository while scheduling enabled scripts to execution queue", e); + LOG.error("Cannot access repository while scheduling enabled scripts to execution queue!", e); + } + } + + private boolean checkScript(Script script, ResourceResolver resourceResolver) { + try (ExecutionContext context = + executor.createContext(ExecutionId.generate(), ExecutionMode.CHECK, script, resourceResolver)) { + Execution execution = executor.execute(context); + LOG.debug("Script checked '{}'", execution); + return execution.getStatus() != ExecutionStatus.SKIPPED; + } catch (Exception e) { + LOG.error("Cannot check script '{}' while scheduling to execution queue!", script.getId(), e); + return false; + } + } + + private void submitScript(Script script, ExecutionContextOptions contextOptions) { + try { + queue.submit(contextOptions, script); + } catch (Exception e) { + LOG.error("Cannot submit script '{}' to execution queue!", script.getId(), e); } } diff --git a/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java b/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java index 6225b3d9..bc9e3573 100644 --- a/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java +++ b/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java @@ -40,6 +40,9 @@ public class QueueCodeServlet extends SlingAllMethodsServlet { @Reference private ExecutionQueue executionQueue; + @Reference + private Executor executor; + @Override protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { try { @@ -50,15 +53,26 @@ protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse return; } - ExecutionContextOptions contextOptions = new ExecutionContextOptions( - ExecutionMode.RUN, request.getResourceResolver().getUserID()); Code code = input.getCode(); - Execution execution = executionQueue.submit(contextOptions, code).orElse(null); - if (execution == null) { - respondJson(response, error("Code execution cannot be queued!")); + + try (ExecutionContext context = + executor.createContext(ExecutionId.generate(), ExecutionMode.CHECK, input.getCode(), request.getResourceResolver())) { + Execution checkExecution = executor.execute(context); + if (checkExecution.getStatus() == ExecutionStatus.SKIPPED) { + QueueOutput output = new QueueOutput(Collections.singletonList(checkExecution)); + respondJson(response, ok(String.format("Code from '%s' skipped execution", code.getId()), output)); + return; + } + } catch (Exception e) { + LOG.error("Code execution cannot be checked!", e); + respondJson(response, badRequest(String.format("Code execution cannot be checked! %s", e.getMessage()).trim())); return; } + ExecutionContextOptions contextOptions = new ExecutionContextOptions( + ExecutionMode.RUN, request.getResourceResolver().getUserID()); + + Execution execution = executionQueue.submit(contextOptions, code); QueueOutput output = new QueueOutput(Collections.singletonList(execution)); respondJson( response, diff --git a/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.event.jobs.QueueConfiguration-acmexecutionqueue.config b/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.event.jobs.QueueConfiguration-acmexecutionqueue.config index 38e7ed7e..eb3c7afd 100644 --- a/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.event.jobs.QueueConfiguration-acmexecutionqueue.config +++ b/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.event.jobs.QueueConfiguration-acmexecutionqueue.config @@ -1,8 +1,8 @@ queue.name="AEM\ Content\ Manager\ Execution\ Queue" queue.topics=["dev/vml/es/acm/ExecutionQueue"] queue.priority="NORM" -queue.type="UNORDERED" -queue.maxparallel="15.0" +queue.type="ORDERED" +queue.maxparallel="1" queue.retries="0" queue.retrydelay="2000" queue.keepJobs="false" diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/disabled/example/ACME-2_changed.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/disabled/example/ACME-2_changed.groovy new file mode 100644 index 00000000..7e3ab5cf --- /dev/null +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/disabled/example/ACME-2_changed.groovy @@ -0,0 +1,7 @@ +boolean canRun() { + return conditions.notQueuedSelf() && conditions.changed() +} + +void doRun() { + throw new RuntimeException("I should run when script content changes or when the instance changes after a failure!") +} diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/disabled/example/ACME-2_instance_changed.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/disabled/example/ACME-2_instance_changed.groovy deleted file mode 100644 index 97968d1c..00000000 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/disabled/example/ACME-2_instance_changed.groovy +++ /dev/null @@ -1,7 +0,0 @@ -boolean canRun() { - return conditions.idleSelf() && (conditions.contentChanged() || conditions.retryIfInstanceChanged()) -} - -void doRun() { - throw new RuntimeException("I should run when content changes or when the instance changes after a failure!") -} diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-20_once.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-20_once.groovy index 43d06bbd..5f76b037 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-20_once.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-20_once.groovy @@ -1,5 +1,5 @@ boolean canRun() { - return conditions.idleSelf() && conditions.once() + return conditions.notQueuedSelf() && conditions.once() } void doRun() { diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-21_every-hour.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-21_every-hour.groovy index b223a9bf..14274e3d 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-21_every-hour.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-21_every-hour.groovy @@ -1,5 +1,5 @@ boolean canRun() { - return conditions.idleSelf() && conditions.everyHourAt(43) + return conditions.notQueuedSelf() && conditions.everyHourAt(43) } void doRun() { diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-22_every-day.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-22_every-day.groovy index 45a3fa64..b49192a5 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-22_every-day.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-22_every-day.groovy @@ -1,5 +1,5 @@ boolean canRun() { - return conditions.idleSelf() && conditions.everyDayAt("07:45") + return conditions.notQueuedSelf() && conditions.everyDayAt("07:45") } void doRun() { diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-23_every-week.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-23_every-week.groovy index 714f221d..2feec899 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-23_every-week.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-23_every-week.groovy @@ -1,5 +1,5 @@ boolean canRun() { - return conditions.idleSelf() && conditions.everyWeekAt("Monday", "07:48") + return conditions.notQueuedSelf() && conditions.everyWeekAt("Monday", "07:48") } void doRun() { diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-24_every-month.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-24_every-month.groovy index 6207d56f..30f075ab 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-24_every-month.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/auto/enabled/example/ACME-24_every-month.groovy @@ -1,5 +1,5 @@ boolean canRun() { - return conditions.idleSelf() && conditions.everyMonthAt(21, "07:49") + return conditions.notQueuedSelf() && conditions.everyMonthAt(21, "07:49") } void doRun() { diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/demo.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/demo.yml index 911b8764..bc969256 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/demo.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/demo.yml @@ -4,7 +4,7 @@ documentation: | Setup the ACLs for the demo users and groups. content: | boolean canRun() { - return conditions.idleSelf() + return conditions.notQueuedSelf() } void doRun() { diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/idle.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/not_queued.yml similarity index 74% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/idle.yml rename to ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/not_queued.yml index e3ba8991..cca10680 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/idle.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/not_queued.yml @@ -1,6 +1,6 @@ group: Condition -name: condition_idle +name: condition_not_queued content: | - conditions.idle() + conditions.notQueued() documentation: | Similar to `conditions.notRunning()` but also checks if none of the scripts are queued for execution. diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/idle_self.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/not_queued_self.yml similarity index 71% rename from ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/idle_self.yml rename to ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/not_queued_self.yml index e1571a6a..687beecb 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/idle_self.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/not_queued_self.yml @@ -1,6 +1,6 @@ group: Condition -name: condition_idle_self +name: condition_not_queued_self content: | - conditions.idleSelf() + conditions.notQueuedSelf() documentation: | Similar to `conditions.notRunningSelf()` but also checks if the script is not even queued for execution. diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/retry_if_instance_changed.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/retry_if_instance_changed.yml index 97a171d9..bb094ef1 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/retry_if_instance_changed.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/condition/retry_if_instance_changed.yml @@ -10,7 +10,7 @@ documentation: | For example: ```groovy boolean canRun() { - return conditions.idleSelf() && (conditions.contentChanged() || conditions.retryIfInstanceChanged()) + return conditions.notQueuedSelf() && (conditions.contentChanged() || conditions.retryIfInstanceChanged()) } void doRun() { diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/general/demo_processing.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/general/demo_processing.yml index 1669891a..af3f69f0 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/general/demo_processing.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/general/demo_processing.yml @@ -2,7 +2,7 @@ group: General name: general_demo_processing content: | boolean canRun() { - return conditions.idleSelf() + return conditions.notQueuedSelf() } void doRun() { diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/demo.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/demo.yml index e4aea15d..b04bce00 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/demo.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/repo/demo.yml @@ -16,7 +16,7 @@ content: | } boolean canRun() { - return conditions.idleSelf() + return conditions.notQueuedSelf() } void doRun() { diff --git a/ui.frontend/package.json b/ui.frontend/package.json index 2e279cf0..c9a1de6e 100644 --- a/ui.frontend/package.json +++ b/ui.frontend/package.json @@ -14,7 +14,6 @@ "dependencies": { "@adobe/react-spectrum": "^3.40.1", "@monaco-editor/react": "^4.6.0", - "@preact/signals-react": "^3.2.0", "@react-spectrum/toast": "^3.0.1", "@spectrum-icons/illustrations": "^3.6.20", "@spectrum-icons/workflow": "^4.2.19", diff --git a/ui.frontend/src/App.tsx b/ui.frontend/src/App.tsx index ee189447..545869db 100644 --- a/ui.frontend/src/App.tsx +++ b/ui.frontend/src/App.tsx @@ -1,18 +1,39 @@ import { defaultTheme, Flex, Provider, View } from '@adobe/react-spectrum'; import { ToastContainer } from '@react-spectrum/toast'; import equal from 'fast-deep-equal'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Outlet } from 'react-router-dom'; import './App.css'; +import { AppContext } from './AppContext'; import Footer from './components/Footer'; import Header from './components/Header'; -import { appState } from './hooks/app.ts'; import router from './router'; import { apiRequest } from './utils/api'; -import { State } from './utils/api.types'; +import { InstanceRole, InstanceType, State } from './utils/api.types'; import { intervalToTimeout } from './utils/spectrum.ts'; function App() { + const [state, setState] = useState({ + spaSettings: { + appStateInterval: 3000, + executionPollInterval: 1400, + scriptStatsLimit: 30, + }, + healthStatus: { + healthy: true, + issues: [], + }, + mockStatus: { + enabled: true, + }, + instanceSettings: { + id: 'default', + timezoneId: 'UTC', + role: InstanceRole.AUTHOR, + type: InstanceType.CLOUD_CONTAINER, + }, + }); + const isFetching = useRef(false); useEffect(() => { @@ -27,12 +48,17 @@ function App() { operation: 'Fetch application state', url: '/apps/acm/api/state.json', method: 'get', - timeout: intervalToTimeout(appState.value.spaSettings.appStateInterval), + timeout: intervalToTimeout(state.spaSettings.appStateInterval), quiet: true, }); const stateNew = response.data.data; - if (!equal(stateNew, appState.value)) { - appState.value = stateNew; + if (!equal(stateNew, state)) { + setState((prevState) => { + if (!equal(stateNew, prevState)) { + return stateNew; + } + return prevState; + }); } } catch (error) { console.warn('Cannot fetch application state:', error); @@ -42,10 +68,10 @@ function App() { }; fetchState(); - const intervalId = setInterval(fetchState, appState.value.spaSettings.appStateInterval); + const intervalId = setInterval(fetchState, state.spaSettings.appStateInterval); return () => clearInterval(intervalId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [appState.value.spaSettings.appStateInterval]); + }, [state.spaSettings.appStateInterval]); return ( - - -
- - - - - -