/*******************************************************************************
* Copyright 2013 Michael Marconi
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
******************************************************************************/
package oncue.tests.redis;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static oncue.backingstore.RedisBackingStore.JOB_KEY;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import akka.actor.ActorRef;
import akka.testkit.JavaTestKit;
import oncue.backingstore.RedisBackingStore;
import oncue.backingstore.RedisBackingStore.RedisConnection;
import oncue.common.messages.EnqueueJob;
import oncue.common.messages.Job;
import oncue.common.messages.Job.State;
import oncue.common.messages.JobFailed;
import oncue.common.messages.JobProgress;
import oncue.tests.base.ActorSystemTest;
import oncue.tests.workers.IncompetentTestWorker;
import oncue.tests.workers.TestWorker;
import redis.clients.jedis.Jedis;
public class RedisBackingStoreTest extends ActorSystemTest {
private RedisConnection redis;
@Before
public void flushRedis() {
redis = new RedisConnection();
redis.flushDB();
}
@After
public void releaseRedisConnection() {
redis.close();
}
@AfterClass
public static void finalFlushRedis() {
try (RedisConnection redis = new RedisConnection()) {
redis.flushDB();
}
}
@Test
public void addScheduledJob() {
new JavaTestKit(system) {
{
// Create a scheduler probe
final JavaTestKit schedulerProbe = new JavaTestKit(system) {
{
new IgnoreMsg() {
@Override
protected boolean ignore(Object message) {
return !(message instanceof JobProgress);
}
};
}
};
// Create a Redis-backed scheduler (see config) with a probe
ActorRef scheduler = createScheduler(system, schedulerProbe.getRef());
// Construct job params
Map<String, String> params = new HashMap<>();
params.put("month", "Jan");
params.put("size", "x-large");
// Enqueue a job
scheduler.tell(new EnqueueJob(TestWorker.class.getName(), params), getRef());
final Job job = expectMsgClass(Job.class);
// Check to see that the job is written as finished in redis
new AwaitCond() {
@Override
protected boolean cond() {
return redis.llen(RedisBackingStore.UNSCHEDULED_JOBS) > 0
&& redis.exists(getJobId(job.getId()));
}
};
Job loadedJob = RedisBackingStore.loadJob(job.getId(), redis);
assertEquals(job.getId(), loadedJob.getId());
assertEquals(job.getEnqueuedAt().toString(), loadedJob.getEnqueuedAt().toString());
assertEquals(job.getWorkerType(), loadedJob.getWorkerType());
assertEquals("Wrong number of parameters", 2, loadedJob.getParams().size());
assertEquals(job.getParams().get("month"), loadedJob.getParams().get("month"));
assertEquals(job.getParams().get("size"), loadedJob.getParams().get("size"));
// Start an Agent
createAgent(system, new HashSet<>(Arrays.asList(TestWorker.class.getName())), null);
// Check to see that the scheduled job has finished
new AwaitCond() {
@Override
protected boolean cond() {
// Wait for some progress
JobProgress progress = schedulerProbe.expectMsgClass(JobProgress.class);
return progress.getJob().getProgress() == 1.0;
}
};
// Check to see that the job goes into the completed jobs list
new AwaitCond() {
@Override
protected boolean cond() {
return redis.llen(RedisBackingStore.COMPLETED_JOBS) > 0;
}
};
}
};
}
@Test
public void addUnscheduledJob() {
new JavaTestKit(system) {
{
// Create a Redis-backed scheduler (see config)
ActorRef scheduler = createScheduler(system, null);
// Construct job params
Map<String, String> params = new HashMap<>();
params.put("month", "Jan");
params.put("size", "x-large");
// Enqueue a job
scheduler.tell(new EnqueueJob(TestWorker.class.getName(), params), getRef());
final Job job = expectMsgClass(Job.class);
// Check to see that the job is written as finished in redis
new AwaitCond() {
@Override
protected boolean cond() {
return redis.llen(RedisBackingStore.UNSCHEDULED_JOBS) > 0
&& redis.exists(getJobId(job.getId()));
}
};
// Check to see that unscheduled job has been recorded in Redis
List<String> jobIDs = redis.brpop(0, RedisBackingStore.UNSCHEDULED_JOBS);
long jobId = new Long(jobIDs.get(1));
Job loadedJob = RedisBackingStore.loadJob(jobId, redis);
assertEquals(job.getId(), loadedJob.getId());
assertEquals(job.getEnqueuedAt().toString(), loadedJob.getEnqueuedAt().toString());
assertEquals(job.getWorkerType(), loadedJob.getWorkerType());
assertEquals("Wrong number of parameters", 2, loadedJob.getParams().size());
assertEquals(job.getParams().get("month"), loadedJob.getParams().get("month"));
assertEquals(job.getParams().get("size"), loadedJob.getParams().get("size"));
assertEquals("There should be no more jobs on the unscheduled queue", 0,
redis.llen(RedisBackingStore.UNSCHEDULED_JOBS).longValue());
}
};
}
@Test
public void persistJobFailure() {
new JavaTestKit(system) {
{
// Create a scheduler probe
final JavaTestKit schedulerProbe = new JavaTestKit(system) {
{
new IgnoreMsg() {
@Override
protected boolean ignore(Object message) {
return !(message instanceof JobFailed);
}
};
}
};
// Create a Redis-backed scheduler (see config) with a probe
ActorRef scheduler = createScheduler(system, schedulerProbe.getRef());
// Enqueue a job for an incompetent worker
scheduler.tell(new EnqueueJob(IncompetentTestWorker.class.getName()), getRef());
Job job = expectMsgClass(Job.class);
final String jobKey = getJobId(job.getId());
// Check to see that the scheduled job has been recorded in Redis
new AwaitCond() {
@Override
protected boolean cond() {
return redis.exists(jobKey);
}
};
// Start an Agent
createAgent(system,
new HashSet<>(Arrays.asList(IncompetentTestWorker.class.getName())), null);
// Expect a job failure message at the scheduler
JobFailed jobFailed = schedulerProbe.expectMsgClass(JobFailed.class);
Job failedJob = jobFailed.getJob();
assertEquals("Job IDs don't match", job.getId(), failedJob.getId());
assertTrue("Wrong exception type", jobFailed.getJob().getErrorMessage()
.contains(ArithmeticException.class.getName()));
expectNoMsg();
new AwaitCond() {
@Override
protected boolean cond() {
return redis.llen(RedisBackingStore.FAILED_JOBS) > 0;
}
};
String state = redis.hget(jobKey, RedisBackingStore.JOB_STATE);
String errorMessage = redis.hget(jobKey, RedisBackingStore.JOB_ERROR_MESSAGE);
assertNotNull("No job state found", state);
assertEquals("The recorded state does not match the expected state",
Job.State.FAILED.toString(), state);
assertTrue(errorMessage.contains(ArithmeticException.class.getName()));
}
};
}
@Test
public void persistJobProgress() {
new JavaTestKit(system) {
{
// Create a scheduler probe
final JavaTestKit schedulerProbe = new JavaTestKit(system) {
{
new IgnoreMsg() {
@Override
protected boolean ignore(Object message) {
return !(message instanceof JobProgress);
}
};
}
};
// Create a Redis-backed scheduler (see config) with a probe
ActorRef scheduler = createScheduler(system, schedulerProbe.getRef());
// Enqueue a job
scheduler.tell(new EnqueueJob(TestWorker.class.getName()), getRef());
Job job = expectMsgClass(Job.class);
// Start an Agent
createAgent(system, new HashSet<>(Arrays.asList(TestWorker.class.getName())), null);
// Expect a series of progress reports
double expectedProgress = 0;
for (int i = 0; i < 5; i++) {
JobProgress jobProgress = schedulerProbe.expectMsgClass(JobProgress.class);
assertEquals(expectedProgress, jobProgress.getJob().getProgress());
expectedProgress += 0.25;
}
schedulerProbe.expectNoMsg();
// Check to see progress has been recorded in Redis
final String jobKey = getJobId(job.getId());
String progress = redis.hget(jobKey, RedisBackingStore.JOB_PROGRESS);
assertNotNull("No progress found", progress);
assertEquals("The recorded progress does not match the expected progress", 1.0,
new Double(progress));
// Check to see that only one job was persisted
assertEquals(1, redis.lrange(RedisBackingStore.COMPLETED_JOBS, 0, -1).size());
}
};
}
@Test
public void getCompletedJobs() {
new JavaTestKit(system) {
{
RedisBackingStore backingStore = new RedisBackingStore(system, settings);
// Push a job into Redis
Job job = new Job(1, TestWorker.class.getName());
RedisBackingStore.persistJob(job, RedisBackingStore.SCHEDULED_JOBS, redis);
// Record progress on the job
job.setProgress(1.0);
job.setState(Job.State.COMPLETE);
backingStore.persistJobProgress(job);
// Get the list of completed jobs
List<Job> completedJobs = backingStore.getCompletedJobs();
assertEquals(1, completedJobs.size());
assertEquals(job.getId(), completedJobs.get(0).getId());
}
};
}
@Test
public void getFailedJobs() {
new JavaTestKit(system) {
{
RedisBackingStore backingStore = new RedisBackingStore(system, settings);
// Push a job into Redis
Job job = new Job(1, TestWorker.class.getName());
RedisBackingStore.persistJob(job, RedisBackingStore.SCHEDULED_JOBS, redis);
// Record job failure on the job
job.setErrorMessage(new Exception("Test exception").toString());
backingStore.persistJobFailure(job);
// Get the list of failed jobs
List<Job> failedJobs = backingStore.getFailedJobs();
assertEquals(1, failedJobs.size());
assertEquals(job.getId(), failedJobs.get(0).getId());
}
};
}
@Test
public void restoreJobs() {
new JavaTestKit(system) {
{
RedisBackingStore backingStore = new RedisBackingStore(system, settings);
// Push an unscheduled job into Redis
Job unscheduledJob = new Job(1, TestWorker.class.getName());
RedisBackingStore.persistJob(unscheduledJob, RedisBackingStore.UNSCHEDULED_JOBS,
redis);
// Push a scheduled job into Redis
Job scheduledJob = new Job(2, TestWorker.class.getName());
RedisBackingStore.persistJob(scheduledJob, RedisBackingStore.SCHEDULED_JOBS, redis);
// Restore the jobs
List<Job> restoredJobs = backingStore.restoreJobs();
// Check the set of restored jobs
assertTrue(restoredJobs.size() == 2);
for (Job job : restoredJobs) {
assertTrue(job.getId() == unscheduledJob.getId()
|| job.getId() == scheduledJob.getId());
}
// Make sure no scheduled jobs remain
assertEquals(0, redis.lrange(RedisBackingStore.SCHEDULED_JOBS, 0, -1).size());
}
};
}
@Test
public void saveAndLoadJob() {
// Construct job params
Map<String, String> params = new HashMap<>();
params.put("month", "Jan");
params.put("size", "x-large");
DateTime testTime = DateTime.now();
Job job = new Job(0, TestWorker.class.getName());
job.setParams(params);
job.setProgress(0.8);
job.setRerun(true);
job.setStartedAt(testTime);
job.setCompletedAt(testTime);
RedisBackingStore.persistJob(job, "test_queue", redis);
Job loadedJob = RedisBackingStore.loadJob(0, redis);
assertNotNull("Expected 'enqueued at' timestamp", job.getEnqueuedAt());
assertNotNull("Expected 'started at' timestamp", job.getStartedAt());
assertNotNull("Expected 'completed at' timestamp", job.getCompletedAt());
assertEquals(job.getId(), loadedJob.getId());
assertEquals(job.getEnqueuedAt().toString(), loadedJob.getEnqueuedAt().toString());
assertEquals(testTime.toString(), loadedJob.getStartedAt().toString());
assertEquals(testTime.toString(), loadedJob.getCompletedAt().toString());
assertEquals(job.getWorkerType(), loadedJob.getWorkerType());
assertEquals(job.getProgress(), loadedJob.getProgress());
assertEquals(job.isRerun(), loadedJob.isRerun());
assertEquals("Wrong number of parameters", 2, loadedJob.getParams().size());
assertEquals(job.getParams().get("month"), loadedJob.getParams().get("month"));
assertEquals(job.getParams().get("size"), loadedJob.getParams().get("size"));
}
@Test
public void cleanUpJobsReturnsCorrectCleanedUpJobCount() {
new JavaTestKit(system) {
private RedisBackingStore backingStore;
{
backingStore = new RedisBackingStore(system, settings);
// Push expired jobs into redis
persistTestJob(1, DateTime.now().minusHours(2), false);
persistTestJob(2, DateTime.now().minusHours(2), true);
persistTestJob(3, DateTime.now().minusHours(2), true);
assertTrue(redis.exists(getJobId(1)));
assertTrue(redis.exists(getJobId(2)));
assertTrue(redis.exists(getJobId(3)));
assertEquals(1, backingStore.cleanupJobs(false, Duration.standardHours(1)));
assertEquals(2, backingStore.cleanupJobs(true, Duration.standardHours(1)));
assertFalse(redis.exists(getJobId(1)));
assertFalse(redis.exists(getJobId(2)));
assertFalse(redis.exists(getJobId(3)));
}
private void persistTestJob(int jobNumber, DateTime completionTime, boolean failed) {
Job job = new Job(jobNumber, TestWorker.class.getName());
job.setCompletedAt(completionTime);
RedisBackingStore.persistJob(job, RedisBackingStore.SCHEDULED_JOBS, redis);
if (failed) {
job.setState(State.FAILED);
job.setErrorMessage(new Exception("Test exception").toString());
backingStore.persistJobFailure(job);
} else {
// Record progress on the job
job.setProgress(1.0);
job.setState(Job.State.COMPLETE);
backingStore.persistJobProgress(job);
}
}
};
}
@Test
public void removeJobById() {
new JavaTestKit(system) {
private RedisBackingStore backingStore;
{
backingStore = new RedisBackingStore(system, settings);
Job job = new Job(1, TestWorker.class.getName());
RedisBackingStore.persistJob(job, RedisBackingStore.SCHEDULED_JOBS, redis);
String jobId = getJobId(job.getId());
assertTrue(redis.exists(jobId));
backingStore.removeJobById(job.getId(), redis);
assertFalse(redis.exists(jobId));
}
};
}
@Test
public void removeCompletedJobById() {
new JavaTestKit(system) {
{
RedisBackingStore backingStore = new RedisBackingStore(system, settings);
// Push a job into Redis
Job job = new Job(1, TestWorker.class.getName());
job.setCompletedAt(DateTime.now().minusYears(1));
RedisBackingStore.persistJob(job, RedisBackingStore.COMPLETED_JOBS, redis);
assertTrue(redis.exists(getJobId(job.getId())));
// Remove the scheduled job
backingStore.removeCompletedJobById(job.getId());
// Assert that the job no longer exists
assertFalse(redis.exists(getJobId(job.getId())));
// Check scheduled list in Redis
assertEquals("Expected no jobs in the completed jobs list", 0,
redis.lrange(RedisBackingStore.COMPLETED_JOBS, 0, -1).size());
}
};
}
@Test
public void removeFailedJobById() {
new JavaTestKit(system) {
{
RedisBackingStore backingStore = new RedisBackingStore(system, settings);
// Push a job into Redis
Job job = new Job(1, TestWorker.class.getName());
job.setCompletedAt(DateTime.now().minusYears(1));
RedisBackingStore.persistJob(job, RedisBackingStore.FAILED_JOBS, redis);
assertTrue(redis.exists(getJobId(job.getId())));
// Remove the scheduled job
backingStore.removeFailedJobById(job.getId());
// Assert that the job no longer exists
assertFalse(redis.exists(getJobId(job.getId())));
// Check scheduled list in Redis
assertEquals("Expected no jobs in the failed jobs list", 0,
redis.lrange(RedisBackingStore.FAILED_JOBS, 0, -1).size());
}
};
}
@Test
public void removeScheduledJobById() {
new JavaTestKit(system) {
{
RedisBackingStore backingStore = new RedisBackingStore(system, settings);
// Push a job into Redis
Job job = new Job(1, TestWorker.class.getName());
RedisBackingStore.persistJob(job, RedisBackingStore.SCHEDULED_JOBS, redis);
assertTrue(redis.exists(getJobId(job.getId())));
// Remove the scheduled job
backingStore.removeScheduledJobById(job.getId());
// Assert that the job still exists
assertTrue(redis.exists(getJobId(job.getId())));
// Check scheduled list in Redis
assertEquals("Expected no jobs in the scheduled jobs list", 0,
redis.lrange(RedisBackingStore.SCHEDULED_JOBS, 0, -1).size());
}
};
}
@Test
public void removeUnscheduledJobById() {
new JavaTestKit(system) {
{
RedisBackingStore backingStore = new RedisBackingStore(system, settings);
// Push a job into Redis
Job job = new Job(1, TestWorker.class.getName());
RedisBackingStore.persistJob(job, RedisBackingStore.UNSCHEDULED_JOBS, redis);
assertTrue(redis.exists(getJobId(job.getId())));
// Remove the scheduled job
backingStore.removeUnscheduledJobById(job.getId());
// Assert that the job still exists
assertTrue(redis.exists(getJobId(job.getId())));
// Check scheduled list in Redis
assertEquals("Expected no jobs in the unscheduled jobs list", 0,
redis.lrange(RedisBackingStore.UNSCHEDULED_JOBS, 0, -1).size());
}
};
}
private String getJobId(long id) {
return String.format(JOB_KEY, id);
}
}