├── .gitignore ├── README ├── src ├── main │ ├── resources │ │ └── META-INF │ │ │ ├── persistence.xml │ │ │ └── spring │ │ │ └── applicationContextTest.xml │ └── java │ │ └── net │ │ └── carinae │ │ └── dev │ │ └── async │ │ ├── Constants.java │ │ ├── util │ │ ├── Serializer.java │ │ └── SerializerJavaImpl.java │ │ ├── dao │ │ ├── QueuedTaskHolderDao.java │ │ └── QueuedTaskHolderDaoJPA2.java │ │ ├── DummyEntity.java │ │ ├── task │ │ └── AbstractBaseTask.java │ │ ├── QueuedTaskHolder.java │ │ └── PersistentTaskExecutor.java └── test │ └── java │ └── net │ └── carinae │ └── dev │ └── async │ ├── dao │ ├── DummyEntityDao.java │ └── DummyEntityDaoJPA2.java │ └── TasksIntegrationTest.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore for bounty 2 | /target 3 | .settings 4 | .classpath 5 | .project 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Template project with a system to reliably execute persistent tasks 2 | 3 | More info in: 4 | http://carinae.net/2010/05/execution-persistent-transactional-tasks-with-spring/ 5 | 6 | Carlos Vara 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async; 17 | 18 | /** 19 | * Constant values used in the async executor. 20 | * 21 | * @author Carlos Vara 22 | */ 23 | public class Constants { 24 | 25 | private Constants() { 26 | // No instances please 27 | } 28 | 29 | public static final long TASK_RUNNER_RATE = 60l*1000l; // Every minute 30 | public static final long TASK_HYPERVISOR_RATE = 60l*60l*1000l; // Every hour 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/net/carinae/dev/async/dao/DummyEntityDao.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async.dao; 17 | 18 | import java.util.List; 19 | import net.carinae.dev.async.DummyEntity; 20 | 21 | 22 | /** 23 | * Simple DAO contract for {@link DummyEntity}s. 24 | * 25 | * @author Carlos Vara 26 | */ 27 | public interface DummyEntityDao { 28 | 29 | /** 30 | * Add the given {@link DummyEntity} to the current persistence context. 31 | * 32 | * @param de 33 | * the entity to add. 34 | */ 35 | void persist(DummyEntity de); 36 | 37 | /** 38 | * Finds all the persisted {@link DummyEntity}s whose data is equals to the 39 | * given one. 40 | * 41 | * @param data 42 | * Data to compare to. 43 | * @return A list of the entities with the same data. 44 | */ 45 | List findByData(String data); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/util/Serializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async.util; 17 | 18 | /** 19 | * Contract for serializer implementations. 20 | *

21 | * @see "http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking" 22 | * 23 | * @author Carlos Vara 24 | */ 25 | public interface Serializer { 26 | 27 | /** 28 | * Serializes an object. 29 | * 30 | * @param obj 31 | * Object to serialize. 32 | * @return The serialized representation of the object. 33 | */ 34 | byte[] serializeObject(Object obj); 35 | 36 | 37 | /** 38 | * Deserializes an object. 39 | * 40 | * @param serializedObj 41 | * Serialized representation of the object. 42 | * @return The deserialized object. 43 | */ 44 | Object deserializeObject(byte[] serializedObj); 45 | 46 | 47 | /** 48 | * Deserializes an object and casts it to the requested type. 49 | * 50 | * @param 51 | * The type of the requested object. 52 | * @param serializedObj 53 | * Serialized representation of the object. 54 | * @return The deserialized object, casted to the requested type. 55 | */ 56 | T deserializeAndCast(byte[] serializedObj); 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/dao/QueuedTaskHolderDao.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async.dao; 17 | 18 | import net.carinae.dev.async.QueuedTaskHolder; 19 | 20 | 21 | /** 22 | * DAO operations for the {@link QueuedTaskHolder} entities. 23 | * 24 | * @author Carlos Vara 25 | */ 26 | public interface QueuedTaskHolderDao { 27 | 28 | /** 29 | * Adds a new task to the current persistence context. The task will be 30 | * persisted into the database at flush/commit. 31 | * 32 | * @param queuedTask 33 | * The task to be saved (enqueued). 34 | */ 35 | void persist(QueuedTaskHolder queuedTask); 36 | 37 | 38 | /** 39 | * Finder that retrieves a task by its id. 40 | * 41 | * @param taskId 42 | * The id of the requested task. 43 | * @return The task with that id, or null if no such task 44 | * exists. 45 | */ 46 | QueuedTaskHolder findById(String taskId); 47 | 48 | 49 | /** 50 | * @return A task which is candidate for execution. The receiving thread 51 | * will need to ensure a lock on it. null if no 52 | * candidate task is available. 53 | */ 54 | QueuedTaskHolder findNextTaskForExecution(); 55 | 56 | 57 | /** 58 | * @return A task which has been in execution for too long without 59 | * finishing. null if there aren't stalled tasks. 60 | */ 61 | QueuedTaskHolder findRandomStalledTask(); 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/DummyEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async; 17 | 18 | import java.util.UUID; 19 | 20 | import javax.persistence.Column; 21 | import javax.persistence.Entity; 22 | import javax.persistence.Id; 23 | import javax.persistence.Version; 24 | import javax.validation.constraints.NotNull; 25 | 26 | /** 27 | * Sample entity used in task scheduling tests. 28 | * 29 | * @author Carlos Vara 30 | */ 31 | @Entity 32 | public class DummyEntity { 33 | 34 | // Getters ----------------------------------------------------------------- 35 | 36 | @Id 37 | public String getId() { 38 | if ( this.id == null ) { 39 | this.setId(UUID.randomUUID().toString()); 40 | } 41 | return this.id; 42 | } 43 | 44 | @NotNull 45 | public String getData() { 46 | return this.data; 47 | } 48 | 49 | @Version 50 | @Column(name="OPTLOCK") 51 | protected int getVersion() { 52 | return this.version; 53 | } 54 | 55 | 56 | // Setters ----------------------------------------------------------------- 57 | 58 | protected void setId(String id) { 59 | this.id = id; 60 | } 61 | 62 | public void setData(String data) { 63 | this.data = data; 64 | } 65 | 66 | protected void setVersion(int version) { 67 | this.version = version; 68 | } 69 | 70 | 71 | // Fields ------------------------------------------------------------------ 72 | 73 | private String id; 74 | private String data; 75 | private int version; 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/net/carinae/dev/async/dao/DummyEntityDaoJPA2.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async.dao; 17 | 18 | import java.util.List; 19 | 20 | import javax.persistence.EntityManager; 21 | import javax.persistence.PersistenceContext; 22 | import javax.persistence.criteria.CriteriaBuilder; 23 | import javax.persistence.criteria.CriteriaQuery; 24 | import javax.persistence.criteria.Root; 25 | 26 | import org.springframework.stereotype.Repository; 27 | import net.carinae.dev.async.DummyEntity; 28 | import net.carinae.dev.async.DummyEntity_; 29 | 30 | 31 | /** 32 | * JPA2 implementation of {@link DummyEntityDao}. 33 | * 34 | * @author Carlos Vara 35 | */ 36 | @Repository 37 | public class DummyEntityDaoJPA2 implements DummyEntityDao { 38 | 39 | // DummyEntityDao methods -------------------------------------------------- 40 | 41 | @Override 42 | public void persist(DummyEntity de) { 43 | this.entityManager.persist(de); 44 | } 45 | 46 | @Override 47 | public List findByData(String data) { 48 | 49 | // select de from DummyEntity 50 | // where de.data = data 51 | CriteriaBuilder cb = this.entityManager.getCriteriaBuilder(); 52 | CriteriaQuery cq = cb.createQuery(DummyEntity.class); 53 | Root de = cq.from(DummyEntity.class); 54 | cq.select(de).where(cb.equal(de.get(DummyEntity_.data), data)); 55 | 56 | return this.entityManager.createQuery(cq).getResultList(); 57 | } 58 | 59 | 60 | // Injected dependencies --------------------------------------------------- 61 | 62 | @PersistenceContext 63 | private EntityManager entityManager; 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/util/SerializerJavaImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async.util; 17 | 18 | import java.io.ByteArrayInputStream; 19 | import java.io.ByteArrayOutputStream; 20 | import java.io.IOException; 21 | import java.io.ObjectInputStream; 22 | import java.io.ObjectOutput; 23 | import java.io.ObjectOutputStream; 24 | import org.springframework.stereotype.Component; 25 | 26 | /** 27 | * Implementation of {@link Serializer} using Java serialization. 28 | * 29 | * @author Carlos Vara 30 | */ 31 | @Component 32 | public class SerializerJavaImpl implements Serializer { 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | @Override 38 | public byte[] serializeObject(Object obj) { 39 | 40 | try { 41 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 42 | ObjectOutput out; 43 | out = new ObjectOutputStream(bos); 44 | out.writeObject(obj); 45 | out.close(); 46 | 47 | byte[] buf = bos.toByteArray(); 48 | return buf; 49 | } 50 | catch (IOException e) { 51 | e.printStackTrace(); 52 | throw new IllegalArgumentException("Could not serialize object " + obj, e); 53 | } 54 | 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | @Override 61 | public Object deserializeObject(byte[] serializedObj) { 62 | 63 | try { 64 | ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(serializedObj)); 65 | Object obj = in.readObject(); 66 | return obj; 67 | } 68 | catch (IOException e) { 69 | e.printStackTrace(); 70 | throw new IllegalArgumentException("Could not deserialize", e); 71 | } 72 | catch (ClassNotFoundException e) { 73 | e.printStackTrace(); 74 | throw new IllegalArgumentException("Could not deserialize", e); 75 | } 76 | 77 | } 78 | 79 | /** 80 | * {@inheritDoc} 81 | */ 82 | @SuppressWarnings("unchecked") 83 | @Override 84 | public T deserializeAndCast(byte[] serializedObj) { 85 | Object obj = deserializeObject(serializedObj); 86 | return (T)obj; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/applicationContextTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/dao/QueuedTaskHolderDaoJPA2.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async.dao; 17 | 18 | import java.util.Calendar; 19 | import java.util.List; 20 | import java.util.Random; 21 | import javax.persistence.EntityManager; 22 | import javax.persistence.PersistenceContext; 23 | import javax.persistence.criteria.CriteriaBuilder; 24 | import javax.persistence.criteria.CriteriaQuery; 25 | import javax.persistence.criteria.Root; 26 | import net.carinae.dev.async.QueuedTaskHolder; 27 | import net.carinae.dev.async.QueuedTaskHolder_; 28 | import org.springframework.stereotype.Repository; 29 | 30 | /** 31 | * JPA2 implementation of {@link QueuedTaskHolderDao}. 32 | * 33 | * @author Carlos Vara 34 | */ 35 | @Repository 36 | public class QueuedTaskHolderDaoJPA2 implements QueuedTaskHolderDao { 37 | 38 | 39 | // QueuedTaskDao methods --------------------------------------------------- 40 | 41 | @Override 42 | public void persist(QueuedTaskHolder queuedTask) { 43 | this.entityManager.persist(queuedTask); 44 | } 45 | 46 | @Override 47 | public QueuedTaskHolder findById(String taskId) { 48 | return this.entityManager.find(QueuedTaskHolder.class, taskId); 49 | } 50 | 51 | @Override 52 | public QueuedTaskHolder findNextTaskForExecution() { 53 | 54 | Calendar NOW = Calendar.getInstance(); 55 | 56 | // select qt from QueuedTask where 57 | // qt.startedStamp == null AND 58 | // (qth.triggerStamp == null || qth.triggerStamp < NOW) 59 | // order by qth.version ASC, qt.creationStamp ASC 60 | CriteriaBuilder cb = this.entityManager.getCriteriaBuilder(); 61 | CriteriaQuery cq = cb.createQuery(QueuedTaskHolder.class); 62 | Root qth = cq.from(QueuedTaskHolder.class); 63 | cq.select(qth) 64 | .where(cb.and(cb.isNull(qth.get(QueuedTaskHolder_.startedStamp)), 65 | cb.or( 66 | cb.isNull(qth.get(QueuedTaskHolder_.triggerStamp)), 67 | cb.lessThan(qth.get(QueuedTaskHolder_.triggerStamp), NOW)))) 68 | .orderBy(cb.asc(qth.get(QueuedTaskHolder_.version)), cb.asc(qth.get(QueuedTaskHolder_.creationStamp))); 69 | 70 | List results = this.entityManager.createQuery(cq).setMaxResults(1).getResultList(); 71 | if ( results.isEmpty() ) { 72 | return null; 73 | } 74 | else { 75 | return results.get(0); 76 | } 77 | 78 | } 79 | 80 | @Override 81 | public QueuedTaskHolder findRandomStalledTask() { 82 | 83 | Calendar TOO_LONG_AGO = Calendar.getInstance(); 84 | TOO_LONG_AGO.add(Calendar.SECOND, -7200); 85 | 86 | // select qth from QueuedTask where 87 | // qth.startedStamp != null AND 88 | // qth.startedStamp < TOO_LONG_AGO 89 | CriteriaBuilder cb = this.entityManager.getCriteriaBuilder(); 90 | CriteriaQuery cq = cb.createQuery(QueuedTaskHolder.class); 91 | Root qth = cq.from(QueuedTaskHolder.class); 92 | cq.select(qth).where( 93 | cb.and( 94 | cb.isNull(qth.get(QueuedTaskHolder_.completedStamp)), 95 | cb.lessThan(qth.get(QueuedTaskHolder_.startedStamp), TOO_LONG_AGO))); 96 | 97 | List stalledTasks = this.entityManager.createQuery(cq).getResultList(); 98 | 99 | if ( stalledTasks.isEmpty() ) { 100 | return null; 101 | } 102 | else { 103 | Random rand = new Random(System.currentTimeMillis()); 104 | return stalledTasks.get(rand.nextInt(stalledTasks.size())); 105 | } 106 | 107 | } 108 | 109 | 110 | // Injected dependencies --------------------------------------------------- 111 | 112 | @PersistenceContext 113 | private EntityManager entityManager; 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/task/AbstractBaseTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async.task; 17 | 18 | import java.io.Serializable; 19 | import java.util.Calendar; 20 | import net.carinae.dev.async.QueuedTaskHolder; 21 | import net.carinae.dev.async.dao.QueuedTaskHolderDao; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.transaction.annotation.Transactional; 26 | 27 | /** 28 | * Superclass for all async tasks. 29 | *

33 | * 34 | * @author Carlos Vara 35 | */ 36 | public abstract class AbstractBaseTask implements Runnable, Serializable { 37 | 38 | final static Logger logger = LoggerFactory.getLogger(AbstractBaseTask.class); 39 | 40 | 41 | // Common data ------------------------------------------------------------- 42 | 43 | private transient String queuedTaskId; 44 | private transient QueuedTaskHolder qth; 45 | private transient Calendar triggerStamp; 46 | 47 | 48 | public void setQueuedTaskId(String queuedTaskId) { 49 | this.queuedTaskId = queuedTaskId; 50 | } 51 | 52 | public String getQueuedTaskId() { 53 | return queuedTaskId; 54 | } 55 | 56 | public void setTriggerStamp(Calendar triggerStamp) { 57 | this.triggerStamp = triggerStamp; 58 | } 59 | 60 | public Calendar getTriggerStamp() { 61 | return triggerStamp; 62 | } 63 | 64 | 65 | // Injected components ----------------------------------------------------- 66 | 67 | @Autowired(required=true) 68 | protected transient QueuedTaskHolderDao queuedTaskHolderDao; 69 | 70 | 71 | // Lifecycle methods ------------------------------------------------------- 72 | 73 | /** 74 | * Entrance point of the task. 75 | * 80 | * 81 | * @see java.lang.Runnable#run() 82 | */ 83 | @Override 84 | final public void run() { 85 | 86 | try { 87 | transactionalOps(); 88 | } catch (RuntimeException e) { 89 | // Free the task, so it doesn't stall 90 | logger.warn("Exception forced task tx rollback: {}", e); 91 | freeTask(); 92 | } 93 | 94 | } 95 | 96 | @Transactional 97 | private void transactionalOps() { 98 | doInTxBeforeTask(); 99 | doTaskInTransaction(); 100 | doInTxAfterTask(); 101 | } 102 | 103 | @Transactional 104 | private void freeTask() { 105 | QueuedTaskHolder task = this.queuedTaskHolderDao.findById(this.queuedTaskId); 106 | task.setStartedStamp(null); 107 | } 108 | 109 | 110 | /** 111 | * Ensures that there is an associated task and that its state is valid. 112 | */ 113 | private void doInTxBeforeTask() { 114 | this.qth = this.queuedTaskHolderDao.findById(this.queuedTaskId); 115 | if ( this.qth == null ) { 116 | throw new IllegalArgumentException("Not executing: no associated task exists: " + this.getQueuedTaskId()); 117 | } 118 | if ( this.qth.getStartedStamp() == null || this.qth.getCompletedStamp() != null ) { 119 | throw new IllegalArgumentException("Illegal queued task status: " + this.qth); 120 | } 121 | } 122 | 123 | 124 | /** 125 | * Method to be implemented by concrete tasks where their operations are 126 | * performed. 127 | */ 128 | public abstract void doTaskInTransaction(); 129 | 130 | 131 | /** 132 | * Marks the associated task as finished. 133 | */ 134 | private void doInTxAfterTask() { 135 | this.qth.setCompletedStamp(Calendar.getInstance()); 136 | } 137 | 138 | 139 | private static final long serialVersionUID = 1L; 140 | } 141 | -------------------------------------------------------------------------------- /src/test/java/net/carinae/dev/async/TasksIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async; 17 | 18 | import java.util.Calendar; 19 | 20 | import junit.framework.Assert; 21 | import net.carinae.dev.async.dao.DummyEntityDao; 22 | import net.carinae.dev.async.task.AbstractBaseTask; 23 | 24 | import org.junit.Test; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.beans.factory.annotation.Configurable; 27 | import org.springframework.beans.factory.annotation.Qualifier; 28 | import org.springframework.core.task.TaskExecutor; 29 | import org.springframework.test.context.ContextConfiguration; 30 | import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; 31 | import org.springframework.transaction.annotation.Transactional; 32 | 33 | /** 34 | * Checks the correct behavior of the task scheduling system. 35 | *

36 | * FIXME: Make the tests modify a row in the db, so the transactionality is tested. 37 | * 38 | * @author Carlos Vara 39 | */ 40 | @ContextConfiguration( locations={"classpath:META-INF/spring/applicationContextTest.xml"} ) 41 | public class TasksIntegrationTest extends AbstractJUnit4SpringContextTests { 42 | 43 | @Qualifier("PersistentExecutor") 44 | @Autowired 45 | private TaskExecutor taskExecutor; 46 | 47 | @Autowired 48 | private DummyEntityDao dummyEntityDao; 49 | 50 | protected static volatile boolean simpleTaskCompleted = false; 51 | 52 | 53 | /** 54 | * Stores a {@link DummyEntity} with the given data. 55 | */ 56 | @Configurable 57 | public static class SimpleTask extends AbstractBaseTask { 58 | 59 | @Autowired 60 | private transient DummyEntityDao dummyEntityDao; 61 | 62 | public SimpleTask(String data) { 63 | super(); 64 | this.data = data; 65 | } 66 | 67 | private final String data; 68 | 69 | @Override 70 | public void doTaskInTransaction() { 71 | DummyEntity de = new DummyEntity(); 72 | de.setData(data); 73 | dummyEntityDao.persist(de); 74 | } 75 | 76 | } 77 | 78 | 79 | /** 80 | * Enqueues a simple task and waits for 3 minutes for it to be executed. 81 | */ 82 | @Test 83 | public void testSimpleTask() throws InterruptedException { 84 | 85 | String data = "" + System.nanoTime(); 86 | 87 | enqueueSimpleTask(data); 88 | 89 | // Now, try to read from the database 90 | int tries = 0; 91 | while (tries < 180 && pollDummyEntity(data)) { 92 | Thread.sleep(1000); // 1 second 93 | tries++; 94 | } 95 | 96 | Assert.assertTrue("Task didn't execute in 3 minutes time", tries < 180); 97 | } 98 | 99 | /** 100 | * Schedules a simple task for 10 seconds in the future and waits for 5 101 | * minutes for it to be executed. 102 | */ 103 | @Test 104 | public void testScheduledSimpleTask() throws InterruptedException { 105 | 106 | String data = "" + System.nanoTime(); 107 | 108 | scheduleSimpleTask(data); 109 | 110 | // Now, try to read from the database 111 | int tries = 0; 112 | while (tries < 300 && pollDummyEntity(data)) { 113 | Thread.sleep(1000); // 1 second 114 | tries++; 115 | } 116 | 117 | Assert.assertTrue("Scheduled task didn't execute in 5 minutes time", tries < 300); 118 | } 119 | 120 | 121 | @Transactional 122 | public void enqueueSimpleTask(String data) { 123 | taskExecutor.execute( new SimpleTask(data)); 124 | } 125 | 126 | @Transactional 127 | public void scheduleSimpleTask(String data) { 128 | SimpleTask st = new SimpleTask(data); 129 | Calendar trigger = Calendar.getInstance(); 130 | trigger.add(Calendar.SECOND, 10); 131 | st.setTriggerStamp(trigger); 132 | taskExecutor.execute(st); 133 | } 134 | 135 | @Transactional 136 | public boolean pollDummyEntity(String data) { 137 | return !this.dummyEntityDao.findByData(data).isEmpty(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/QueuedTaskHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async; 17 | 18 | import java.text.SimpleDateFormat; 19 | import java.util.Calendar; 20 | import java.util.TimeZone; 21 | import java.util.UUID; 22 | import javax.persistence.Column; 23 | import javax.persistence.Entity; 24 | import javax.persistence.Id; 25 | import javax.persistence.Lob; 26 | import javax.persistence.PrePersist; 27 | import javax.persistence.Table; 28 | import javax.persistence.Temporal; 29 | import javax.persistence.TemporalType; 30 | import javax.persistence.Version; 31 | import javax.validation.constraints.NotNull; 32 | import javax.validation.constraints.Past; 33 | import org.springframework.core.style.ToStringCreator; 34 | 35 | 36 | /** 37 | * Persistent entity that stores an async task. 38 | * 39 | * @author Carlos Vara 40 | */ 41 | @Entity 42 | @Table(name="TASK_QUEUE") 43 | public class QueuedTaskHolder { 44 | 45 | // Getters ----------------------------------------------------------------- 46 | 47 | @Id 48 | public String getId() { 49 | if ( this.id == null ) { 50 | this.setId(UUID.randomUUID().toString()); 51 | } 52 | return this.id; 53 | } 54 | 55 | @NotNull 56 | @Past 57 | @Temporal(TemporalType.TIMESTAMP) 58 | @Column(name="CREATION_STAMP") 59 | public Calendar getCreationStamp() { 60 | return this.creationStamp; 61 | } 62 | 63 | @Temporal(TemporalType.TIMESTAMP) 64 | @Column(name="TRIGGER_STAMP") 65 | public Calendar getTriggerStamp() { 66 | return triggerStamp; 67 | } 68 | 69 | @Past 70 | @Temporal(TemporalType.TIMESTAMP) 71 | @Column(name="STARTED_STAMP") 72 | public Calendar getStartedStamp() { 73 | return this.startedStamp; 74 | } 75 | 76 | @Past 77 | @Temporal(TemporalType.TIMESTAMP) 78 | @Column(name="COMPLETED_STAMP") 79 | public Calendar getCompletedStamp() { 80 | return this.completedStamp; 81 | } 82 | 83 | @Lob 84 | @NotNull 85 | @Column(name="SERIALIZED_TASK") 86 | public byte[] getSerializedTask() { 87 | return this.serializedTask; 88 | } 89 | 90 | @Version 91 | @Column(name="OPTLOCK") 92 | protected int getVersion() { 93 | return this.version; 94 | } 95 | 96 | 97 | // Setters ----------------------------------------------------------------- 98 | 99 | protected void setId(String id) { 100 | this.id = id; 101 | } 102 | 103 | public void setCreationStamp(Calendar creationStamp) { 104 | this.creationStamp = creationStamp; 105 | } 106 | 107 | public void setTriggerStamp(Calendar triggerStamp) { 108 | this.triggerStamp = triggerStamp; 109 | } 110 | 111 | public void setStartedStamp(Calendar startedStamp) { 112 | this.startedStamp = startedStamp; 113 | } 114 | 115 | public void setCompletedStamp(Calendar completedStamp) { 116 | this.completedStamp = completedStamp; 117 | } 118 | 119 | public void setSerializedTask(byte[] serializedTask) { 120 | this.serializedTask = serializedTask; 121 | } 122 | 123 | protected void setVersion(int version) { 124 | this.version = version; 125 | } 126 | 127 | 128 | // Fields ------------------------------------------------------------------ 129 | 130 | private String id; 131 | private Calendar creationStamp; 132 | private Calendar triggerStamp = null; 133 | private Calendar startedStamp = null; 134 | private Calendar completedStamp = null; 135 | private byte[] serializedTask; 136 | private int version; 137 | 138 | 139 | // Lifecycle events -------------------------------------------------------- 140 | 141 | @SuppressWarnings("unused") 142 | @PrePersist 143 | private void onAbstractBaseEntityPrePersist() { 144 | this.ensureId(); 145 | this.markCreation(); 146 | } 147 | 148 | /** 149 | * Ensures that the entity has a unique UUID. 150 | */ 151 | private void ensureId() { 152 | this.getId(); 153 | } 154 | 155 | /** 156 | * Sets the creation stamp to now. 157 | */ 158 | private void markCreation() { 159 | setCreationStamp(Calendar.getInstance(TimeZone.getTimeZone("Etc/UTC"))); 160 | } 161 | 162 | 163 | // Methods ----------------------------------------------------------------- 164 | 165 | @Override 166 | public String toString() { 167 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z"); 168 | return new ToStringCreator(this).append("id", getId()) 169 | .append("creationStamp", (getCreationStamp()!=null)?sdf.format(getCreationStamp().getTime()):null) 170 | .append("startedStamp", (getStartedStamp()!=null)?sdf.format(getStartedStamp().getTime()):null) 171 | .append("completedStamp", (getCompletedStamp()!=null)?sdf.format(getCompletedStamp().getTime()):null) 172 | .toString(); 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/net/carinae/dev/async/PersistentTaskExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Carlos Vara 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package net.carinae.dev.async; 17 | 18 | import java.util.Calendar; 19 | import java.util.TimeZone; 20 | import net.carinae.dev.async.dao.QueuedTaskHolderDao; 21 | import net.carinae.dev.async.task.AbstractBaseTask; 22 | import net.carinae.dev.async.util.Serializer; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.beans.factory.annotation.Autowired; 27 | import org.springframework.core.task.TaskExecutor; 28 | import org.springframework.dao.OptimisticLockingFailureException; 29 | import org.springframework.scheduling.annotation.Scheduled; 30 | import org.springframework.stereotype.Component; 31 | import org.springframework.transaction.annotation.Propagation; 32 | import org.springframework.transaction.annotation.Transactional; 33 | 34 | /** 35 | * A task executor with persistent task queueing. 36 | * 37 | * @author Carlos Vara 38 | */ 39 | @Component("PersistentExecutor") 40 | public class PersistentTaskExecutor implements TaskExecutor { 41 | 42 | final static Logger logger = LoggerFactory.getLogger(PersistentTaskExecutor.class); 43 | 44 | 45 | @Autowired 46 | protected QueuedTaskHolderDao queuedTaskDao; 47 | 48 | @Autowired 49 | protected Serializer serializer; 50 | 51 | 52 | /** 53 | * Additional requirement: must be run inside a transaction. 54 | * Currently using MANDATORY as Bounty won't create tasks outside a 55 | * transaction. 56 | * 57 | * @see org.springframework.core.task.TaskExecutor#execute(java.lang.Runnable) 58 | */ 59 | @Override 60 | @Transactional(propagation=Propagation.MANDATORY) 61 | public void execute(Runnable task) { 62 | 63 | logger.debug("Trying to enqueue: {}", task); 64 | 65 | AbstractBaseTask abt; 66 | try { 67 | abt = AbstractBaseTask.class.cast(task); 68 | } catch (ClassCastException e) { 69 | logger.error("Only runnables that extends AbstractBaseTask are accepted."); 70 | throw new IllegalArgumentException("Invalid task: " + task); 71 | } 72 | 73 | // Serialize the task 74 | QueuedTaskHolder newTask = new QueuedTaskHolder(); 75 | byte[] serializedTask = this.serializer.serializeObject(abt); 76 | newTask.setTriggerStamp(abt.getTriggerStamp()); 77 | 78 | logger.debug("New serialized task takes {} bytes", serializedTask.length); 79 | 80 | newTask.setSerializedTask(serializedTask); 81 | 82 | // Store it in the db 83 | this.queuedTaskDao.persist(newTask); 84 | 85 | // POST: Task has been enqueued 86 | } 87 | 88 | 89 | /** 90 | * Runs enqueued tasks. 91 | */ 92 | @Scheduled(fixedRate=Constants.TASK_RUNNER_RATE) 93 | public void runner() { 94 | 95 | logger.debug("Started runner {}", Thread.currentThread().getName()); 96 | 97 | QueuedTaskHolder lockedTask = null; 98 | 99 | // While there is work to do... 100 | while ( (lockedTask = tryLockTask()) != null ) { 101 | 102 | logger.debug("Obtained lock on {}", lockedTask); 103 | 104 | // Deserialize the task 105 | AbstractBaseTask runnableTask = this.serializer.deserializeAndCast(lockedTask.getSerializedTask()); 106 | runnableTask.setQueuedTaskId(lockedTask.getId()); 107 | 108 | // Run it 109 | runnableTask.run(); 110 | } 111 | 112 | logger.debug("Finishing runner {}, nothing else to do.", Thread.currentThread().getName()); 113 | } 114 | 115 | 116 | /** 117 | * The hypervisor re-queues for execution possible stalled tasks. 118 | */ 119 | @Scheduled(fixedRate=Constants.TASK_HYPERVISOR_RATE) 120 | public void hypervisor() { 121 | 122 | logger.debug("Started hypervisor {}", Thread.currentThread().getName()); 123 | 124 | // Reset stalled threads, one at a time to avoid too wide transactions 125 | while ( tryResetStalledTask() ); 126 | 127 | logger.debug("Finishing hypervisor {}, nothing else to do.", Thread.currentThread().getName()); 128 | } 129 | 130 | 131 | /** 132 | * Tries to ensure a lock on a task in order to execute it. 133 | * 134 | * @return A locked task, or null if there is no task available 135 | * or no lock could be obtained. 136 | */ 137 | private QueuedTaskHolder tryLockTask() { 138 | 139 | int tries = 3; 140 | 141 | QueuedTaskHolder ret = null; 142 | while ( tries > 0 ) { 143 | try { 144 | ret = obtainLockedTask(); 145 | return ret; 146 | } catch (OptimisticLockingFailureException e) { 147 | tries--; 148 | } 149 | } 150 | 151 | return null; 152 | } 153 | 154 | /** 155 | * Tries to reset a stalled task. 156 | * 157 | * @return true if one task was successfully re-queued, 158 | * false if no task was re-queued, either because there 159 | * are no stalled tasks or because there was a conflict re-queueing 160 | * it. 161 | */ 162 | private boolean tryResetStalledTask() { 163 | int tries = 3; 164 | 165 | QueuedTaskHolder qt = null; 166 | while ( tries > 0 ) { 167 | try { 168 | qt = resetStalledTask(); 169 | return qt != null; 170 | } catch (OptimisticLockingFailureException e) { 171 | tries--; 172 | } 173 | } 174 | 175 | return false; 176 | } 177 | 178 | 179 | /** 180 | * @return A locked task ready for execution, null if no ready 181 | * task is available. 182 | * @throws OptimisticLockingFailureException 183 | * If getting the lock fails. 184 | */ 185 | @Transactional 186 | public QueuedTaskHolder obtainLockedTask() { 187 | QueuedTaskHolder qt = this.queuedTaskDao.findNextTaskForExecution(); 188 | logger.debug("Next possible task for execution {}", qt); 189 | if ( qt != null ) { 190 | qt.setStartedStamp(Calendar.getInstance(TimeZone.getTimeZone("etc/UTC"))); 191 | } 192 | return qt; 193 | } 194 | 195 | 196 | /** 197 | * Tries to reset a stalled task, returns null if no stalled task was reset. 198 | * 199 | * @return The re-queued task, null if no stalled task is 200 | * available. 201 | * @throws OptimisticLockingFailureException 202 | * If the stalled task is modified by another thread during 203 | * re-queueing. 204 | */ 205 | @Transactional 206 | public QueuedTaskHolder resetStalledTask() { 207 | QueuedTaskHolder stalledTask = this.queuedTaskDao.findRandomStalledTask(); 208 | logger.debug("Obtained this stalledTask {}", stalledTask); 209 | if ( stalledTask != null ) { 210 | stalledTask.setStartedStamp(null); 211 | } 212 | return stalledTask; 213 | } 214 | 215 | 216 | } 217 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 4.0.0 21 | 22 | net.carinae.dev 23 | async-tasks 24 | jar 25 | 1.0 26 | Async-Tasks 27 | 28 | 29 | 0.1-incubating 30 | 1.6.8 31 | 1.2.2 32 | 1.0.0.GA 33 | 4.7 34 | 1.2.129 35 | 3.5.3-Final 36 | 0.9.18 37 | 1.5.8 38 | 3.0.3.RELEASE 39 | 40 | 41 | 42 | 43 | org.aspectj 44 | aspectjrt 45 | ${aspectj.version} 46 | 47 | 48 | org.aspectj 49 | aspectjweaver 50 | ${aspectj.version} 51 | 52 | 53 | org.hibernate 54 | hibernate-core 55 | ${hibernate.version} 56 | 57 | 58 | org.hibernate 59 | hibernate-annotations 60 | ${hibernate.version} 61 | 62 | 63 | org.hibernate 64 | hibernate-entitymanager 65 | ${hibernate.version} 66 | 67 | 68 | org.apache.bval 69 | bval-jsr303 70 | ${apache-beanvalidator.version} 71 | 72 | 73 | commons-logging 74 | commons-logging 75 | 76 | 77 | 78 | 79 | javax.validation 80 | validation-api 81 | ${javax-validation.version} 82 | 83 | 84 | org.springframework 85 | spring-aspects 86 | ${springframework.version} 87 | 88 | 89 | commons-logging 90 | commons-logging 91 | 92 | 93 | 94 | 95 | org.springframework 96 | spring-context-support 97 | ${springframework.version} 98 | 99 | 100 | org.springframework 101 | spring-orm 102 | ${springframework.version} 103 | 104 | 105 | junit 106 | junit 107 | ${junit.version} 108 | test 109 | 110 | 111 | org.springframework 112 | spring-test 113 | ${springframework.version} 114 | test 115 | 116 | 117 | commons-dbcp 118 | commons-dbcp 119 | ${commons-dbcp.version} 120 | runtime 121 | 122 | 123 | org.slf4j 124 | jcl-over-slf4j 125 | ${slf4j.version} 126 | runtime 127 | 128 | 129 | ch.qos.logback 130 | logback-classic 131 | ${logback.version} 132 | runtime 133 | 134 | 135 | com.h2database 136 | h2 137 | ${h2database.version} 138 | runtime 139 | 140 | 141 | 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-compiler-plugin 147 | 148 | 1.6 149 | 1.6 150 | -proc:none 151 | 152 | 153 | 154 | org.bsc.maven 155 | maven-processor-plugin 156 | 1.3.5 157 | 158 | 159 | process 160 | 161 | process 162 | 163 | generate-sources 164 | 165 | target/metamodel 166 | 167 | 168 | 169 | 170 | 171 | org.hibernate 172 | hibernate-jpamodelgen 173 | 1.0.0.Final 174 | compile 175 | 176 | 177 | 178 | 179 | org.codehaus.mojo 180 | build-helper-maven-plugin 181 | 1.3 182 | 183 | 184 | add-source 185 | generate-sources 186 | 187 | add-source 188 | 189 | 190 | 191 | target/metamodel 192 | 193 | 194 | 195 | 196 | 197 | 198 | org.codehaus.mojo 199 | aspectj-maven-plugin 200 | 1.3 201 | 202 | true 203 | false 204 | 1.6 205 | 206 | 207 | org.springframework 208 | spring-aspects 209 | 210 | 211 | 212 | 213 | 214 | 215 | compile 216 | test-compile 217 | 218 | 219 | 220 | 221 | 222 | org.aspectj 223 | aspectjtools 224 | ${aspectj.version} 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | jboss-public-repository-group 234 | JBoss Public Maven Repository Group 235 | https://repository.jboss.org/nexus/content/groups/public/ 236 | default 237 | 238 | true 239 | never 240 | fail 241 | 242 | 243 | 244 | springsource-release 245 | http://repository.springsource.com/maven/bundles/release 246 | 247 | 248 | springsource-external 249 | http://repository.springsource.com/maven/bundles/external 250 | 251 | 252 | 253 | 254 | 255 | Maven annotation plugin 256 | http://maven-annotation-plugin.googlecode.com/svn/trunk/mavenrepo 257 | 258 | 259 | JFrog maven plugins 260 | http://www.jfrog.org/artifactory/plugins-releases 261 | 262 | 263 | 264 | 265 | --------------------------------------------------------------------------------