Skip to content

Commit 45c86cc

Browse files
manovotngsmet
authored andcommitted
Quartz - prevent memory leak when Job instance is a @dependent bean
(cherry picked from commit 6dcfaca)
1 parent ca7c1bd commit 45c86cc

File tree

3 files changed

+164
-2
lines changed

3 files changed

+164
-2
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package io.quarkus.quartz.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.util.concurrent.CountDownLatch;
7+
import java.util.concurrent.TimeUnit;
8+
9+
import jakarta.annotation.PostConstruct;
10+
import jakarta.annotation.PreDestroy;
11+
import jakarta.enterprise.context.ApplicationScoped;
12+
import jakarta.enterprise.context.Dependent;
13+
import jakarta.inject.Inject;
14+
15+
import org.jboss.shrinkwrap.api.asset.StringAsset;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.RegisterExtension;
18+
import org.quartz.Job;
19+
import org.quartz.JobBuilder;
20+
import org.quartz.JobDetail;
21+
import org.quartz.JobExecutionContext;
22+
import org.quartz.JobExecutionException;
23+
import org.quartz.Scheduler;
24+
import org.quartz.SchedulerException;
25+
import org.quartz.SimpleScheduleBuilder;
26+
import org.quartz.Trigger;
27+
import org.quartz.TriggerBuilder;
28+
29+
import io.quarkus.test.QuarkusUnitTest;
30+
31+
public class DependentBeanJobTest {
32+
33+
@RegisterExtension
34+
static final QuarkusUnitTest test = new QuarkusUnitTest()
35+
.withApplicationRoot((jar) -> jar
36+
.addClasses(Service.class, MyJob.class)
37+
.addAsResource(new StringAsset("quarkus.quartz.start-mode=forced"),
38+
"application.properties"));
39+
40+
@Inject
41+
Scheduler quartz;
42+
43+
@Inject
44+
Service service;
45+
46+
@Test
47+
public void testDependentBeanJobDestroyed() throws SchedulerException, InterruptedException {
48+
assertEquals(0, MyJob.timesConstructed);
49+
assertEquals(0, MyJob.timesDestroyed);
50+
// prepare latch, schedule 10 one-off jobs, assert
51+
CountDownLatch latch = service.initializeLatch(10);
52+
for (int i = 0; i < 10; i++) {
53+
Trigger trigger = TriggerBuilder.newTrigger()
54+
.withIdentity("myTrigger" + i, "myGroup")
55+
.startNow()
56+
.build();
57+
JobDetail job = JobBuilder.newJob(MyJob.class)
58+
.withIdentity("myJob" + i, "myGroup")
59+
.build();
60+
quartz.scheduleJob(job, trigger);
61+
}
62+
assertTrue(latch.await(5, TimeUnit.SECONDS), "Latch count: " + latch.getCount());
63+
assertEquals(10, MyJob.timesConstructed);
64+
assertEquals(10, MyJob.timesDestroyed);
65+
66+
// now try the same with repeating job triggering three times
67+
latch = service.initializeLatch(3);
68+
JobDetail job = JobBuilder.newJob(MyJob.class)
69+
.withIdentity("myRepeatingJob", "myGroup")
70+
.build();
71+
Trigger trigger = TriggerBuilder.newTrigger()
72+
.withIdentity("myRepeatingTrigger", "myGroup")
73+
.startNow()
74+
.withSchedule(
75+
SimpleScheduleBuilder.simpleSchedule()
76+
.withIntervalInMilliseconds(333)
77+
.withRepeatCount(3))
78+
.build();
79+
quartz.scheduleJob(job, trigger);
80+
81+
assertTrue(latch.await(2, TimeUnit.SECONDS), "Latch count: " + latch.getCount());
82+
assertEquals(13, MyJob.timesConstructed);
83+
assertEquals(13, MyJob.timesDestroyed);
84+
}
85+
86+
@ApplicationScoped
87+
public static class Service {
88+
89+
volatile CountDownLatch latch;
90+
91+
public CountDownLatch initializeLatch(int latchCountdown) {
92+
this.latch = new CountDownLatch(latchCountdown);
93+
return latch;
94+
}
95+
96+
public void execute() {
97+
latch.countDown();
98+
}
99+
100+
}
101+
102+
@Dependent
103+
static class MyJob implements Job {
104+
105+
public static volatile int timesConstructed = 0;
106+
public static volatile int timesDestroyed = 0;
107+
108+
@Inject
109+
Service service;
110+
111+
@PostConstruct
112+
void postConstruct() {
113+
timesConstructed++;
114+
}
115+
116+
@PreDestroy
117+
void preDestroy() {
118+
timesDestroyed++;
119+
}
120+
121+
@Override
122+
public void execute(JobExecutionContext context) throws JobExecutionException {
123+
service.execute();
124+
}
125+
}
126+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.quarkus.quartz.runtime;
2+
3+
import jakarta.enterprise.context.Dependent;
4+
import jakarta.enterprise.inject.Instance;
5+
6+
import org.quartz.Job;
7+
import org.quartz.JobExecutionContext;
8+
import org.quartz.JobExecutionException;
9+
import org.quartz.Scheduler;
10+
import org.quartz.spi.TriggerFiredBundle;
11+
12+
/**
13+
* An abstraction allowing proper destruction of Job instances in case they are dependent beans.
14+
* According to {@link org.quartz.spi.JobFactory#newJob(TriggerFiredBundle, Scheduler)}, a new job instance is created for every
15+
* trigger.
16+
* We will therefore create a new dependent bean for every trigger and destroy it afterwards.
17+
*/
18+
class CdiAwareJob implements Job {
19+
20+
private final Instance.Handle<? extends Job> jobInstanceHandle;
21+
22+
public CdiAwareJob(Instance.Handle<? extends Job> jobInstanceHandle) {
23+
this.jobInstanceHandle = jobInstanceHandle;
24+
}
25+
26+
@Override
27+
public void execute(JobExecutionContext context) throws JobExecutionException {
28+
try {
29+
jobInstanceHandle.get().execute(context);
30+
} finally {
31+
if (jobInstanceHandle.getBean().getScope().equals(Dependent.class)) {
32+
jobInstanceHandle.destroy();
33+
}
34+
}
35+
}
36+
}

extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,10 +1243,10 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler Scheduler) thr
12431243
// Get the original class from an intercepted bean class
12441244
jobClass = (Class<? extends Job>) jobClass.getSuperclass();
12451245
}
1246-
Instance<?> instance = jobs.select(jobClass);
1246+
Instance<? extends Job> instance = jobs.select(jobClass);
12471247
if (instance.isResolvable()) {
12481248
// This is a job backed by a CDI bean
1249-
return jobWithSpanWrapper((Job) instance.get());
1249+
return jobWithSpanWrapper(new CdiAwareJob(instance.getHandle()));
12501250
}
12511251
// Instantiate a plain job class
12521252
return jobWithSpanWrapper(super.newJob(bundle, Scheduler));

0 commit comments

Comments
 (0)