/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.usergrid.batch.service;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import com.google.inject.Injector;
import org.apache.usergrid.batch.JobExecution;
import org.apache.usergrid.batch.JobExecution.Status;
import org.apache.usergrid.batch.JobRuntime;
import org.apache.usergrid.batch.JobRuntimeException;
import org.apache.usergrid.batch.repository.JobAccessor;
import org.apache.usergrid.batch.repository.JobDescriptor;
import org.apache.usergrid.mq.Message;
import org.apache.usergrid.mq.QueueManager;
import org.apache.usergrid.mq.QueueManagerFactory;
import org.apache.usergrid.mq.QueueQuery;
import org.apache.usergrid.mq.QueueResults;
import org.apache.usergrid.persistence.EntityManager;
import org.apache.usergrid.persistence.EntityManagerFactory;
import org.apache.usergrid.persistence.index.EntityIndex;
import org.apache.usergrid.persistence.Query;
import org.apache.usergrid.persistence.Results;
import org.apache.usergrid.persistence.Schema;
import org.apache.usergrid.persistence.SimpleEntityRef;
import org.apache.usergrid.persistence.entities.JobData;
import org.apache.usergrid.persistence.entities.JobStat;
import org.apache.usergrid.persistence.exceptions.TransactionNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
/**
* Should be referenced by services as a SchedulerService instance. Only the internal job
* runtime should refer to this as a JobAccessor
*/
public class SchedulerServiceImpl implements SchedulerService, JobAccessor, JobRuntimeService {
private static final String STATS_ID = "statsId";
private static final String JOB_ID = "jobId";
private static final String JOB_NAME = "jobName";
private static final Logger logger = LoggerFactory.getLogger( SchedulerServiceImpl.class );
private static final String DEFAULT_QUEUE_NAME = "/jobs";
private QueueManagerFactory qmf;
private EntityManagerFactory emf;
private String jobQueueName = DEFAULT_QUEUE_NAME;
private QueueManager qm;
private EntityManager em;
/** Timeout for how long to set the transaction timeout from the queue. Default is 30000 */
private long jobTimeout = 30000;
private Injector injector;
private EntityIndex entityIndex;
/**
*
*/
public SchedulerServiceImpl() {
}
/*
* (non-Javadoc)
*
* @see
* org.apache.usergrid.batch.service.SchedulerService#createJob(java.lang.String,
* long, org.apache.usergrid.persistence.Entity)
*/
@Override
public JobData createJob( String jobName, long fireTime, JobData jobData ) {
Assert.notNull( jobName, "jobName is required" );
Assert.notNull( jobData, "jobData is required" );
try {
jobData.setJobName( jobName );
JobData job = getEm().create( jobData );
JobStat stat = getEm().create( new JobStat( jobName, job.getUuid() ) );
scheduleJob( jobName, fireTime, job.getUuid(), stat.getUuid() );
return job;
}
catch ( Exception e ) {
throw new JobRuntimeException( e );
}
}
/** Schedule the job internally */
private void scheduleJob( String jobName, long fireTime, UUID jobDataId, UUID jobStatId ) {
Assert.notNull( jobName, "jobName is required" );
Assert.isTrue( fireTime > -1, "fireTime must be positive" );
Assert.notNull( jobDataId, "jobDataId is required" );
Assert.notNull( jobStatId, "jobStatId is required" );
Message message = new Message();
message.setTimestamp( fireTime );
message.setStringProperty( JOB_NAME, jobName );
message.setProperty( JOB_ID, jobDataId.toString() );
message.setProperty( STATS_ID, jobStatId.toString() );
getQm().postToQueue( jobQueueName, message );
}
/*
* (non-Javadoc)
*
* @see org.apache.usergrid.batch.service.SchedulerService#deleteJob(java.util.UUID)
*/
@Override
public void deleteJob( UUID jobId ) {
/**
* just delete our target job data. This is easier than attempting to delete
* from the queue. The runner should catch this and treat the queued message
* as discarded
*/
try {
if (logger.isDebugEnabled()) {
logger.debug("deleteJob {}", jobId);
}
getEm().delete( new SimpleEntityRef(
Schema.getDefaultSchema().getEntityType(JobData.class), jobId ) );
}
catch ( Exception e ) {
throw new JobRuntimeException( e );
}
}
/*
* (non-Javadoc)
*
* @see org.apache.usergrid.batch.repository.JobAccessor#getJobs(int)
*/
@Override
public List<JobDescriptor> getJobs( int size ) {
QueueQuery query = new QueueQuery();
query.setTimeout( jobTimeout );
query.setLimit( size );
QueueResults jobs = getQm().getFromQueue( jobQueueName, query );
List<JobDescriptor> results = new ArrayList<JobDescriptor>( jobs.size() );
for ( Message job : jobs.getMessages() ) {
Object jo = job.getStringProperty( JOB_ID );
UUID jobUuid = UUID.fromString( job.getStringProperty( JOB_ID ) );
UUID statsUuid = UUID.fromString( job.getStringProperty( STATS_ID ) );
String jobName = job.getStringProperty( JOB_NAME );
try {
JobData data = getEm().get( jobUuid, JobData.class );
JobStat stats = getEm().get( statsUuid, JobStat.class );
/**
* no job data, which is required even if empty to signal the job should
* still fire. Ignore this job
*/
if ( data == null || stats == null ) {
logger.info( "Received job with data id '{}' from the queue, but no data was found. Dropping job",
jobUuid );
getQm().deleteTransaction( jobQueueName, job.getTransaction(), null );
if ( data != null ) {
getEm().delete( data );
}
if ( stats != null ) {
getEm().delete( stats );
}
continue;
}
results.add( new JobDescriptor( jobName, job.getUuid(), job.getTransaction(), data, stats, this ) );
}
catch ( Exception e ) {
// log and skip. This is a catastrophic runtime error if we see an
// exception here. We don't want to cause job loss, so leave the job in
// the Q.
logger.error(
"Unable to retrieve job data for jobname {}, job id {}, stats id {}. Skipping to avoid job loss",
jobName, jobUuid, statsUuid, e );
}
}
return results;
}
@Override
public void heartbeat( JobRuntime execution, long delay ) {
if (logger.isDebugEnabled()) {
logger.debug("renew transaction {}", execution.getTransactionId());
}
try {
// @TODO - what's the point to this sychronized block on an argument?
synchronized ( execution ) {
UUID newId = getQm().renewTransaction( jobQueueName, execution.getTransactionId(),
new QueueQuery().withTimeout( delay ) );
execution.setTransactionId( newId );
if (logger.isDebugEnabled()) {
logger.debug("renewed transaction {}", newId);
}
}
}
catch ( TransactionNotFoundException e ) {
logger.error( "Could not renew transaction", e );
throw new JobRuntimeException( "Could not renew transaction during heartbeat", e );
}
}
/* (non-Javadoc)
* @see org.apache.usergrid.batch.service.JobRuntimeService#heartbeat(org.apache.usergrid.batch.JobRuntime)
*/
@Override
public void heartbeat( JobRuntime execution ) {
heartbeat( execution, jobTimeout );
}
/*
* (non-Javadoc)
*
* @see org.apache.usergrid.batch.service.SchedulerService#delay(org.apache.usergrid.batch.
* JobExecutionImpl)
*/
@Override
public void delay( JobRuntime execution ) {
delayRetry( execution.getExecution(), execution.getDelay() );
}
/*
* (non-Javadoc)
*
* @see
* org.apache.usergrid.batch.repository.JobAccessor#save(org.apache.usergrid.batch.JobExecution
* )
*/
@Override
public void save( JobExecution bulkJobExecution ) {
JobData data = bulkJobExecution.getJobData();
JobStat stat = bulkJobExecution.getJobStats();
Status jobStatus = bulkJobExecution.getStatus();
try {
// we're done. Mark the transaction as complete and delete the job info
if ( jobStatus == Status.COMPLETED ) {
logger.info( "Job {} is complete id: {}", data.getJobName(), bulkJobExecution.getTransactionId() );
getQm().deleteTransaction( jobQueueName, bulkJobExecution.getTransactionId(), null );
if (logger.isDebugEnabled()) {
logger.debug("delete job data {}", data.getUuid());
}
getEm().delete( data );
}
// the job failed too many times. Delete the transaction to prevent it
// running again and save it for querying later
else if ( jobStatus == Status.DEAD ) {
logger.warn( "Job {} is dead. Removing", data.getJobName() );
getQm().deleteTransaction( jobQueueName, bulkJobExecution.getTransactionId(), null );
getEm().update( data );
}
// update the job for the next run
else {
getEm().update( data );
}
logger.debug( "Updating stats for job {}", data.getJobName() );
getEm().update( stat );
}
catch ( Exception e ) {
// should never happen
throw new JobRuntimeException( String.format( "Unable to delete job data with id %s", data.getUuid() ), e );
}
}
/*
* (non-Javadoc)
*
* @see org.apache.usergrid.batch.service.SchedulerService#queryJobData(org.apache.usergrid.
* persistence.Query)
*/
@Override
public Results queryJobData( Query query ) throws Exception {
if ( query == null ) {
query = new Query();
}
String jobDataType = Schema.getDefaultSchema().getEntityType(JobData.class);
return getEm().searchCollection( getEm().getApplicationRef(),
Schema.defaultCollectionName(jobDataType), query );
}
/*
* (non-Javadoc)
*
* @see
* org.apache.usergrid.batch.repository.JobAccessor#delayRetry(org.apache.usergrid.batch
* .JobExecution, long)
*/
@Override
public void delayRetry( JobExecution execution, long delay ) {
JobData data = execution.getJobData();
JobStat stat = execution.getJobStats();
try {
// if it's a dead status, it's failed too many times, just kill the job
if ( execution.getStatus() == Status.DEAD ) {
getQm().deleteTransaction( jobQueueName, execution.getTransactionId(), null );
getEm().update( data );
getEm().update( stat );
return;
}
// re-schedule the job to run again in the future
scheduleJob( execution.getJobName(), System.currentTimeMillis() + delay, data.getUuid(), stat.getUuid() );
// delete the pending transaction
getQm().deleteTransaction( jobQueueName, execution.getTransactionId(), null );
// update the data for the next run
getEm().update( data );
getEm().update( stat );
}
catch ( Exception e ) {
// should never happen
throw new JobRuntimeException( String.format( "Unable to delete job data with id %s", data.getUuid() ), e );
}
}
/* (non-Javadoc)
* @see org.apache.usergrid.batch.service.SchedulerService#getStatsForJob(java.lang.String, java.util.UUID)
*/
@Override
public JobStat getStatsForJob( String jobName, UUID jobId ) throws Exception {
EntityManager em = emf.getEntityManager( emf.getManagementAppId() );
Query query = Query.fromQL( "select * where " + JOB_NAME + " = '" + jobName + "' AND " + JOB_ID + " = " + jobId );
Results r = em.searchCollection( em.getApplicationRef(), "job_stats", query );
if ( r.size() == 1 ) {
return ( JobStat ) r.getEntity();
}
return null;
}
/** @param qmf the qmf to set */
@Autowired
public void setQmf( QueueManagerFactory qmf ) {
this.qmf = qmf;
}
/** @param emf the emf to set */
@Autowired
public void setEmf( EntityManagerFactory emf ) {
this.emf = emf;
}
/** @param injector **/
@Autowired
public void setInjector( Injector injector){ this.injector = injector;}
/** @param jobQueueName the jobQueueName to set */
public void setJobQueueName( String jobQueueName ) {
this.jobQueueName = jobQueueName;
}
/** @param timeout the timeout to set */
public void setJobTimeout( long timeout ) {
this.jobTimeout = timeout;
}
public QueueManager getQm() {
if ( qm == null ) {
this.qm = qmf.getQueueManager( emf.getManagementAppId());
}
return qm;
}
public EntityManager getEm() {
if ( em == null ) {
this.em = emf.getEntityManager( emf.getManagementAppId() );
}
return em;
}
@Override
public void refreshIndex() {
this.entityIndex = entityIndex == null ? injector.getInstance(EntityIndex.class) : entityIndex;
entityIndex.refreshAsync().toBlocking().first();
}
}