/* * Copyright 2008 Niels Peter Strandberg. * * 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 com.github.Allogy.simplemq; import com.github.Allogy.simplemq.config.MessageQueueConfig; import com.github.Allogy.simplemq.config.PersistentMessageQueueConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.lang.ref.WeakReference; import java.sql.*; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class MessageQueueImp implements MessageQueue, Serializable { private static final long serialVersionUID = 4688999278205140460L; private static Logger log = LoggerFactory.getLogger(MessageQueue.class); //???: wouldn't a timer be better here? private transient final Timer timer; //Use Memory support like BlockingQueue or Here use memory database. //内存数据库和内存不同的是: 数据库将对象表示成表的形式, 而内存直接存放对象. --> like ORM mapping need db as middle man private transient Connection conn; private final MessageQueueConfig queueConfig; private final String queueName; //should we use volatile for multi thread? private boolean shutdown; private boolean deleted; private transient final Lock lock = new ReentrantLock(); //TODO: maybe make this configurable per-queue? public static boolean INSTALL_SHUTDOWN_HOOK = true; /** * The shutdown thread for the queue. We have to unregistrer it * when we shutdown the database, or else the GC cannot remove it. */ private transient Thread shutdownHook; /** * Constructs a message queue instance that is not controled by * the {@link MessageQueueService}. Will normaly only be used * by a remoting framework. For at better JVM local usage, use the * MessageQueueService. * * @see MessageQueueService */ public MessageQueueImp() { this("noname", new MessageQueueConfig()); } /** * Constructs a message queue instance. Normaly only use by the {@link MessageQueueService}. * For message queues that is JVM local, use the {@link MessageQueueService}. * * @param queueName - should be unique in the MessageQueueService * @see MessageQueueService */ public MessageQueueImp(String queueName) { this(queueName, new MessageQueueConfig()); } /** * Constructs a message queue instance. Normaly only use by the {@link MessageQueueService}. * For message queues that is JVM local, use the {@link MessageQueueService}. * * @param queueName - should be unique in the MessageQueueService * @param queueConfig - configur the message queue * @see MessageQueueService */ public MessageQueueImp(String queueName, MessageQueueConfig queueConfig) { this.queueName = queueName; this.queueConfig = (MessageQueueConfig) Utils.copy(queueConfig); this.timer = new Timer(queueName+"-message-queue-timer", true); boolean hsqldbapplog = queueConfig.getHsqldbapplog(); try { Class.forName("org.hsqldb.jdbcDriver").newInstance(); //WTF: 内存和磁盘都采用hsqldb: 不同的是jdbc:hsqldb:file和jdbc:hsqldb:mem if (queueConfig instanceof PersistentMessageQueueConfig) { PersistentMessageQueueConfig pqc = (PersistentMessageQueueConfig) queueConfig; String cacheDirectory = (pqc.getDatabaseDirectory() == null) ? "" : pqc.getDatabaseDirectory(); conn = DriverManager.getConnection("jdbc:hsqldb:file:" + cacheDirectory + "queues/" + queueName + "/" + queueName + (hsqldbapplog ? ";hsqldb.applog=1" : ""), "sa", ""); } else { conn = DriverManager.getConnection("jdbc:hsqldb:mem:" + queueName + (hsqldbapplog ? ";hsqldb.applog=1" : ""), "sa", ""); } //初始化数据库 createOrMigrateDatabaseStructure(); } catch (Exception e) { throw new RuntimeException("unable to initialize "+queueName+" message queue", e); } setWriteDelay(); startQueueMaintainers(); //In some cases, this same queue is needed for another shutdown hook, or is otherwise unwanted. if (INSTALL_SHUTDOWN_HOOK) { // 'shutdownhook' is called when the JVM shuts down. // Used here to make sure we shutdown properly. shutdownHook = new RelayVMShutdown(this); Runtime.getRuntime().addShutdownHook(shutdownHook); } } public String getQueueName() { return queueName; } //关闭线程 private static class RelayVMShutdown extends Thread { //弱引用 final WeakReference<MessageQueueImp> messageQueue; RelayVMShutdown(MessageQueueImp messageQueue) { this.messageQueue = new WeakReference<MessageQueueImp>(messageQueue); } public void run() { log.info("VM is shutting down, relaying to HSQL DB"); //在内部类了, 要获得外部类, 使用对象的get方法. 而不能用this. MessageQueueImp mq = messageQueue.get(); if (mq == null) { log.info("already gone away"); } else { //Set shutdown to null so that shutdown will not try and unregister it (esp. since we can't, at this point) mq.shutdownHook=null; mq.shutdown(); } }; } private void mustNotBeShutdown() { if (this.shutdown) { throw new IllegalStateException("message queue is shutdown"); } } //发送一批消息 public void send(Collection<Message> messageInputs) { mustNotBeShutdown(); RuntimeException exception=null; for (Message messageInput : messageInputs) { try { send(messageInput); } catch (RuntimeException e) { if (exception==null) { exception = e; } else { log.error("error sending multiple messages", e); } } } if (exception!=null) { throw exception; } } //发送一条消息 public void send(Message messageInput) { mustNotBeShutdown(); if (messageInput == null) { throw new NullPointerException("The messageInput cannot be 'null'"); } //序列化消息 byte[] b = null; if (messageInput.getObject() != null) { b = Utils.serialize(messageInput.getObject()); } long startDelay=messageInput.getStartDelay(); if (startDelay<0) startDelay=0; long time=System.currentTimeMillis()+startDelay; //重复的key, 表示队列中已经存在这个消息了. String dupeKey=messageInput.getDuplicateSuppressionKey(); //有冲突了,怎么解决? OnCollision onCollision=messageInput.getDuplicateSuppressionAction(); //有重复的消息, 这个是旧的消息 MessageWrapper existing=null; lock.lock(); try { if (dupeKey!=null) { //找出已经存在的旧的消息. peek按照队列是取队列头, 在这里因为用内存数据库,直接select! existing = peek(dupeKey); if (existing!=null) switch (onCollision) { //降级: 如何实现将消息移动到队列尾部: //更新时间为当前时间,查询时按照时间升序排列,在队列尾部的消息排序后在查询结果的最后 case DEMOTE : //new message is dropped, but update existing with new time touchMessage(existing, time); //fall through... //丢弃新消息, 不处理 case DROP : //no-op, new message is dropped. log.debug("onCollision({}) drops new message on {}", onCollision, dupeKey); return; //no new message //排除新旧消息, 新消息不处理, 删除旧的消息 case EXCLUDE: log.debug("onCollision(EXCLUDE) drops both messages: {}", dupeKey); delete(existing); return; //no new message case REPLACE: //will delete existing later break; //交换: 更新记录的时间 case SWAP: time=existing.getTime(); //will delete existing later break; } } try { //发送一条消息,往数据库中添加一条记录. 标记read=false, 并且time字段表示消息的到达时间 PreparedStatement ps = conn.prepareStatement("INSERT INTO message (body, time, read, object, dupeKey, onCollision) VALUES(?, ?, ?, ?, ?, ?)"); ps.setString(1, messageInput.getBody()); ps.setLong(2, time); ps.setBoolean(3, false); if (b == null) { ps.setNull(4, Types.BLOB); } else { ps.setBinaryStream(4, new ByteArrayInputStream(b), b.length); } ps.setString(5, messageInput.getDuplicateSuppressionKey()); if (onCollision==null) { ps.setNull(6, Types.VARCHAR); } else { ps.setString(6, onCollision.toString()); } ps.executeUpdate(); ps.close(); } catch (SQLException e) { throw new RuntimeException("unable to save message", e); } //删除旧消息的冲突状态包括了: EXCLUDE(前面已经处理过了), SWAP, REPLACE if (existing!=null && (onCollision==OnCollision.SWAP || onCollision==OnCollision.REPLACE)) { log.debug("onCollision({}) deletes existing message: {}", onCollision, dupeKey); delete(existing); } } finally { lock.unlock(); } } //touch: touch is non free operation. u touched, so will update the message's time //Like touch a file in linux, if file not exist, will create a file //if file already exist, it will update the file recently update time //When u touch a girl. It's also non-free. u touch her, u get back the girl's response.well,the resp depends. private void touchMessage(MessageWrapper existing, long time) { try { //更新消息的时间, 可以表示为消息的到达时间,用于模拟队列怎么按照时间取出数据处理 PreparedStatement updateInventory = conn.prepareStatement("UPDATE message SET time=? WHERE id=?"); updateInventory.setLong(1, time); updateInventory.setLong(2, existing.getId()); updateInventory.executeUpdate(); updateInventory.close(); } catch (SQLException e) { throw new RuntimeException(e); } } public Message receiveAndDelete() { mustNotBeShutdown(); List<Message> messages = this.receiveInternal(1, true); if (messages.size() > 0) { return messages.get(0); } else { return null; } } public List<Message> receiveAndDelete(int limit) { mustNotBeShutdown(); return receiveInternal(limit, true); } public Message receive() { mustNotBeShutdown(); List<Message> messages = this.receiveInternal(1, false); if (messages.size() > 0) { return messages.get(0); } else { return null; } } public List<Message> receive(int limit) { mustNotBeShutdown(); return receiveInternal(limit, false); } public Message peek() { mustNotBeShutdown(); List<Message> messages = this.peekInternal(1); if (messages.size() > 0) { return messages.get(0); } else { return null; } } public List<Message> peek(int limit) { mustNotBeShutdown(); return peekInternal(limit); } private List<Message> peekInternal(int limit) { if (limit < 1) limit = 1; List<Message> messages = new ArrayList<Message>(limit); try { //选择还没读过的消息,说明消息还没处理 // 'ORDER BY time' depends on that the host computer times is always right. // 'ORDER BY id' what happens with the 'id' when we hit Long.MAX_VALUE? PreparedStatement ps = conn.prepareStatement("SELECT LIMIT 0 " + limit +MessageWrapper.RS_FIELDS+ " FROM message WHERE read=false ORDER BY time"); // The lock is making sure, that a SELECT and DELETE/UPDATE is only // done by one thread at a time. No update means no lock is needed //lock.lock(); ResultSet rs = ps.executeQuery(); while (rs.next()) { MessageWrapper mw=MessageWrapper.fromResultSet(rs); if (!queueTimeIsInTheFuture(mw)) { messages.add(mw); } } ps.close(); } catch (SQLException e) { throw new RuntimeException(e); } finally { //lock.unlock(); } return messages; } private boolean queueTimeIsInTheFuture(MessageWrapper mw) { long now=System.currentTimeMillis(); return (mw.getTime() > now); } /** * Returns the first/only message with the given duplicate-suppression key, * even if it's delayed-start time has not yet passed. * * @param dupeKey * @return */ public MessageWrapper peek(String dupeKey) { mustNotBeShutdown(); try { PreparedStatement ps = conn.prepareStatement("SELECT LIMIT 0 1" + MessageWrapper.RS_FIELDS+ " FROM message WHERE dupeKey = ?"); ps.setString(1, dupeKey); ResultSet rs = ps.executeQuery(); MessageWrapper mw=null; if (rs.next()) { mw=MessageWrapper.fromResultSet(rs); } rs.close(); ps.close(); return mw; } catch (SQLException e) { throw new RuntimeException("could not locate message by duplicate suppression key: "+e, e); } } private List<Message> receiveInternal(int limit, boolean delete) { if (limit < 1) limit = 1; List<Message> messages = new ArrayList<Message>(limit); try { // 'ORDER BY time' depends on that the host computer times is always right, but it lets us use delayed messages, etc... // 'ORDER BY id' what happens with the 'id' when we hit Long.MAX_VALUE? ( the heat-death of the universe? ) PreparedStatement ps = conn.prepareStatement("SELECT LIMIT 0 " + limit + MessageWrapper.RS_FIELDS + "FROM message WHERE read=false ORDER BY time"); // The lock is making sure, that a SELECT and DELETE/UPDATE is only // done by one thread at a time. lock.lock(); ResultSet rs = ps.executeQuery(); while (rs.next()) { MessageWrapper mw=MessageWrapper.fromResultSet(rs); if (queueTimeIsInTheFuture(mw)) continue; long id = mw.getId(); //处理完消息,是否要从队列中删除消息 if (delete) { PreparedStatement updateInventory = conn.prepareStatement("DELETE FROM message WHERE id=?"); updateInventory.setLong(1, id); updateInventory.executeUpdate(); } else { //如果没有删除,则要标记read=true,表示已经处理过该消息了 PreparedStatement updateInventory = conn.prepareStatement("UPDATE message SET read=? WHERE id=?"); updateInventory.setBoolean(1, true); updateInventory.setLong(2, id); updateInventory.executeUpdate(); } messages.add(mw); } ps.close(); } catch (SQLException e) { throw new RuntimeException(e); } finally { lock.unlock(); } return messages; } public boolean delete(List<Message> messages) { mustNotBeShutdown(); try { conn.setAutoCommit(false); Statement stmt = conn.createStatement(); for (Message message : messages) { if (!(message instanceof MessageWrapper)) { throw new IllegalArgumentException("This instance of 'Message' is not valid."); } stmt.addBatch("DELETE FROM message WHERE id=" + message.getId()); } int[] updateCounts = stmt.executeBatch(); // Have we deleted them all if (updateCounts.length == messages.size()) { return true; } else { log.error("Not all Messages was deleted! Only {} out of {} were deleted!", updateCounts.length, messages.size()); } } catch (SQLException e) { throw new RuntimeException(e); } finally { try { conn.setAutoCommit(true); } catch (SQLException e) { log.error("unable to restore autocommit", e); } } return false; } public boolean delete(Message message) { mustNotBeShutdown(); // The Message instances this queue returns is and instance of 'MessageWrapper', so we only accept them. if (!(message instanceof MessageWrapper)) { throw new IllegalArgumentException("This instance of 'Message' is not valid."); } try { PreparedStatement ps = conn.prepareStatement("DELETE FROM message WHERE id=?"); ps.setLong(1, message.getId()); int i = ps.executeUpdate(); ps.close(); return (i > 0); } catch (SQLException e) { throw new RuntimeException(e); } } public void deleteQueue() { if (deleted) return; deleted = true; shutdown(); // if we have a persistent queue, delete the files. if (queueConfig instanceof PersistentMessageQueueConfig) { PersistentMessageQueueConfig pqc = (PersistentMessageQueueConfig) queueConfig; String cacheDirectory = (pqc.getDatabaseDirectory() == null) ? "" : pqc.getDatabaseDirectory(); File queues=new File(cacheDirectory + "queues"); Utils.deleteDirectory(new File(queues, queueName)); String[] remaining=queues.list(); if (remaining==null || remaining.length==0) { queues.delete(); } } } public MessageQueueConfig getMessageQueueConfig() { return queueConfig; } public boolean deleted() { return deleted; } public long messageCount() { return unreadMessageCount(); } public int unreadMessageCount() { mustNotBeShutdown(); int count = -1; try { PreparedStatement ps = conn.prepareStatement("SELECT COUNT(id) FROM message WHERE read=?"); ps.setBoolean(1, false); ResultSet rs = ps.executeQuery(); rs.next(); count = rs.getInt(1); ps.close(); } catch (SQLException e) { throw new RuntimeException(e); } return count; } public int totalMessageCount() { mustNotBeShutdown(); int count = -1; try { PreparedStatement ps = conn.prepareStatement("SELECT COUNT(id) FROM message"); ResultSet rs = ps.executeQuery(); rs.next(); count = rs.getInt(1); ps.close(); } catch (SQLException e) { throw new RuntimeException(e); } return count; } public boolean isPersistent() { return (queueConfig instanceof PersistentMessageQueueConfig); } private void createOrMigrateDatabaseStructure() throws SQLException { Statement st = conn.createStatement(); int currentVersion=readDatabaseVersion(st); String cached = ""; //NB: once set, the CACHED will be indelible, until the queue is deleted. An original design/flaw. if (queueConfig instanceof PersistentMessageQueueConfig) { PersistentMessageQueueConfig pqc = (PersistentMessageQueueConfig) queueConfig; if (pqc.isCached()) { cached = "CACHED "; } } final int LATEST=1400; log.debug("db on disk is version {}, latest is {}", currentVersion, LATEST); switch (currentVersion) { case 0: st.execute("CREATE " + cached + "TABLE message (id BIGINT IDENTITY PRIMARY KEY, object LONGVARBINARY, body VARCHAR, time BIGINT, read BOOLEAN)"); st.execute("CREATE INDEX id_index ON message(id)"); st.execute("CREATE INDEX time_index ON message(time)"); st.execute("CREATE INDEX read_index ON message(read)"); case 1330: st.execute("CREATE TABLE meta (version INTEGER);"); case 1331: st.execute("INSERT INTO meta (version) VALUES (1340);"); case 1340: st.execute("ALTER TABLE message ADD COLUMN dupeKey VARCHAR;"); st.execute("ALTER TABLE message ADD COLUMN onCollision VARCHAR;"); st.execute("CREATE INDEX dupe_index ON message(dupeKey)"); /* --------------------------------------------------------------------------------------- new migrations go above this line, with *current* value of LATEST, and should fall-through. Remember to then update LATEST to resemble the current version number (plus a digit for some wiggle-room between versions). --------------------------------------------------------------------------------------- */ st.execute("UPDATE meta SET version = "+LATEST+";"); log.info("simple-mq database updated to version {}", LATEST); break; case LATEST: log.debug("simple-mq database is up-to-date"); break; default: log.warn("simple-mq database (on-disk) is not a supported version: {}, this is version {}", currentVersion, LATEST); //TODO: maybe have an option to die here, I favor continuing... } st.close(); } private int readDatabaseVersion(Statement st) throws SQLException { if (tableExists("meta")) { log.debug("meta table exists"); /* read the integer -- For reasons that are not quite clear, the "LIMIT 1" syntax (which *is* a bit redundant) fails to parse: java.sql.SQLException: Unexpected token: 1 in statement [1] at org.hsqldb.jdbc.Util.sqlException(Unknown Source) at org.hsqldb.jdbc.jdbcStatement.fetchResult(Unknown Source) at org.hsqldb.jdbc.jdbcStatement.executeQuery(Unknown Source) at MessageQueueImp.readDatabaseVersion(MessageQueueImp.java:648) at MessageQueueImp.createOrMigrateDatabaseStructure(MessageQueueImp.java:581) at MessageQueueImp.<init>(MessageQueueImp.java:111) ... 109 more */ //ResultSet resultSet = st.executeQuery("SELECT version FROM meta LIMIT 1"); ResultSet resultSet = st.executeQuery("SELECT version FROM meta"); try { if (resultSet.next()) { int retval=resultSet.getInt(1); log.debug("on-disk simple-mq database is at version {}", retval); return retval; } else { return 1331; //aka... need the meta row. } } finally { resultSet.close(); } } else if (tableExists("message")) { log.debug("meta table does not exist, but message table does"); //pre-meta table return 1330; } else { log.debug("database appears to be empty"); //empty database return 0; } } // To check if a 'tableName' table allready exsists in the db. private boolean tableExists(String tableName) throws SQLException { PreparedStatement stmt = null; ResultSet results = null; try { stmt = conn.prepareStatement("SELECT COUNT(*) FROM " + tableName); results = stmt.executeQuery(); return true; // if table does exist, no rows will ever be returned } catch (SQLException e) { return false; // if table does not exist, an exception will be thrown } finally { if (results != null) { results.close(); } if (stmt != null) { stmt.close(); } } } private void setWriteDelay() { if (queueConfig instanceof PersistentMessageQueueConfig) { PersistentMessageQueueConfig pqc = (PersistentMessageQueueConfig) queueConfig; try { Statement st = conn.createStatement(); st.execute("SET WRITE_DELAY " + pqc.getDatabaseWriteDelay() + " MILLIS"); st.close(); } catch (SQLException e) { throw new RuntimeException(e); } } } // A 'SHUTDOWN' command is send to the db, so that // it can flush changes and cleanup before the JVM halts. public void shutdown() { if (shutdown) { log.debug("{} message queue already shutdown", queueName); return; } shutdown=true; timer.cancel(); if (shutdownHook != null) { Runtime.getRuntime().removeShutdownHook(shutdownHook); shutdownHook = null; } try { Statement st = conn.createStatement(); st.execute("SHUTDOWN"); st.close(); conn.close(); } catch (SQLException e) { log.error("could not shutdown hsqldb", e); } try { conn.isClosed(); } catch (SQLException e) { log.warn("can not close connection", e); } } @Override public void decommissionQueue(MessageVisitor messageVisitor) { if (messageVisitor==null) throw new NullPointerException(); this.shutdown=true; timer.cancel(); List<Message> messages = new ArrayList<Message>(); try { PreparedStatement ps = conn.prepareStatement("SELECT " + MessageWrapper.RS_FIELDS + " FROM message ORDER BY time"); ResultSet rs = ps.executeQuery(); while (rs.next()) { MessageWrapper mw=MessageWrapper.fromResultSet(rs); messages.add(mw); } ps.close(); } catch (SQLException e) { throw new RuntimeException(e); } try { messageVisitor.visit(messages); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } deleteQueue(); } // 'QueueMaintainers' are 2 background threads. 队列维护有2个后台线程. 一个用于删除太旧的消息. 一个用于恢复已经读过,还没删除的消息 // One deletes 'too old' messages, and the other 'revives' messages that has been read, but not deleted. private void startQueueMaintainers() { long oldDelay=TimeUnit.SECONDS.toMillis(queueConfig.getDeleteOldMessagesThreadDelay()); timer.schedule(new DeleteOldTimerTask(this), oldDelay, oldDelay); long threadDelay=TimeUnit.SECONDS.toMillis(queueConfig.getReviveNonDeletedMessagsThreadDelay()); timer.schedule(new ReviveTimerTask(this), threadDelay, threadDelay); } private void deleteOldMessages() { log.debug("delete old messages"); try { PreparedStatement ps = conn.prepareStatement("SELECT id FROM message WHERE time<?"); ps.setLong(1, System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(queueConfig.getMessageRemoveTime())); ResultSet rs = ps.executeQuery(); List<Long> ids = new ArrayList<Long>(); while (rs.next()) { ids.add(rs.getLong(1)); } ps.close(); ps = conn.prepareStatement("DELETE FROM message WHERE id=?"); for (Long id : ids) { ps.setLong(1, id); ps.executeUpdate(); log.info("message #{} has expired", id); } ps.close(); } catch (SQLException e) { throw new RuntimeException(e); } } private void reviveStaleMessage() { log.debug("reviving stale messages"); //!!!: This operation is technically unsafe, couldn't it be optimized to a single update command? would that make an ever-growing transaction log file? try { PreparedStatement ps = conn.prepareStatement("SELECT id FROM message WHERE time<? AND read=true"); ps.setLong(1, System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(queueConfig.getMessageReviveTime())); ResultSet rs = ps.executeQuery(); List<Long> ids = new ArrayList<Long>(); while (rs.next()) { ids.add(rs.getLong(1)); } ps.close(); if (ids.isEmpty()) { log.debug("no {} messages to revive", queueName); } else { log.info("{} {} messages have been revived", ids.size(), queueName); ps = conn.prepareStatement("UPDATE message SET read=? WHERE id=?"); for (Long id : ids) { ps.setBoolean(1, false); ps.setLong(2, id); ps.executeUpdate(); } ps.close(); } } catch (SQLException e) { throw new RuntimeException(e); } } // A Message returned by this queue, is an instance of MessageWrapper. private static class MessageWrapper implements Message, Serializable { //ivate static final long serialVersionUID = 7465209569623629016L; private static final long serialVersionUID = 7465209569623629017L; private String body; private long id; private long time; private Serializable object; private String duplicateSuppressionKey; private OnCollision duplicateSuppressionAction; private static final String RS_FIELDS=" id, time, body, dupeKey, onCollision, object "; private MessageWrapper() {}; static MessageWrapper fromResultSet(ResultSet rs) throws SQLException { MessageWrapper mw = new MessageWrapper(); mw.id = rs.getLong(1); mw.time = rs.getLong(2); mw.body = rs.getString(3); mw.duplicateSuppressionKey=rs.getString(4); String collision=rs.getString(5); if (collision!=null) { mw.duplicateSuppressionAction=OnCollision.valueOf(collision); } InputStream is = rs.getBinaryStream(6); if (is != null) { try { mw.object = Utils.deserialize(is); } finally { try { is.close(); } catch (IOException e) { //okay. } } } return mw; } public String getBody() { return body; } public Serializable getObject() { return object; } public long getId() { return id; } public String getDuplicateSuppressionKey() { return duplicateSuppressionKey; } public OnCollision getDuplicateSuppressionAction() { return duplicateSuppressionAction; } @Override public long getStartDelay() { return 0; } public long getTime() { return time; } } protected void finalize() throws Throwable { if (!shutdown) { log.error("{} message queue was dereferenced without being shutdown", queueName); shutdown(); } super.finalize(); } private static class DeleteOldTimerTask extends TimerTask { private final WeakReference<MessageQueueImp> messageQueue; public DeleteOldTimerTask(MessageQueueImp messageQueueImp) { messageQueue = new WeakReference<MessageQueueImp>(messageQueueImp); } @Override public void run() { MessageQueueImp messageQueueImp=messageQueue.get(); if (messageQueueImp==null) { log.warn("message queue has gone away"); } else { messageQueueImp.deleteOldMessages(); } } } private static class ReviveTimerTask extends TimerTask { private final WeakReference<MessageQueueImp> messageQueue; public ReviveTimerTask(MessageQueueImp messageQueueImp) { messageQueue = new WeakReference<MessageQueueImp>(messageQueueImp); } @Override public void run() { MessageQueueImp messageQueueImp=messageQueue.get(); if (messageQueueImp==null) { log.warn("message queue has gone away"); } else { messageQueueImp.reviveStaleMessage(); } } } }