1 package com.atlassian.scheduler.core;
2
3 import com.atlassian.scheduler.JobRunner;
4 import com.atlassian.scheduler.JobRunnerRequest;
5 import com.atlassian.scheduler.JobRunnerResponse;
6 import com.atlassian.scheduler.config.JobConfig;
7 import com.atlassian.scheduler.config.Schedule;
8 import com.atlassian.scheduler.core.impl.RunningJobImpl;
9 import com.atlassian.scheduler.core.status.SimpleJobDetails;
10 import com.atlassian.scheduler.core.status.UnusableJobDetails;
11 import com.atlassian.scheduler.status.JobDetails;
12 import org.junit.Test;
13 import org.mockito.ArgumentCaptor;
14 import org.mockito.InOrder;
15
16 import java.util.Date;
17
18 import static com.atlassian.scheduler.JobRunnerResponse.failed;
19 import static com.atlassian.scheduler.JobRunnerResponse.success;
20 import static com.atlassian.scheduler.config.RunMode.RUN_LOCALLY;
21 import static com.atlassian.scheduler.config.RunMode.RUN_ONCE_PER_CLUSTER;
22 import static com.atlassian.scheduler.core.Constants.JOB_ID;
23 import static com.atlassian.scheduler.core.Constants.KEY;
24 import static com.atlassian.scheduler.status.RunOutcome.ABORTED;
25 import static com.atlassian.scheduler.status.RunOutcome.FAILED;
26 import static com.atlassian.scheduler.status.RunOutcome.SUCCESS;
27 import static com.atlassian.scheduler.status.RunOutcome.UNAVAILABLE;
28 import static java.lang.Math.abs;
29 import static java.lang.System.currentTimeMillis;
30 import static org.hamcrest.Matchers.lessThan;
31 import static org.junit.Assert.assertNotNull;
32 import static org.junit.Assert.assertThat;
33 import static org.mockito.Matchers.any;
34 import static org.mockito.Matchers.eq;
35 import static org.mockito.Mockito.inOrder;
36 import static org.mockito.Mockito.mock;
37 import static org.mockito.Mockito.never;
38 import static org.mockito.Mockito.verify;
39 import static org.mockito.Mockito.when;
40
41
42
43
44 @SuppressWarnings({"ResultOfObjectAllocationIgnored", "ConstantConditions"})
45 public class JobLauncherTest {
46 private static final Date NOW = new Date();
47 private static final Schedule SCHEDULE = Schedule.forInterval(60000L, null);
48
49 @Test(expected = IllegalArgumentException.class)
50 public void testSchedulerServiceNull() {
51 new JobLauncher(null, RUN_LOCALLY, new Date(), JOB_ID);
52 }
53
54 @Test(expected = IllegalArgumentException.class)
55 public void testRunModeNull() {
56 new JobLauncher(mock(AbstractSchedulerService.class), null, new Date(), JOB_ID);
57 }
58
59 @Test(expected = IllegalArgumentException.class)
60 public void testJobIdNull() {
61 new JobLauncher(mock(AbstractSchedulerService.class), RUN_LOCALLY, new Date(), null);
62 }
63
64 @Test
65 public void testLaunchJobDetailsNull() {
66 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
67 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, null, JOB_ID);
68 assertCloseToNow(jobLauncher.firedAt);
69
70 jobLauncher.launch();
71
72 verify(schedulerService).addRunDetails(JOB_ID, jobLauncher.firedAt, ABORTED, "No corresponding job details");
73 }
74
75 @Test
76 public void testLaunchJobDetailsNotRunnable() {
77 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
78 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
79 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(unusable(null));
80
81 jobLauncher.launch();
82
83 verify(schedulerService).addRunDetails(JOB_ID, NOW, UNAVAILABLE, "Job runner key 'test.key' is not registered");
84 }
85
86 @Test
87 public void testLaunchJobRunnerVanished() {
88 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
89 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
90 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(details());
91
92 jobLauncher.launch();
93
94 verify(schedulerService).addRunDetails(JOB_ID, NOW, UNAVAILABLE, "Job runner key 'test.key' is not registered");
95 }
96
97 @Test
98 public void testLaunchRunModeInconsistency() {
99 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
100 final JobRunner jobRunner = mock(JobRunner.class);
101 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_ONCE_PER_CLUSTER, NOW, JOB_ID);
102 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(details());
103 when(schedulerService.getJobRunner(KEY)).thenReturn(jobRunner);
104
105 jobLauncher.launch();
106
107 verify(schedulerService).addRunDetails(JOB_ID, NOW, ABORTED,
108 "Inconsistent run mode: expected 'RUN_LOCALLY' got: 'RUN_ONCE_PER_CLUSTER'");
109 }
110
111 @Test
112 public void testLaunchJobRunnerWhileAlreadyRunning() throws Exception {
113 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
114 final JobRunner jobRunner = mock(JobRunner.class);
115 final RunningJob existing = mock(RunningJob.class);
116 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
117
118 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(details());
119 when(schedulerService.getJobRunner(KEY)).thenReturn(jobRunner);
120 when(schedulerService.enterJob(eq(JOB_ID), any(RunningJob.class))).thenReturn(existing);
121
122 jobLauncher.launch();
123
124 verify(schedulerService).addRunDetails(JOB_ID, NOW, ABORTED, "Already running");
125 verify(schedulerService).enterJob(eq(JOB_ID), any(RunningJob.class));
126 verify(schedulerService, never()).leaveJob(eq(JOB_ID), any(RunningJob.class));
127 verify(schedulerService, never()).unscheduleJob(JOB_ID);
128 verify(schedulerService, never()).preJob();
129 verify(schedulerService, never()).postJob();
130 verify(jobRunner, never()).runJob(new RunningJobImpl(NOW, JOB_ID, config()));
131 }
132
133 @Test
134 public void testLaunchJobRunnerThatReturnsNull() throws Exception {
135 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
136 final JobRunner jobRunner = mock(JobRunner.class);
137 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
138
139 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(details());
140 when(schedulerService.getJobRunner(KEY)).thenReturn(jobRunner);
141
142 jobLauncher.launch();
143
144 verify(schedulerService).addRunDetails(JOB_ID, NOW, SUCCESS, null);
145 verify(schedulerService, never()).unscheduleJob(JOB_ID);
146 assertJobLifeCycle(schedulerService, jobRunner, config());
147 }
148
149 @Test
150 public void testLaunchJobRunnerThatReturnsInfo() throws Exception {
151 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
152 final JobRunner jobRunner = mock(JobRunner.class);
153 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
154 final JobRunnerRequest request = new RunningJobImpl(NOW, JOB_ID, config());
155 final JobRunnerResponse response = success("Info");
156
157 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(details());
158 when(schedulerService.getJobRunner(KEY)).thenReturn(jobRunner);
159 when(jobRunner.runJob(request)).thenReturn(response);
160
161 jobLauncher.launch();
162
163 verify(schedulerService).addRunDetails(JOB_ID, NOW, SUCCESS, "Info");
164 verify(schedulerService, never()).unscheduleJob(JOB_ID);
165 assertJobLifeCycle(schedulerService, jobRunner, config());
166 }
167
168
169 @Test
170 public void testLaunchDeletesRunOnceJobOnSuccess() throws Exception {
171 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
172 final JobRunner jobRunner = mock(JobRunner.class);
173 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
174 final JobDetails jobDetails = new SimpleJobDetails(JOB_ID, KEY, RUN_LOCALLY, Schedule.runOnce(null), null, null, null);
175 final JobConfig config = config(jobDetails);
176 final JobRunnerRequest request = new RunningJobImpl(NOW, JOB_ID, config);
177 final JobRunnerResponse response = success("Info");
178
179 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(jobDetails);
180 when(schedulerService.getJobRunner(KEY)).thenReturn(jobRunner);
181 when(jobRunner.runJob(request)).thenReturn(response);
182
183 jobLauncher.launch();
184
185 verify(schedulerService).addRunDetails(JOB_ID, NOW, SUCCESS, "Info");
186 verify(schedulerService).unscheduleJob(JOB_ID);
187 assertJobLifeCycle(schedulerService, jobRunner, config);
188 }
189
190 @Test
191 public void testLaunchDeletesRunOnceJobOnFailure() throws Exception {
192 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
193 final JobRunner jobRunner = mock(JobRunner.class);
194 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
195 final JobDetails jobDetails = new SimpleJobDetails(JOB_ID, KEY, RUN_LOCALLY, Schedule.runOnce(null), null, null, null);
196 final JobConfig config = config(jobDetails);
197 final JobRunnerRequest request = new RunningJobImpl(NOW, JOB_ID, config);
198 final IllegalArgumentException testEx = new IllegalArgumentException("Just testing!");
199
200 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(jobDetails);
201 when(schedulerService.getJobRunner(KEY)).thenReturn(jobRunner);
202 when(jobRunner.runJob(request)).thenThrow(testEx);
203
204 jobLauncher.launch();
205
206 verify(schedulerService).addRunDetails(JOB_ID, NOW, FAILED, failed(testEx).getMessage());
207 verify(schedulerService).unscheduleJob(JOB_ID);
208 assertJobLifeCycle(schedulerService, jobRunner, config);
209 }
210
211 @Test
212 public void testLaunchDeletesRunOnceJobOnUnavailable() throws Exception {
213 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
214 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
215 final JobDetails jobDetails = new UnusableJobDetails(JOB_ID, KEY, RUN_LOCALLY, Schedule.runOnce(null), null, null, null);
216
217 when(schedulerService.getJobDetails(JOB_ID)).thenReturn(jobDetails);
218
219 jobLauncher.launch();
220
221 verify(schedulerService).addRunDetails(JOB_ID, NOW, UNAVAILABLE, "Job runner key 'test.key' is not registered");
222 verify(schedulerService).unscheduleJob(JOB_ID);
223 }
224
225
226 @Test
227 public void testLaunchJobRunnerDoesNotAttemptToDeleteWithoutJobDetails() throws Exception {
228 final AbstractSchedulerService schedulerService = mock(AbstractSchedulerService.class);
229 final JobLauncher jobLauncher = new JobLauncher(schedulerService, RUN_LOCALLY, NOW, JOB_ID);
230
231 jobLauncher.launch();
232
233 verify(schedulerService).addRunDetails(JOB_ID, NOW, ABORTED, "No corresponding job details");
234 verify(schedulerService, never()).unscheduleJob(JOB_ID);
235 }
236
237 private static void assertCloseToNow(final Date date) {
238 assertNotNull("Expected a date close to now but got null", date);
239 final long delta = abs(currentTimeMillis() - date.getTime());
240 assertThat("Expected date to be close to now but the delta was too large", delta, lessThan(1000L));
241 }
242
243 private static void assertJobLifeCycle(AbstractSchedulerService schedulerService, JobRunner jobRunner, JobConfig config) {
244 final InOrder inOrder = inOrder(schedulerService, jobRunner);
245 final ArgumentCaptor<RunningJob> jobCaptor = ArgumentCaptor.forClass(RunningJob.class);
246 inOrder.verify(schedulerService).enterJob(eq(JOB_ID), jobCaptor.capture());
247 inOrder.verify(schedulerService).preJob();
248 inOrder.verify(jobRunner).runJob(new RunningJobImpl(NOW, JOB_ID, config));
249 inOrder.verify(schedulerService).leaveJob(JOB_ID, jobCaptor.getValue());
250 inOrder.verify(schedulerService).postJob();
251 }
252
253
254 private static JobConfig config() {
255 return config(details());
256 }
257
258 private static JobConfig config(JobDetails jobDetails) {
259 return JobConfig.forJobRunnerKey(jobDetails.getJobRunnerKey())
260 .withRunMode(jobDetails.getRunMode())
261 .withSchedule(jobDetails.getSchedule())
262 .withParameters(jobDetails.getParameters());
263 }
264
265 private static SimpleJobDetails details() {
266 return new SimpleJobDetails(JOB_ID, KEY, RUN_LOCALLY, SCHEDULE, null, null, null);
267 }
268
269
270 private static UnusableJobDetails unusable(String reason) {
271 return new UnusableJobDetails(JOB_ID, KEY, RUN_LOCALLY, SCHEDULE, null, null,
272 (reason != null) ? new IllegalStateException("Bet you didn't see this coming!") : null);
273 }
274 }