Skip to content

Commit 1ed328f

Browse files
committed
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
1 parent 3c8a2ec commit 1ed328f

File tree

5 files changed

+192
-15
lines changed

5 files changed

+192
-15
lines changed

xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<properties>
3535
<!-- Name to display by the Extension Manager -->
3636
<xwiki.extension.name>Scheduler API</xwiki.extension.name>
37-
<xwiki.jacoco.instructionRatio>0.08</xwiki.jacoco.instructionRatio>
37+
<xwiki.jacoco.instructionRatio>0.07</xwiki.jacoco.instructionRatio>
3838
</properties>
3939
<dependencies>
4040
<dependency>
@@ -46,6 +46,11 @@
4646
<groupId>org.quartz-scheduler</groupId>
4747
<artifactId>quartz</artifactId>
4848
</dependency>
49+
<dependency>
50+
<groupId>org.xwiki.commons</groupId>
51+
<artifactId>xwiki-commons-classloader-api</artifactId>
52+
<version>${commons.version}</version>
53+
</dependency>
4954
<dependency>
5055
<groupId>org.xwiki.platform</groupId>
5156
<artifactId>xwiki-platform-test-oldcore</artifactId>

xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/SchedulerPlugin.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import java.net.URL;
2323
import java.util.ArrayList;
24-
import java.util.Arrays;
2524
import java.util.Date;
2625
import java.util.List;
2726
import java.util.Set;
@@ -48,6 +47,8 @@
4847
import org.xwiki.bridge.event.DocumentDeletedEvent;
4948
import org.xwiki.bridge.event.DocumentUpdatedEvent;
5049
import org.xwiki.bridge.event.WikiDeletedEvent;
50+
import org.xwiki.classloader.NamespaceURLClassLoader;
51+
import org.xwiki.classloader.internal.ClassLoaderResetedEvent;
5152
import org.xwiki.configuration.ConfigurationSource;
5253
import org.xwiki.context.concurrent.ExecutionContextRunnable;
5354
import org.xwiki.model.reference.DocumentReference;
@@ -68,6 +69,7 @@
6869
import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer;
6970
import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobsInitializedEvent;
7071
import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobsInitializingEvent;
72+
import com.xpn.xwiki.plugin.scheduler.internal.SchedulersClassLoaderManager;
7173
import com.xpn.xwiki.plugin.scheduler.internal.StatusListener;
7274
import com.xpn.xwiki.web.Utils;
7375
import com.xpn.xwiki.web.XWikiResponse;
@@ -102,8 +104,13 @@ public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener
102104
public static final EntityReference XWIKI_JOB_CLASSREFERENCE =
103105
SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE;
104106

105-
private static final List<Event> EVENTS = Arrays.<Event>asList(new DocumentCreatedEvent(),
106-
new DocumentDeletedEvent(), new DocumentUpdatedEvent(), new WikiDeletedEvent());
107+
private static final List<Event> EVENTS = List.of(
108+
new DocumentCreatedEvent(),
109+
new DocumentDeletedEvent(),
110+
new DocumentUpdatedEvent(),
111+
new WikiDeletedEvent(),
112+
new ClassLoaderResetedEvent()
113+
);
107114

108115
/**
109116
* Default Quartz scheduler instance.
@@ -112,6 +119,8 @@ public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener
112119

113120
private boolean enabled;
114121

122+
private SchedulersClassLoaderManager schedulersClassLoaderManager;
123+
115124
/**
116125
* Default plugin constructor.
117126
*
@@ -128,6 +137,8 @@ public void init(XWikiContext context)
128137
// Check if the Scheduler plugin is enabled
129138
this.enabled =
130139
Utils.getComponent(ConfigurationSource.class, "xwikiproperties").getProperty("scheduler.enabled", true);
140+
this.schedulersClassLoaderManager = Utils.getComponent(SchedulersClassLoaderManager.class);
141+
this.schedulersClassLoaderManager.setSchedulerPlugin(this);
131142

132143
if (this.enabled) {
133144
Thread thread = new Thread(new ExecutionContextRunnable(new Runnable()
@@ -397,13 +408,9 @@ public boolean scheduleJob(BaseObject object, XWikiContext context) throws Sched
397408
try {
398409
// compute the job unique Id
399410
String xjob = getObjectUniqueId(object);
400-
401-
// Load the job class.
402-
// Note: Remember to always use the current thread's class loader and not the container's
403-
// (Class.forName(...)) since otherwise we will not be able to load classes installed with EM.
404-
ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader();
405-
String jobClassName = object.getStringValue("jobClass");
406-
Class<Job> jobClass = (Class<Job>) Class.forName(jobClassName, true, currentThreadClassLoader);
411+
String jobClassName = object.getStringValue(SchedulerJobClassDocumentInitializer.FIELD_JOBCLASS);
412+
Class<Job> jobClass = (Class<Job>) this.schedulersClassLoaderManager
413+
.loadClassAndRegister(jobClassName, object.getReference());
407414

408415
// Build the new job.
409416
JobBuilder jobBuilder = JobBuilder.newJob(jobClass);
@@ -570,6 +577,7 @@ private void deleteJob(BaseObject object) throws SchedulerPluginException
570577
throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_PAUSE_JOB,
571578
"Error occured while trying to pause job " + object.getStringValue("jobName"), e);
572579
}
580+
this.schedulersClassLoaderManager.removeScheduler(object.getReference());
573581
}
574582

575583
/**
@@ -733,13 +741,17 @@ public List<Event> getEvents()
733741
@Override
734742
public void onEvent(Event event, Object source, Object data)
735743
{
736-
if (event instanceof WikiDeletedEvent) {
737-
String wikiId = ((WikiDeletedEvent) event).getWikiId();
744+
if (event instanceof WikiDeletedEvent wikiDeletedEvent) {
745+
String wikiId = wikiDeletedEvent.getWikiId();
738746
try {
739747
onWikiDeletedEvent(wikiId);
740748
} catch (SchedulerException e) {
741749
LOGGER.error("Failed to remove schedulers for wiki [{}]", wikiId, e);
742750
}
751+
this.schedulersClassLoaderManager.removeSchedulers(wikiId);
752+
} else if (event instanceof ClassLoaderResetedEvent classLoaderResetedEvent) {
753+
String namespace = (String) source;
754+
this.schedulersClassLoaderManager.onClassLoaderReset(namespace);
743755
} else {
744756
onDocumentEvent(source, data);
745757
}

xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/internal/SchedulerJobClassDocumentInitializer.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,15 @@ public class SchedulerJobClassDocumentInitializer extends AbstractMandatoryClass
5656
public static final LocalDocumentReference XWIKI_JOB_CLASSREFERENCE =
5757
new LocalDocumentReference(XWiki.SYSTEM_SPACE, "SchedulerJobClass");
5858

59+
/**
60+
* Field containing the class name of the job.
61+
*/
62+
public static final String FIELD_JOBCLASS = "jobClass";
63+
5964
private static final String FIELD_JOBNAME = "jobName";
6065

6166
private static final String FIELD_JOBDESCRIPTION = "jobDescription";
6267

63-
private static final String FIELD_JOBCLASS = "jobClass";
64-
6568
private static final String FIELD_STATUS = "status";
6669

6770
private static final String FIELD_CRON = "cron";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* See the NOTICE file distributed with this work for additional
3+
* information regarding copyright ownership.
4+
*
5+
* This is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU Lesser General Public License as
7+
* published by the Free Software Foundation; either version 2.1 of
8+
* the License, or (at your option) any later version.
9+
*
10+
* This software is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this software; if not, write to the Free
17+
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18+
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19+
*/
20+
package com.xpn.xwiki.plugin.scheduler.internal;
21+
22+
import java.util.HashMap;
23+
import java.util.HashSet;
24+
import java.util.Map;
25+
import java.util.Set;
26+
27+
import javax.inject.Inject;
28+
import javax.inject.Provider;
29+
import javax.inject.Singleton;
30+
31+
import org.slf4j.Logger;
32+
import org.xwiki.classloader.ClassLoaderManager;
33+
import org.xwiki.classloader.NamespaceURLClassLoader;
34+
import org.xwiki.component.annotation.Component;
35+
import org.xwiki.model.EntityType;
36+
import org.xwiki.model.reference.EntityReference;
37+
38+
import com.xpn.xwiki.XWikiContext;
39+
import com.xpn.xwiki.XWikiException;
40+
import com.xpn.xwiki.doc.XWikiDocument;
41+
import com.xpn.xwiki.objects.BaseObject;
42+
import com.xpn.xwiki.objects.BaseObjectReference;
43+
import com.xpn.xwiki.plugin.scheduler.SchedulerPlugin;
44+
45+
/**
46+
* Component dedicated to handle operations related to loading classes for Scheduler.
47+
*
48+
* @version $Id$
49+
* @since 17.10.1
50+
* @since 18.0.0RC1
51+
*/
52+
@Component(roles = SchedulersClassLoaderManager.class)
53+
@Singleton
54+
public class SchedulersClassLoaderManager
55+
{
56+
private SchedulerPlugin schedulerPlugin;
57+
58+
private final Map<String, Set<BaseObjectReference>> schedulersMapPerNamespace = new HashMap<>();
59+
60+
@Inject
61+
private Provider<XWikiContext> contextProvider;
62+
63+
@Inject
64+
private Logger logger;
65+
66+
@Inject
67+
private ClassLoaderManager classLoaderManager;
68+
69+
/**
70+
* Define the instance of the scheduler plugin to use.
71+
* @param schedulerPlugin the scheduler plugin instance this instance should use.
72+
*/
73+
public void setSchedulerPlugin(SchedulerPlugin schedulerPlugin)
74+
{
75+
this.schedulerPlugin = schedulerPlugin;
76+
}
77+
78+
private void registerScheduler(String namespace, BaseObjectReference objectReference)
79+
{
80+
if (!this.schedulersMapPerNamespace.containsKey(namespace)) {
81+
this.schedulersMapPerNamespace.put(namespace, new HashSet<>());
82+
}
83+
this.schedulersMapPerNamespace.get(namespace).add(objectReference);
84+
}
85+
86+
/**
87+
* Remove scheduler information related to given object reference.
88+
* @param objectReference the reference of a scheduler object.
89+
*/
90+
public void removeScheduler(BaseObjectReference objectReference)
91+
{
92+
for (Set<BaseObjectReference> objectReferenceSet : this.schedulersMapPerNamespace.values()) {
93+
objectReferenceSet.remove(objectReference);
94+
}
95+
}
96+
97+
/**
98+
* Remove all schedulers information associated to a namespace.
99+
* @param namespace the namespace for which to remove information.
100+
*/
101+
public void removeSchedulers(String namespace)
102+
{
103+
this.schedulersMapPerNamespace.remove(namespace);
104+
}
105+
106+
/**
107+
* Perform operations when a classloader of a specific namespace is reset.
108+
* @param namespace the namespace for which an event has been triggered.
109+
*/
110+
public void onClassLoaderReset(String namespace)
111+
{
112+
schedulersMapPerNamespace
113+
.getOrDefault(namespace, Set.of())
114+
.parallelStream()
115+
.forEach(this::reloadScheduler);
116+
}
117+
118+
private void reloadScheduler(BaseObjectReference objectReference)
119+
{
120+
XWikiContext context = contextProvider.get();
121+
EntityReference documentReference = objectReference.extractReference(EntityType.DOCUMENT);
122+
try {
123+
XWikiDocument document = context.getWiki().getDocument(documentReference, context);
124+
BaseObject jobObject = document.getXObject(SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE);
125+
this.schedulerPlugin.unscheduleJob(jobObject, context);
126+
this.schedulerPlugin.scheduleJob(jobObject, context);
127+
} catch (XWikiException e) {
128+
this.logger.error("Error while trying to reload scheduler for object [{}]: ", objectReference, e);
129+
}
130+
}
131+
132+
/**
133+
* Load a class for a scheduler and register it at the same time.
134+
* @param className the name of the class to load.
135+
* @param baseObjectReference the reference of the object of the scheduler.
136+
* @return the instance of the given class name.
137+
* @throws ClassNotFoundException if the class cannot be found.
138+
*/
139+
public Class<?> loadClassAndRegister(String className, BaseObjectReference baseObjectReference)
140+
throws ClassNotFoundException
141+
{
142+
String namespace = null;
143+
144+
// Reload the root classloader if needed: it's important if it's been dropped.
145+
NamespaceURLClassLoader classLoader = this.classLoaderManager.getURLClassLoader(null, true);
146+
Class<?> result = Class.forName(className, true, classLoader);
147+
148+
// find the actual namespace of the classloader from where the class has been found.
149+
if (result.getClassLoader() instanceof NamespaceURLClassLoader namespaceURLClassLoader) {
150+
namespace = namespaceURLClassLoader.getNamespace();
151+
}
152+
153+
this.registerScheduler(namespace, baseObjectReference);
154+
return result;
155+
}
156+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer
2+
com.xpn.xwiki.plugin.scheduler.internal.SchedulersClassLoaderManager

0 commit comments

Comments
 (0)