diff --git a/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java b/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java index 74c8ff8c3201..729b3468dbcf 100644 --- a/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java +++ b/dotCMS/src/main/java/com/dotcms/api/system/event/SystemEventType.java @@ -262,5 +262,8 @@ public enum SystemEventType { SESSION_LOGOUT, // Analytics App - ANALYTICS_APP + ANALYTICS_APP, + + // Progress of action tasks + PROGRESS } diff --git a/dotCMS/src/main/java/com/dotcms/concurrent/DotConcurrentFactory.java b/dotCMS/src/main/java/com/dotcms/concurrent/DotConcurrentFactory.java index 7cbf85736ee8..2265eda9dc41 100644 --- a/dotCMS/src/main/java/com/dotcms/concurrent/DotConcurrentFactory.java +++ b/dotCMS/src/main/java/com/dotcms/concurrent/DotConcurrentFactory.java @@ -5,8 +5,12 @@ import com.dotcms.concurrent.lock.DotKeyLockManagerBuilder; import com.dotcms.concurrent.lock.DotKeyLockManagerFactory; import com.dotcms.concurrent.lock.IdentifierStripedLock; +import com.dotcms.concurrent.monitor.LoggingTaskMonitor; +import com.dotcms.concurrent.monitor.TaskMonitor; +import com.dotcms.concurrent.monitor.ToastTaskMonitor; import com.dotcms.util.ConversionUtils; import com.dotcms.util.ReflectionUtils; +import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.init.DotInitScheduler; import com.dotmarketing.util.Config; @@ -92,6 +96,8 @@ public class DotConcurrentFactory implements DotConcurrentFactoryMBean, Serializ public static final String LOCK_MANAGER = "IdentifierStripedLock"; + private final Map taskMonitorMap = new ConcurrentHashMap<>(); + /** * Used to keep the instance of the submitter * Should be volatile to avoid thread-caching @@ -191,6 +197,9 @@ private DotConcurrentFactory () { this.delayQueueConsumer = new DelayQueueConsumer(); getScheduledThreadPoolExecutor().scheduleWithFixedDelay(this.delayQueueConsumer, 0, Config.getLongProperty("dotcms.concurrent.delayqueue.waitmillis", 100), TimeUnit.MILLISECONDS); + this.taskMonitorMap.put("logging", new LoggingTaskMonitor()); + this.taskMonitorMap.put("toast", new LoggingTaskMonitor()); + this.taskMonitorMap.put("progress", new LoggingTaskMonitor()); } private void subscribeDelayQueue(final BlockingQueue delayedQueue) { @@ -315,6 +324,15 @@ public static T get (final Future future) { } } + /** + * Return the current task id based on the server id and the thread id + * @return Object + */ + public Object getCurrentTaskId() { + + return APILocator.getServerAPI().readServerId() + Thread.currentThread().getId(); + } + private static class SingletonHolder { private static final DotConcurrentFactory INSTANCE = new DotConcurrentFactory(); } @@ -528,6 +546,16 @@ public IdentifierStripedLock getIdentifierStripedLock(){ return this.identifierStripedLock; } +/** + * Get the task monitor + * @param taskMonitorName {@link String} + * @return TaskMonitor + */ + public TaskMonitor getTaskMonitor(final String taskMonitorName) { + + final String defaultTaskMonitorName = Config.getStringProperty("DOT_DEFAULT_TASK_MONITOR_NAME","logging"); + return this.taskMonitorMap.getOrDefault(taskMonitorName, this.taskMonitorMap.get(defaultTaskMonitorName)); + } /** * Create a composite completable futures and results any of the first results done of the futures parameter. * @param futures diff --git a/dotCMS/src/main/java/com/dotcms/concurrent/monitor/LoggingTaskMonitor.java b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/LoggingTaskMonitor.java new file mode 100644 index 000000000000..44550b343bd8 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/LoggingTaskMonitor.java @@ -0,0 +1,34 @@ +package com.dotcms.concurrent.monitor; + +import com.dotmarketing.util.Logger; + +/** + * This is a simple implementation of the TaskMonitor interface that does just logging of the progress. + * @author jsanca + */ +public class LoggingTaskMonitor implements TaskMonitor { + + @Override + public void onTaskStarted(final Object processId, final Object subProcessId) { + + Logger.debug(this.getClass(), "Task started, process id: " + processId + " - sub process id:" + subProcessId); + } + + @Override + public void onTaskProgress(final Object processId, final Object subProcessId, final int progress) { + + Logger.debug(this.getClass(), "Task progress, process id: " + processId + " - sub process id:" + subProcessId + " - " + progress + "%"); + } + + @Override + public void onTaskCompleted(final Object processId, final Object subProcessId) { + + Logger.debug(this.getClass(), "Task completed, process id: " + processId + " - sub process id:" + subProcessId); + } + + @Override + public void onTaskFailed(final Object processId, final Object subProcessId, final Exception error) { + + Logger.error(this.getClass(), "Task failed, process id: " + processId + " - sub process id:" + subProcessId, error); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ProgressBean.java b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ProgressBean.java new file mode 100644 index 000000000000..312fa27525ad --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ProgressBean.java @@ -0,0 +1,101 @@ +package com.dotcms.concurrent.monitor; + +import java.io.Serializable; + +/** + * Class to manifest the progress of a bean + * @author jsanca + */ +public class ProgressBean implements Serializable { + + private static final long serialVersionUID = 1L; + private final int progress; + private final String message; + private final boolean completed; + private final boolean failed; + private final Object processId; + private final Object subProcessId; + + public ProgressBean(final Builder builder) { + super(); + this.progress = builder.progress; + this.message = builder.message; + this.completed = builder.completed; + this.failed = builder.failed; + this.processId = builder.processId; + this.subProcessId = builder.subProcessId; + } + + public int getProgress() { + return progress; + } + + public String getMessage() { + return message; + } + + public boolean isCompleted() { + return completed; + } + + public boolean isFailed() { + return failed; + } + + public Object getProcessId() { + return processId; + } + + public Object getSubProcessId() { + return subProcessId; + } + + public static class Builder { + + private int progress; + private String message; + private boolean completed; + private boolean failed; + private Object processId; + private Object subProcessId; + + public Builder progress(final int progress) { + this.progress = progress; + return this; + } + + public Builder message(final String message) { + this.message = message; + return this; + } + + public Builder completed(final boolean completed) { + this.completed = completed; + return this; + } + + public Builder failed(final boolean failed) { + this.failed = failed; + return this; + } + + public Builder processId(final Object processId) { + this.processId = processId; + return this; + } + + public Builder subProcessId(final Object subProcessId) { + this.subProcessId = subProcessId; + return this; + } + + public ProgressBean build() { + return new ProgressBean(this); + } + } + + @Override + public String toString() { + return "ProgressBean [progress=" + progress + ", message=" + message + ", completed=" + completed + ", failed=" + failed + ", processId=" + processId + ", subProcessId=" + subProcessId + "]"; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ProgressTaskMonitor.java b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ProgressTaskMonitor.java new file mode 100644 index 000000000000..f7b7aec6a1bf --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ProgressTaskMonitor.java @@ -0,0 +1,70 @@ +package com.dotcms.concurrent.monitor; + +import com.dotcms.api.system.event.Payload; +import com.dotcms.api.system.event.SystemEvent; +import com.dotcms.api.system.event.SystemEventType; +import com.dotcms.api.system.event.SystemEventsAPI; +import com.dotcms.api.system.event.Visibility; +import com.dotcms.api.system.event.message.MessageSeverity; +import com.dotcms.api.system.event.message.builder.SystemMessage; +import com.dotcms.api.system.event.message.builder.SystemMessageBuilder; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.util.Logger; + +/** + * This is an implementation of the TaskMonitor interface that sends to the client + * Progress Beans undert the Progress system event type + * @author jsanca + */ +public class ProgressTaskMonitor implements TaskMonitor { + + private final SystemEventsAPI systemEventsAPI = APILocator.getSystemEventsAPI(); + + @Override + public void onTaskStarted(final Object processId, final Object subProcessId) { + + this.push(new ProgressBean.Builder() + .processId(processId). + subProcessId(subProcessId).build()); + } + + private void push (final ProgressBean progressBean) { + + try { + + this.systemEventsAPI.push(new SystemEvent(SystemEventType.PROGRESS, + new Payload(progressBean))); + } catch (DotDataException e) { + Logger.error(this.getClass(), "Error pushing system event", e); + } + } + + @Override + public void onTaskProgress(final Object processId, final Object subProcessId, final int progress) { + + this.push(new ProgressBean.Builder() + .processId(processId). + subProcessId(subProcessId) + .progress(progress).build()); + } + + @Override + public void onTaskCompleted(final Object processId, final Object subProcessId) { + + this.push(new ProgressBean.Builder() + .processId(processId). + subProcessId(subProcessId) + .completed(true).build()); + } + + @Override + public void onTaskFailed(final Object processId, final Object subProcessId, final Exception error) { + + this.push(new ProgressBean.Builder() + .processId(processId). + subProcessId(subProcessId) + .failed(true) + .message(error.getMessage()).build()); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/concurrent/monitor/TaskMonitor.java b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/TaskMonitor.java new file mode 100644 index 000000000000..79a88d3216d4 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/TaskMonitor.java @@ -0,0 +1,33 @@ +package com.dotcms.concurrent.monitor; + +/** + * Interface for monitoring the progress of a task/process. + + * @author jsanca + */ +public interface TaskMonitor { + + /** + * Called when the task starts. + */ + void onTaskStarted(Object processId, Object subProcessId); + + /** + * Called to update the progress of the task. + * + * @param progress The current progress of the task, represented as a percentage (0-100). + */ + void onTaskProgress(Object processId, Object subProcessId, int progress); + + /** + * Called when the task completes successfully. + */ + void onTaskCompleted(Object processId, Object subProcessId); + + /** + * Called when the task fails. + * + * @param error The error that caused the task to fail. + */ + void onTaskFailed(Object processId, Object subProcessId, Exception error); +} diff --git a/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ToastTaskMonitor.java b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ToastTaskMonitor.java new file mode 100644 index 000000000000..7be265055b8b --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/concurrent/monitor/ToastTaskMonitor.java @@ -0,0 +1,61 @@ +package com.dotcms.concurrent.monitor; + +import com.dotcms.api.system.event.Payload; +import com.dotcms.api.system.event.SystemEvent; +import com.dotcms.api.system.event.SystemEventType; +import com.dotcms.api.system.event.SystemEventsAPI; +import com.dotcms.api.system.event.Visibility; +import com.dotcms.api.system.event.message.MessageSeverity; +import com.dotcms.api.system.event.message.builder.SystemMessage; +import com.dotcms.api.system.event.message.builder.SystemMessageBuilder; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.util.Logger; + +/** + * This is a simple implementation of the TaskMonitor interface that does toast messages of the progress. + * @author jsanca + */ +public class ToastTaskMonitor implements TaskMonitor { + + private final SystemEventsAPI systemEventsAPI = APILocator.getSystemEventsAPI(); + + @Override + public void onTaskStarted(final Object processId, final Object subProcessId) { + + this.sendMessage("Task started, process id: " + processId + " - sub process id:" + subProcessId); + } + + private void sendMessage (final String message) { + + try { + + final SystemMessage systemMessage = new SystemMessageBuilder() + .setMessage(message) + .setLife(15000) // 15 secs for testing + .setSeverity(MessageSeverity.INFO).create(); + this.systemEventsAPI.push(new SystemEvent(SystemEventType.MESSAGE, + new Payload(systemMessage))); + } catch (DotDataException e) { + Logger.error(this.getClass(), "Error pushing system event", e); + } + } + + @Override + public void onTaskProgress(final Object processId, final Object subProcessId, final int progress) { + + this.sendMessage("Task progress, process id: " + processId + " - sub process id:" + subProcessId + " - " + progress + "%"); + } + + @Override + public void onTaskCompleted(final Object processId, final Object subProcessId) { + + this.sendMessage("Task completed, process id: " + processId + " - sub process id:" + subProcessId); + } + + @Override + public void onTaskFailed(final Object processId, final Object subProcessId, final Exception error) { + + this.sendMessage("Task failed, process id: " + processId + " - sub process id:" + subProcessId + " - " + error.getMessage()); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java index 0e027ef132da..a7e4e29acc7b 100644 --- a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java @@ -5,6 +5,7 @@ import com.dotcms.business.CloseDBIfOpened; import com.dotcms.business.WrapInTransaction; import com.dotcms.concurrent.DotConcurrentFactory; +import com.dotcms.concurrent.monitor.TaskMonitor; import com.dotcms.concurrent.lock.IdentifierStripedLock; import com.dotcms.content.elasticsearch.business.event.ContentletArchiveEvent; import com.dotcms.content.elasticsearch.business.event.ContentletCheckinEvent; @@ -193,7 +194,6 @@ import java.io.Serializable; import java.math.BigDecimal; import java.nio.file.Files; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -4911,6 +4911,11 @@ public Contentlet checkinWithoutVersioning(final Contentlet contentlet, false); } + private TaskMonitor getTaskMonitor() { + + final TaskMonitor taskMonitor = ThreadContextUtil.getOrCreateContext().getTaskMonitor(); + return Objects.nonNull(taskMonitor)?taskMonitor: DotConcurrentFactory.getInstance().getTaskMonitor("workflowTaskMonitor"); + } /** * @param contentletIn * @param contentRelationships @@ -4933,9 +4938,12 @@ private Contentlet checkin(final Contentlet contentletIn, Contentlet contentletOut = null; Boolean autoAssign = null; + final Object taskId = DotConcurrentFactory.getInstance().getCurrentTaskId(); + final TaskMonitor taskMonitor = this.getTaskMonitor(); try { + taskMonitor.onTaskStarted(taskId, "CheckinContentlet"); String wfPublishDate = contentletIn.getStringProperty(Contentlet.WORKFLOW_PUBLISH_DATE); String wfExpireDate = contentletIn.getStringProperty(Contentlet.WORKFLOW_EXPIRE_DATE); final boolean isWorkflowInProgress = contentletIn.isWorkflowInProgress(); @@ -5013,6 +5021,8 @@ private Contentlet checkin(final Contentlet contentletIn, return contentletOut; } finally { + + getTaskMonitor().onTaskCompleted(taskId, "CheckinContentlet"); this.cleanup(contentletOut); } } @@ -5238,6 +5248,9 @@ private Contentlet internalCheckin(Contentlet contentlet, "CONTENT_APIS_ALLOW_ANONYMOUS setting does not allow anonymous content WRITEs"); } + final Object taskId = DotConcurrentFactory.getInstance().getCurrentTaskId(); + final TaskMonitor taskMonitor = this.getTaskMonitor(); + if (contentRelationships == null) { //Obtain all relationships @@ -5353,6 +5366,7 @@ private Contentlet internalCheckin(Contentlet contentlet, APILocator.getContentletAPI() .validateContentlet(contentlet, contentRelationships, categories); + taskMonitor.onTaskProgress(taskId, "CheckingContentlet", 25); if (contentlet.getMap().get(Contentlet.DONT_VALIDATE_ME) == null) { canLock(contentlet, user, respectFrontendRoles); } @@ -5402,6 +5416,7 @@ private Contentlet internalCheckin(Contentlet contentlet, //While the original contentlet first will get stuff marked for delete and then refreshed after saved in the database. final Contentlet contentletRaw = populateHost(contentlet); + taskMonitor.onTaskProgress(taskId, "CheckingContentlet", 50); if (contentlet.getMap().get("_use_mod_date") != null) { /* When a content is sent using the remote push publishing we want to respect the modification @@ -5500,6 +5515,7 @@ private Contentlet internalCheckin(Contentlet contentlet, categories, user, respectFrontendRoles); } + taskMonitor.onTaskProgress(taskId, "CheckingContentlet", 75); // Refreshing permissions if (structureHasAHostField && !isNewContent) { permissionAPI.resetPermissionReferences(contentlet); diff --git a/dotCMS/src/main/java/com/dotcms/util/ThreadContext.java b/dotCMS/src/main/java/com/dotcms/util/ThreadContext.java index a9e575c76e97..d6c35c1bd9da 100644 --- a/dotCMS/src/main/java/com/dotcms/util/ThreadContext.java +++ b/dotCMS/src/main/java/com/dotcms/util/ThreadContext.java @@ -1,5 +1,7 @@ package com.dotcms.util; +import com.dotcms.concurrent.monitor.TaskMonitor; + /** * Encapsulates Thread Local context information * @author jsanca @@ -14,6 +16,8 @@ public class ThreadContext { private String tag; + private TaskMonitor taskMonitor; + public boolean isReindex() { return reindex; } @@ -37,4 +41,13 @@ public String getTag() { public void setTag(String tag) { this.tag = tag; } + + public void setTaskMonitor(final TaskMonitor taskMonitor) { + this.taskMonitor = taskMonitor; + } + + public TaskMonitor getTaskMonitor() { + return this.taskMonitor; + } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index 94aed1f18342..1256fbc3ebc3 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -12,6 +12,7 @@ import com.dotcms.business.WrapInTransaction; import com.dotcms.concurrent.DotConcurrentFactory; import com.dotcms.concurrent.DotSubmitter; +import com.dotcms.concurrent.monitor.TaskMonitor; import com.dotcms.content.elasticsearch.business.ContentletIndexAPI; import com.dotcms.contenttype.business.ContentTypeAPI; import com.dotcms.contenttype.model.event.ContentTypeDeletedEvent; @@ -3369,13 +3370,18 @@ public Contentlet fireContentWorkflow(final Contentlet contentlet, final Content ); } + final Object taskId = DotConcurrentFactory.getInstance().getCurrentTaskId(); + final TaskMonitor taskMonitor = DotConcurrentFactory.getInstance().getTaskMonitor("workflowTaskMonitor"); + + ThreadContextUtil.getOrCreateContext().setTaskMonitor(taskMonitor); + taskMonitor.onTaskStarted(taskId, "FireContentWorkflow"); setWorkflowPropertiesToContentlet(contentlet, dependencies); this.validateActionStepAndWorkflow(contentlet, dependencies.getModUser()); this.checkShorties (contentlet); final WorkflowProcessor processor = ThreadContextUtil.wrapReturnNoReindex(()-> this.fireWorkflowPreCheckin(contentlet, dependencies.getModUser())); - + processor.setTaskMonitor(taskMonitor); processor.setContentletDependencies(dependencies); processor.getContentlet().setProperty(Contentlet.WORKFLOW_IN_PROGRESS, Boolean.TRUE); @@ -3389,6 +3395,7 @@ public Contentlet fireContentWorkflow(final Contentlet contentlet, final Content : "Unknown"))); } + taskMonitor.onTaskCompleted(taskId, "FireContentWorkflow"); return processor.getContentlet(); } // fireContentWorkflow diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/model/WorkflowProcessor.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/model/WorkflowProcessor.java index 611b9fb2432f..069caea1008c 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/model/WorkflowProcessor.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/model/WorkflowProcessor.java @@ -3,6 +3,7 @@ import static com.dotmarketing.business.APILocator.getRoleAPI; import static com.dotmarketing.business.APILocator.getWorkflowAPI; +import com.dotcms.concurrent.monitor.TaskMonitor; import com.dotmarketing.business.Role; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; @@ -41,6 +42,7 @@ public class WorkflowProcessor { private final Map contextMap = new HashMap<>(); private ConcurrentMap actionsContext; + private TaskMonitor taskMonitor; /** * True if the processor was aborted @@ -311,4 +313,12 @@ public boolean isRunningBulk(){ public ConcurrentMap getActionsContext() { return actionsContext; } + + public void setTaskMonitor(final TaskMonitor taskMonitor) { + this.taskMonitor = taskMonitor; + } + + public TaskMonitor getTaskMonitor() { + return this.taskMonitor; + } }