1 package com.atlassian.scheduler.quartz1;
2
3 import org.junit.Before;
4 import org.junit.Rule;
5 import org.junit.Test;
6 import org.mockito.ArgumentCaptor;
7 import org.mockito.Captor;
8 import org.mockito.Mock;
9 import org.mockito.invocation.InvocationOnMock;
10 import org.mockito.junit.MockitoJUnit;
11 import org.mockito.junit.MockitoRule;
12 import org.mockito.stubbing.Answer;
13 import org.quartz.JobDetail;
14 import org.quartz.JobPersistenceException;
15 import org.quartz.Trigger;
16 import org.quartz.core.SchedulingContext;
17 import org.quartz.impl.jdbcjobstore.DriverDelegate;
18 import org.quartz.impl.jdbcjobstore.NoSuchDelegateException;
19 import org.quartz.spi.ClassLoadHelper;
20 import org.quartz.utils.Key;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
23
24 import javax.annotation.Nullable;
25 import java.sql.Connection;
26 import java.util.List;
27 import java.util.concurrent.atomic.AtomicInteger;
28
29 import static org.hamcrest.Matchers.containsString;
30 import static org.junit.Assert.assertEquals;
31 import static org.junit.Assert.assertThat;
32 import static org.junit.Assert.fail;
33 import static org.mockito.Matchers.any;
34 import static org.mockito.Matchers.anyInt;
35 import static org.mockito.Matchers.anyLong;
36 import static org.mockito.Matchers.eq;
37 import static org.mockito.Mockito.when;
38 import static org.quartz.impl.jdbcjobstore.Constants.STATE_MISFIRED;
39 import static org.quartz.impl.jdbcjobstore.Constants.STATE_WAITING;
40
41 public class Quartz1HardenedJobStoreTest {
42 static final Logger LOG = LoggerFactory.getLogger(Quartz1HardenedJobStoreTest.class);
43
44 final AtomicInteger callCount = new AtomicInteger();
45 final AtomicInteger throwCount = new AtomicInteger();
46 final AtomicInteger escapeCount = new AtomicInteger();
47
48 @Rule
49 public MockitoRule mockitoRule = MockitoJUnit.rule();
50
51 @Mock
52 Connection conn;
53 @Mock
54 DriverDelegate delegate;
55 @Mock
56 Key key1;
57 @Mock
58 Key key2;
59 @Mock
60 Trigger trigger1;
61 @Mock
62 Trigger trigger2;
63 @Mock
64 JobDetail job1;
65 @Mock
66 JobDetail job2;
67
68 @Captor
69 ArgumentCaptor<List<Key>> misfired;
70
71 @Before
72 public void setUp() throws Exception {
73 when(key1.getGroup()).thenReturn("key1group");
74 when(key1.getName()).thenReturn("key1name");
75 when(key2.getGroup()).thenReturn("key2group");
76 when(key2.getName()).thenReturn("key2name");
77 when(trigger1.getJobGroup()).thenReturn("job1group");
78 when(trigger1.getJobName()).thenReturn("job1name");
79 when(trigger2.getJobGroup()).thenReturn("job2group");
80 when(trigger2.getJobName()).thenReturn("job2name");
81
82 when(delegate.selectTriggersForRecoveringJobs(conn)).thenReturn(new Trigger[]{trigger1, trigger2});
83 when(delegate.selectJobDetail(eq(conn), eq("job1name"), eq("job1group"), any(ClassLoadHelper.class)))
84 .thenAnswer(new Answer<JobDetail>() {
85 @Override
86 public JobDetail answer(InvocationOnMock invocation) throws Throwable {
87 throwCount.incrementAndGet();
88 throw new ClassNotFoundException("He's DEAD, Jim!");
89 }
90 });
91 when(delegate.selectJobDetail(eq(conn), eq("job2name"), eq("job2group"), any(ClassLoadHelper.class)))
92 .thenReturn(job2);
93 }
94
95 @Test
96 public void testStoreTriggerThrowsExceptionOutsideOfRecovery() throws JobPersistenceException {
97 final Fixture fixture = new Fixture();
98 try {
99 fixture.storeTrigger(conn, new SchedulingContext(), trigger1, null, true, STATE_MISFIRED, true, true);
100 fail("Should have thrown JobPersistenceException");
101 } catch (JobPersistenceException jpe) {
102 assertThat(jpe.getMessage(), containsString("He's DEAD, Jim!"));
103 assertEquals("callCount", 1, callCount.get());
104 assertEquals("throwCount", 1, throwCount.get());
105 assertEquals("escapeCount", 1, escapeCount.get());
106 }
107 }
108
109 @Test
110 public void testStoreTriggerDoesNotThrowExceptionFromRecovery() throws Exception {
111 when(delegate.selectMisfiredTriggersInStates(
112 eq(conn), eq(STATE_MISFIRED), eq(STATE_WAITING), anyLong(),
113 anyInt(), misfired.capture())).thenAnswer(new Answer<Boolean>() {
114 @Override
115 public Boolean answer(InvocationOnMock invocation) throws Throwable {
116 final List<Key> keys = misfired.getValue();
117 keys.add(key1);
118 keys.add(key2);
119 return true;
120 }
121 });
122
123 final Fixture fixture = new Fixture();
124 fixture.recoverJobs();
125 assertEquals("callCount", 2, callCount.get());
126 assertEquals("throwCount", 1, throwCount.get());
127 assertEquals("escapeCount", 0, escapeCount.get());
128 }
129
130
131 class Fixture extends Quartz1HardenedJobStore {
132
133 @Override
134 protected DriverDelegate getDelegate() throws NoSuchDelegateException {
135 return delegate;
136 }
137
138 @Override
139 protected Object executeInLock(String lockName, TransactionCallback txCallback) throws JobPersistenceException {
140 return txCallback.execute(conn);
141 }
142
143 @Override
144 protected void executeInNonManagedTXLock(
145 final String lockName,
146 final VoidTransactionCallback txCallback) throws JobPersistenceException {
147 txCallback.execute(conn);
148 }
149
150 @Override
151 protected Logger getLog() {
152 return LOG;
153 }
154
155 @Override
156 protected boolean jobExists(Connection conn, String jobName, String groupName) throws JobPersistenceException {
157 return true;
158 }
159
160 @Override
161 protected void storeTrigger(Connection conn, SchedulingContext ctxt, Trigger newTrigger,
162 @Nullable JobDetail job, boolean replaceExisting, String state, boolean forceState,
163 boolean recovering) throws JobPersistenceException {
164 callCount.incrementAndGet();
165 boolean ok = false;
166 try {
167 super.storeTrigger(conn, ctxt, newTrigger, job, replaceExisting, state, forceState, recovering);
168 ok = true;
169 } finally {
170 if (!ok) {
171 escapeCount.incrementAndGet();
172 }
173 }
174 }
175 }
176 }