From 061a64a6e862c9c9fdef11f1b5668a542bfc27e4 Mon Sep 17 00:00:00 2001 From: Simon Urli Date: Tue, 2 Dec 2025 17:44:28 +0100 Subject: [PATCH 1/4] XWIKI-14494: Java scheduler job coming from an extension is not rescheduled when the extension is upgraded * Handle properly classloader reload in Scheduler plugin with a dedicated component --- .../xwiki-platform-scheduler-api/pom.xml | 7 +- .../plugin/scheduler/SchedulerPlugin.java | 36 ++-- .../SchedulerJobClassDocumentInitializer.java | 7 +- .../SchedulersClassLoaderManager.java | 156 ++++++++++++++++++ .../main/resources/META-INF/components.txt | 1 + 5 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/pom.xml b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/pom.xml index 71028f0b8a4b..abc152b976fe 100644 --- a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/pom.xml +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/pom.xml @@ -34,7 +34,7 @@ Scheduler API - 0.08 + 0.07 @@ -46,6 +46,11 @@ org.quartz-scheduler quartz + + org.xwiki.commons + xwiki-commons-classloader-api + ${commons.version} + org.xwiki.platform xwiki-platform-test-oldcore diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java index 0f7fe23cda1e..b223edf3007d 100644 --- a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java @@ -21,7 +21,6 @@ import java.net.URL; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Set; @@ -48,6 +47,8 @@ import org.xwiki.bridge.event.DocumentDeletedEvent; import org.xwiki.bridge.event.DocumentUpdatedEvent; import org.xwiki.bridge.event.WikiDeletedEvent; +import org.xwiki.classloader.NamespaceURLClassLoader; +import org.xwiki.classloader.internal.ClassLoaderResetedEvent; import org.xwiki.configuration.ConfigurationSource; import org.xwiki.context.concurrent.ExecutionContextRunnable; import org.xwiki.model.reference.DocumentReference; @@ -68,6 +69,7 @@ import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer; import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobsInitializedEvent; import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobsInitializingEvent; +import com.xpn.xwiki.plugin.scheduler.internal.SchedulersClassLoaderManager; import com.xpn.xwiki.plugin.scheduler.internal.StatusListener; import com.xpn.xwiki.web.Utils; import com.xpn.xwiki.web.XWikiResponse; @@ -102,8 +104,13 @@ public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener public static final EntityReference XWIKI_JOB_CLASSREFERENCE = SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE; - private static final List EVENTS = Arrays.asList(new DocumentCreatedEvent(), - new DocumentDeletedEvent(), new DocumentUpdatedEvent(), new WikiDeletedEvent()); + private static final List EVENTS = List.of( + new DocumentCreatedEvent(), + new DocumentDeletedEvent(), + new DocumentUpdatedEvent(), + new WikiDeletedEvent(), + new ClassLoaderResetedEvent() + ); /** * Default Quartz scheduler instance. @@ -112,6 +119,8 @@ public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener private boolean enabled; + private SchedulersClassLoaderManager schedulersClassLoaderManager; + /** * Default plugin constructor. * @@ -128,6 +137,8 @@ public void init(XWikiContext context) // Check if the Scheduler plugin is enabled this.enabled = Utils.getComponent(ConfigurationSource.class, "xwikiproperties").getProperty("scheduler.enabled", true); + this.schedulersClassLoaderManager = Utils.getComponent(SchedulersClassLoaderManager.class); + this.schedulersClassLoaderManager.setSchedulerPlugin(this); if (this.enabled) { Thread thread = new Thread(new ExecutionContextRunnable(new Runnable() @@ -397,13 +408,9 @@ public boolean scheduleJob(BaseObject object, XWikiContext context) throws Sched try { // compute the job unique Id String xjob = getObjectUniqueId(object); - - // Load the job class. - // Note: Remember to always use the current thread's class loader and not the container's - // (Class.forName(...)) since otherwise we will not be able to load classes installed with EM. - ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader(); - String jobClassName = object.getStringValue("jobClass"); - Class jobClass = (Class) Class.forName(jobClassName, true, currentThreadClassLoader); + String jobClassName = object.getStringValue(SchedulerJobClassDocumentInitializer.FIELD_JOBCLASS); + Class jobClass = (Class) this.schedulersClassLoaderManager + .loadClassAndRegister(jobClassName, object.getReference()); // Build the new job. JobBuilder jobBuilder = JobBuilder.newJob(jobClass); @@ -570,6 +577,7 @@ private void deleteJob(BaseObject object) throws SchedulerPluginException throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_PAUSE_JOB, "Error occured while trying to pause job " + object.getStringValue("jobName"), e); } + this.schedulersClassLoaderManager.removeScheduler(object.getReference()); } /** @@ -733,13 +741,17 @@ public List getEvents() @Override public void onEvent(Event event, Object source, Object data) { - if (event instanceof WikiDeletedEvent) { - String wikiId = ((WikiDeletedEvent) event).getWikiId(); + if (event instanceof WikiDeletedEvent wikiDeletedEvent) { + String wikiId = wikiDeletedEvent.getWikiId(); try { onWikiDeletedEvent(wikiId); } catch (SchedulerException e) { LOGGER.error("Failed to remove schedulers for wiki [{}]", wikiId, e); } + this.schedulersClassLoaderManager.removeSchedulers(wikiId); + } else if (event instanceof ClassLoaderResetedEvent classLoaderResetedEvent) { + String namespace = (String) source; + this.schedulersClassLoaderManager.onClassLoaderReset(namespace); } else { onDocumentEvent(source, data); } diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulerJobClassDocumentInitializer.java b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulerJobClassDocumentInitializer.java index d884477c2007..5d4b6b7beb98 100644 --- a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulerJobClassDocumentInitializer.java +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulerJobClassDocumentInitializer.java @@ -56,12 +56,15 @@ public class SchedulerJobClassDocumentInitializer extends AbstractMandatoryClass public static final LocalDocumentReference XWIKI_JOB_CLASSREFERENCE = new LocalDocumentReference(XWiki.SYSTEM_SPACE, "SchedulerJobClass"); + /** + * Field containing the class name of the job. + */ + public static final String FIELD_JOBCLASS = "jobClass"; + private static final String FIELD_JOBNAME = "jobName"; private static final String FIELD_JOBDESCRIPTION = "jobDescription"; - private static final String FIELD_JOBCLASS = "jobClass"; - private static final String FIELD_STATUS = "status"; private static final String FIELD_CRON = "cron"; diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java new file mode 100644 index 000000000000..34db2e2aa2d6 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java @@ -0,0 +1,156 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xpn.xwiki.plugin.scheduler.internal; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.slf4j.Logger; +import org.xwiki.classloader.ClassLoaderManager; +import org.xwiki.classloader.NamespaceURLClassLoader; +import org.xwiki.component.annotation.Component; +import org.xwiki.model.EntityType; +import org.xwiki.model.reference.EntityReference; + +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; +import com.xpn.xwiki.objects.BaseObjectReference; +import com.xpn.xwiki.plugin.scheduler.SchedulerPlugin; + +/** + * Component dedicated to handle operations related to loading classes for Scheduler. + * + * @version $Id$ + * @since 17.10.1 + * @since 18.0.0RC1 + */ +@Component(roles = SchedulersClassLoaderManager.class) +@Singleton +public class SchedulersClassLoaderManager +{ + private SchedulerPlugin schedulerPlugin; + + private final Map> schedulersMapPerNamespace = new HashMap<>(); + + @Inject + private Provider contextProvider; + + @Inject + private Logger logger; + + @Inject + private ClassLoaderManager classLoaderManager; + + /** + * Define the instance of the scheduler plugin to use. + * @param schedulerPlugin the scheduler plugin instance this instance should use. + */ + public void setSchedulerPlugin(SchedulerPlugin schedulerPlugin) + { + this.schedulerPlugin = schedulerPlugin; + } + + private void registerScheduler(String namespace, BaseObjectReference objectReference) + { + if (!this.schedulersMapPerNamespace.containsKey(namespace)) { + this.schedulersMapPerNamespace.put(namespace, new HashSet<>()); + } + this.schedulersMapPerNamespace.get(namespace).add(objectReference); + } + + /** + * Remove scheduler information related to given object reference. + * @param objectReference the reference of a scheduler object. + */ + public void removeScheduler(BaseObjectReference objectReference) + { + for (Set objectReferenceSet : this.schedulersMapPerNamespace.values()) { + objectReferenceSet.remove(objectReference); + } + } + + /** + * Remove all schedulers information associated to a namespace. + * @param namespace the namespace for which to remove information. + */ + public void removeSchedulers(String namespace) + { + this.schedulersMapPerNamespace.remove(namespace); + } + + /** + * Perform operations when a classloader of a specific namespace is reset. + * @param namespace the namespace for which an event has been triggered. + */ + public void onClassLoaderReset(String namespace) + { + schedulersMapPerNamespace + .getOrDefault(namespace, Set.of()) + .parallelStream() + .forEach(this::reloadScheduler); + } + + private void reloadScheduler(BaseObjectReference objectReference) + { + XWikiContext context = contextProvider.get(); + EntityReference documentReference = objectReference.extractReference(EntityType.DOCUMENT); + try { + XWikiDocument document = context.getWiki().getDocument(documentReference, context); + BaseObject jobObject = document.getXObject(SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE); + this.schedulerPlugin.unscheduleJob(jobObject, context); + this.schedulerPlugin.scheduleJob(jobObject, context); + } catch (XWikiException e) { + this.logger.error("Error while trying to reload scheduler for object [{}]: ", objectReference, e); + } + } + + /** + * Load a class for a scheduler and register it at the same time. + * @param className the name of the class to load. + * @param baseObjectReference the reference of the object of the scheduler. + * @return the instance of the given class name. + * @throws ClassNotFoundException if the class cannot be found. + */ + public Class loadClassAndRegister(String className, BaseObjectReference baseObjectReference) + throws ClassNotFoundException + { + String namespace = null; + + // Reload the root classloader if needed: it's important if it's been dropped. + NamespaceURLClassLoader classLoader = this.classLoaderManager.getURLClassLoader(null, true); + Class result = Class.forName(className, true, classLoader); + + // find the actual namespace of the classloader from where the class has been found. + if (result.getClassLoader() instanceof NamespaceURLClassLoader namespaceURLClassLoader) { + namespace = namespaceURLClassLoader.getNamespace(); + } + + this.registerScheduler(namespace, baseObjectReference); + return result; + } +} diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/resources/META-INF/components.txt index 84d4d5a99345..749d53b765c0 100644 --- a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/resources/META-INF/components.txt +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/resources/META-INF/components.txt @@ -1 +1,2 @@ com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer +com.xpn.xwiki.plugin.scheduler.internal.SchedulersClassLoaderManager From db3c380a42ede0b670489da559676b104a6558cd Mon Sep 17 00:00:00 2001 From: Simon Urli Date: Wed, 3 Dec 2025 11:20:09 +0100 Subject: [PATCH 2/4] XWIKI-14494: Java scheduler job coming from an extension is not rescheduled when the extension is upgraded * Use proper event --- .../xpn/xwiki/plugin/scheduler/SchedulerPlugin.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java index b223edf3007d..2d97a65883c5 100644 --- a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java @@ -47,8 +47,7 @@ import org.xwiki.bridge.event.DocumentDeletedEvent; import org.xwiki.bridge.event.DocumentUpdatedEvent; import org.xwiki.bridge.event.WikiDeletedEvent; -import org.xwiki.classloader.NamespaceURLClassLoader; -import org.xwiki.classloader.internal.ClassLoaderResetedEvent; +import org.xwiki.classloader.internal.ClassLoaderResetEvent; import org.xwiki.configuration.ConfigurationSource; import org.xwiki.context.concurrent.ExecutionContextRunnable; import org.xwiki.model.reference.DocumentReference; @@ -109,7 +108,7 @@ public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener new DocumentDeletedEvent(), new DocumentUpdatedEvent(), new WikiDeletedEvent(), - new ClassLoaderResetedEvent() + new ClassLoaderResetEvent() ); /** @@ -748,9 +747,9 @@ public void onEvent(Event event, Object source, Object data) } catch (SchedulerException e) { LOGGER.error("Failed to remove schedulers for wiki [{}]", wikiId, e); } - this.schedulersClassLoaderManager.removeSchedulers(wikiId); - } else if (event instanceof ClassLoaderResetedEvent classLoaderResetedEvent) { - String namespace = (String) source; + this.schedulersClassLoaderManager.removeSchedulers(String.format("wiki:%s", wikiId)); + } else if (event instanceof ClassLoaderResetEvent classLoaderResetEvent) { + String namespace = classLoaderResetEvent.getNamespace(); this.schedulersClassLoaderManager.onClassLoaderReset(namespace); } else { onDocumentEvent(source, data); From 4c97ba488be2715d0fdd676ffe924f539d17a141 Mon Sep 17 00:00:00 2001 From: Simon Urli Date: Wed, 3 Dec 2025 11:55:15 +0100 Subject: [PATCH 3/4] XWIKI-14494: Java scheduler job coming from an extension is not rescheduled when the extension is upgraded * use WikiNamespace --- .../java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java index 2d97a65883c5..058dbad1215e 100644 --- a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java @@ -50,6 +50,7 @@ import org.xwiki.classloader.internal.ClassLoaderResetEvent; import org.xwiki.configuration.ConfigurationSource; import org.xwiki.context.concurrent.ExecutionContextRunnable; +import org.xwiki.model.namespace.WikiNamespace; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.EntityReference; import org.xwiki.observation.EventListener; @@ -747,7 +748,7 @@ public void onEvent(Event event, Object source, Object data) } catch (SchedulerException e) { LOGGER.error("Failed to remove schedulers for wiki [{}]", wikiId, e); } - this.schedulersClassLoaderManager.removeSchedulers(String.format("wiki:%s", wikiId)); + this.schedulersClassLoaderManager.removeSchedulers(new WikiNamespace(wikiId).serialize()); } else if (event instanceof ClassLoaderResetEvent classLoaderResetEvent) { String namespace = classLoaderResetEvent.getNamespace(); this.schedulersClassLoaderManager.onClassLoaderReset(namespace); From ece7e322fe5e603008ef074d22d86260194bacd5 Mon Sep 17 00:00:00 2001 From: Simon Urli Date: Tue, 9 Dec 2025 16:01:49 +0100 Subject: [PATCH 4/4] XWIKI-14494: Java scheduler job coming from an extension is not rescheduled when the extension is upgraded * Fix bad handling of iteration --- .../scheduler/internal/SchedulersClassLoaderManager.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java index 34db2e2aa2d6..d9fbad49c69d 100644 --- a/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java +++ b/xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulersClassLoaderManager.java @@ -109,10 +109,11 @@ public void removeSchedulers(String namespace) */ public void onClassLoaderReset(String namespace) { - schedulersMapPerNamespace - .getOrDefault(namespace, Set.of()) - .parallelStream() - .forEach(this::reloadScheduler); + Set objectReferences = + new HashSet<>(schedulersMapPerNamespace.getOrDefault(namespace, Set.of())); + for (BaseObjectReference objectReference : objectReferences) { + this.reloadScheduler(objectReference); + } } private void reloadScheduler(BaseObjectReference objectReference)