/* * $Id$ * * Authors: * Jeff Buchbinder <jeff@freemedsoftware.org> * * REMITT Electronic Medical Information Translation and Transmission * Copyright (C) 1999-2014 FreeMED Software Foundation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.remitt.server; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import org.remitt.datastore.ProcessorStore; import org.remitt.prototype.JobThreadState; import org.remitt.prototype.PayloadDto; import org.remitt.prototype.ProcessorThread; import org.remitt.prototype.ProcessorThread.ThreadType; /** * Singleton control thread which controls work. * * @author jeff@freemedsoftware.org * */ public class ControlThread extends Thread { /** * Exception thrown when there are no free threads available in the * processor thread pool to take a new processing job. * * @author jeff@freemedsoftware.org * */ public class FreeThreadNotFoundException extends Exception { private static final long serialVersionUID = 20090803000L; } static final Logger log = Logger.getLogger(ControlThread.class); protected int SLEEP_TIME = 500; protected MasterControl servletContext = null; protected Map<Long, ProcessorThread> workerThreads = new HashMap<Long, ProcessorThread>(); protected List<JobThreadState> threadPool = new ArrayList<JobThreadState>(); public void run() { Configuration.loadConfiguration(); log.info("Thread [ControlThread] initializing"); SLEEP_TIME = Configuration.getConfiguration().getInt( "remitt.control.sleepTime"); log.info("Initialized with SLEEP_TIME = " + SLEEP_TIME); startChildren(); while (!isInterrupted() && servletContext != null) { try { Thread.sleep(SLEEP_TIME); log.trace("Thread [ControlThread] Waking up after " + SLEEP_TIME + "ms to check for work"); work(); } catch (InterruptedException e) { log.warn(e); stopChildren(); } } } public void setServletContext(MasterControl mc) { servletContext = mc; } public List<JobThreadState> getThreadPool() { return this.threadPool; } /** * Find the current running payload. * * @param threadId * @return */ public Integer getPayloadForThread(Long threadId) { Iterator<JobThreadState> iter = threadPool.iterator(); while (iter.hasNext()) { JobThreadState s = iter.next(); if (s.getProcessorId() == null) { // Skip processing if there's nothing assigned to this continue; } if (s.getProcessorId() == 0) { // Skip processing if there's nothing assigned to this continue; } if (s.getThreadId() == threadId) { // If we find the thread id, return the payload return s.getProcessorId(); } } // If no job, return null return null; } /** * Get an input payload from a tPayload id. * * @return */ public PayloadDto getPayloadById(Integer payloadId) { Connection c = Configuration.getConnection(); PayloadDto payload = new PayloadDto(); PreparedStatement cStmt = null; try { cStmt = c.prepareStatement("SELECT * FROM tPayload WHERE id = ?;"); cStmt.setInt(1, payloadId); cStmt.execute(); ResultSet rs = cStmt.getResultSet(); rs.next(); payload.setId(rs.getInt("id")); payload.setPayload(rs.getBytes("payload")); payload.setRenderPlugin(rs.getString("renderPlugin")); payload.setRenderOption(rs.getString("renderOption")); payload.setTransportPlugin(rs.getString("transportPlugin")); payload.setTransportOption(rs.getString("transportOption")); payload.setUserName(rs.getString("user")); rs.close(); } catch (NullPointerException npe) { log.error("Caught NullPointerException", npe); payload = null; } catch (SQLException e) { log.error("Caught SQLException", e); payload = null; } finally { DbUtil.closeSafely(cStmt); DbUtil.closeSafely(c); } return payload; } /** * Get an input payload from a tProcessor id. * * @return */ public PayloadDto getPayloadFromProcessor(Integer processorId) { Connection c = Configuration.getConnection(); // Safety check so that if there isn't a value, we tell the calling // function so. if (processorId == -1) { return null; } PreparedStatement cStmt = null; PayloadDto payload = null; try { log.trace("SELECT payloadId FROM tProcessor WHERE id = " + processorId.toString()); cStmt = c .prepareStatement("SELECT payloadId FROM tProcessor WHERE id = ?;"); cStmt.setInt(1, processorId); cStmt.execute(); ResultSet rs = cStmt.getResultSet(); rs.next(); Integer payloadId = rs.getInt(1); rs.close(); log.trace("getPayloadFromProcessor for " + processorId + " returned " + payloadId); payload = getPayloadById(payloadId); } catch (NullPointerException npe) { log.error("Caught NullPointerException", npe); } catch (SQLException e) { log.error("Caught SQLException", e); } finally { DbUtil.closeSafely(cStmt); DbUtil.closeSafely(c); } return payload; } /** * Record data from the output of a ProcessorThread to the database table * tProcessor. * * @param payloadId * @param availThread * @param input * @param output * @param threadType * @param plugin * @param tsStart * @param tsEnd */ public Integer migratePayloadToProcessor(Integer payloadId, Long availThread, byte[] input, ThreadType threadType, String plugin, Date tsStart) { Connection c = Configuration.getConnection(); PreparedStatement cStmt = null; Integer ret = null; try { cStmt = c.prepareStatement("INSERT INTO tProcessor ( " + " threadId, payloadId, stage, plugin, tsStart, pInput " + " ) VALUES ( " + "?, ?, ?, ?, ?, ? " + " );", PreparedStatement.RETURN_GENERATED_KEYS); log.trace("INSERT INTO tProcessor ( " + " threadId, payloadId, stage, plugin, tsStart, pInput " + " ) VALUES ( " + availThread + ", " + payloadId + ", " + threadType.toString() + ", " + plugin + ", " + tsStart.getTime() + ", PAYLOAD " + " );"); cStmt.setLong(1, availThread); cStmt.setInt(2, payloadId); cStmt.setString(3, threadType.toString()); cStmt.setString(4, plugin); cStmt.setTimestamp(5, new Timestamp(tsStart.getTime())); cStmt.setBytes(6, input); cStmt.execute(); ResultSet newKey = cStmt.getGeneratedKeys(); newKey.next(); ret = newKey.getInt(1); newKey.close(); } catch (NullPointerException npe) { log.error("Caught NullPointerException", npe); } catch (SQLException e) { log.error("Caught SQLException", e); } finally { DbUtil.closeSafely(cStmt); DbUtil.closeSafely(c); } return ret; } /** * Record data from the output of a ProcessorThread to the database table * tProcessor. */ public void commitPayloadRun(Integer processorId, byte[] output, ThreadType threadType, Date tsEnd) { Connection c = Configuration.getConnection(); if (tsEnd == null) { tsEnd = new Date(); } PreparedStatement cStmt = null; try { cStmt = c.prepareStatement("UPDATE tProcessor SET " + " tsEnd = ?, " + "pOutput = ? " + " WHERE id = ? " + ";"); log.trace("UPDATE tProcessor SET " + " tsEnd = " + tsEnd.getTime() + ", " + "pOutput = OUTPUT " + " WHERE id = " + processorId + " ;"); cStmt.setTimestamp(1, new Timestamp(tsEnd.getTime())); cStmt.setBytes(2, output); cStmt.setInt(3, processorId); cStmt.executeUpdate(); } catch (NullPointerException npe) { log.error("Caught NullPointerException", npe); } catch (SQLException e) { log.error("Caught SQLException", e); } finally { DbUtil.closeSafely(cStmt); DbUtil.closeSafely(c); } } /** * Record a failed run to the processor table. * * @param processorId * @param tsEnd */ public void setFailedPayloadRun(Integer processorId, Date tsEnd) { Connection c = Configuration.getConnection(); if (tsEnd == null) { tsEnd = new Date(); } PreparedStatement cStmt = null; PreparedStatement cStmt2 = null; try { cStmt = c.prepareStatement("UPDATE tProcessor SET " + " tsEnd = ? " + " WHERE id = ? " + ";"); log.trace("UPDATE tProcessor SET " + " tsEnd = " + tsEnd.getTime() + " WHERE id = " + processorId + " ;"); cStmt.setTimestamp(1, new Timestamp(tsEnd.getTime())); cStmt.setInt(2, processorId); cStmt.executeUpdate(); cStmt2 = c .prepareStatement("UPDATE tPayload SET payloadState = 'failed' " + " WHERE id = " + " ( SELECT payloadId FROM tProcessor WHERE id = ? ); "); cStmt2.setInt(1, processorId); cStmt2.executeUpdate(); } catch (NullPointerException npe) { log.error("Caught NullPointerException", npe); } catch (SQLException e) { log.error("Caught SQLException", e); } finally { DbUtil.closeSafely(cStmt); DbUtil.closeSafely(cStmt2); DbUtil.closeSafely(c); } } /** * Mark a payload as having finished processing. * * @param payloadId */ public void setPayloadCompleted(Integer payloadId) { Connection c = Configuration.getConnection(); PreparedStatement cStmt = null; try { log.trace("UPDATE tProcessor SET " + " payloadState = 'completed' " + " WHERE id = " + payloadId + " ;"); cStmt = c.prepareStatement("UPDATE tPayload SET " + " payloadState = 'completed' " + " WHERE id = ? " + ";"); cStmt.setInt(1, payloadId); cStmt.executeUpdate(); } catch (NullPointerException npe) { log.error("Caught NullPointerException", npe); } catch (SQLException e) { log.error("Caught SQLException", e); } finally { DbUtil.closeSafely(cStmt); DbUtil.closeSafely(c); } } /** * Clear the internal thread/payload status for the specified thread, * effectively signalling that the thread in question is no longer working * on the payload it was working on. * * @param threadId */ public void clearProcessorForThread(Long threadId) { setProcessorForThread(threadId, 0); } /** * Push processor id to thread. * * @param threadId * @param processorId */ public void setProcessorForThread(Long threadId, Integer processorId) { Iterator<JobThreadState> iter = threadPool.iterator(); while (iter.hasNext()) { JobThreadState s = iter.next(); if (s.getThreadId() == threadId) { ((ProcessorThread) workerThreads.get(threadId)) .setJobThreadState(s); s.setProcessorId(processorId); } } } /** * Start all processor threads and add them to the thread pool. */ protected void startChildren() { Integer numberOfWorkers = Configuration.getConfiguration().getInt( "remitt.worker.threadPoolSize", 5); // Spawn RenderProcessThreads for (int iter = 0; iter < numberOfWorkers; iter++) { log.debug("Spawning RenderProcessorThread #" + (iter + 1)); RenderProcessorThread t = new RenderProcessorThread(); t.start(); workerThreads.put(t.getId(), t); addThreadToPool(t); // Create a small delay to avoid pig-piling on threads try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } // Spawn TranslationProcessThreads for (int iter = 0; iter < numberOfWorkers; iter++) { log.debug("Spawning TranslationProcessorThread #" + (iter + 1)); TranslationProcessorThread t = new TranslationProcessorThread(); t.start(); workerThreads.put(t.getId(), t); addThreadToPool(t); // Create a small delay to avoid pig-piling on threads try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } // Spawn TransportProcessThreads for (int iter = 0; iter < numberOfWorkers; iter++) { log.debug("Spawning TransportProcessorThread #" + (iter + 1)); TransportProcessorThread t = new TransportProcessorThread(); t.start(); workerThreads.put(t.getId(), t); addThreadToPool(t); // Create a small delay to avoid pig-piling on threads try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * Stop all processor threads. */ protected void stopChildren() { log.info("Stopping all worker threads"); Iterator<ProcessorThread> iter = workerThreads.values().iterator(); while (iter.hasNext()) { Thread t = iter.next(); log.info("Interrupting thread #" + t.getId()); t.interrupt(); workerThreads.remove(t); } // Attempt to reap threads until they're all dead while (workerThreads.size() > 0) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } for (Thread t : workerThreads.values()) { if (!t.isAlive()) { log.info("Reaping thread #" + t.getId()); workerThreads.remove(t); } workerThreads.remove(t); } } log.info("All worker threads destroyed"); } /** * Internal method to add a processor thread to the <ControlThread> pool of * worker threads, as well as setting up the <JobThreadState> both for the * <ProcessorThread> and internally. * * @param pt */ protected void addThreadToPool(ProcessorThread pt) { JobThreadState s = new JobThreadState(); s.setThreadId(pt.getId()); s.setProcessorId(0); s.setThreadType(pt.getThreadType()); pt.setJobThreadState(s); threadPool.add(pt.getJobThreadState()); } /** * Request next available thread for a particular processor type and assign * a payload to its internal state. * * @param threadType * @param payloadId * @return Grabbed thread id. * @throws FreeThreadNotFoundException */ protected Long getNextAvailableThread(ThreadType threadType, Integer payloadId) throws FreeThreadNotFoundException { Iterator<JobThreadState> iter = threadPool.iterator(); Long found = 0L; log.debug("Iterating through threads"); while (found == 0 && iter.hasNext()) { JobThreadState s = iter.next(); if (s.getProcessorId() > 0) { log.debug("Skipping as processorId is already defined"); continue; } // Deal with wait state if (s.getProcessorId() == -1) { log.debug("Skipping as processorId indicates wait state (-1)"); continue; } if (s.getThreadType() != threadType) { log.debug("Wrong thread type (" + s.getThreadType() + ")"); continue; } found = s.getThreadId(); log.debug("Found thread id " + found); // If we've found something, grab it so no other thread sees it s.setProcessorId(-1); } if (found == 0) { throw new FreeThreadNotFoundException(); } else { return found; } } /** * Get the list of tPayload id entries which haven't been assigned to any * tProcessor table entries yet. * * @return Array of id */ protected Integer[] getUnassignedPayloads() { List<Integer> r = new ArrayList<Integer>(); Connection c = Configuration.getConnection(); PreparedStatement cStmt = null; try { cStmt = c.prepareStatement("SELECT a.id AS id FROM tPayload AS a " + " WHERE a.id NOT IN " + " ( SELECT b.payloadId FROM tProcessor AS b ) " + " AND a.payloadState = 'valid' " + " ORDER BY a.insert_stamp " + ";"); if (cStmt.execute()) { ResultSet rs = cStmt.getResultSet(); while (rs.next()) { r.add(rs.getInt("id")); } rs.close(); } } catch (NullPointerException npe) { log.error("Caught NullPointerException", npe); } catch (Throwable e) { } finally { DbUtil.closeSafely(cStmt); DbUtil.closeSafely(c); } return (Integer[]) r.toArray(new Integer[0]); } protected boolean work() { log.trace("Entering control thread work cycle"); // Set first step for insertion ThreadType initialStep = null; try { initialStep = ThreadType.valueOf(Configuration.getConfiguration() .getString("remitt.control.initialStep")); } catch (IllegalArgumentException e) { log.warn(e); initialStep = ThreadType.RENDER; } catch (NullPointerException e) { log.warn(e); initialStep = ThreadType.RENDER; } log.trace("Using remitt.control.initialStep = " + initialStep.name()); // Search for unassigned payloads which are not being processed Integer[] newWork = getUnassignedPayloads(); if (newWork.length == 0) { log.trace("No new work found for this cycle"); } else { log.debug("Found " + newWork.length + " payloads to process"); } // For each payload ... for (int iter = 0; iter < newWork.length; iter++) { // ... attempt to insert into ThreadType.[[initialStep]] threads and // let the processing begin. Long availThread = null; try { // Attempt to get the next available thread for insertion, which // will "lock" the found thread with a -1 payloadId entry availThread = getNextAvailableThread(initialStep, newWork[iter]); log.trace("Grabbed next available thread " + availThread); // ... and populate the appropriate pieces PayloadDto payload = getPayloadById(newWork[iter]); log .trace("Created payload dto with render plugin identified as " + payload.getRenderPlugin() + ", option = " + payload.getRenderOption()); Integer processorId = migratePayloadToProcessor( payload.getId(), availThread, payload.getPayload(), initialStep, (initialStep.equals(ThreadType.RENDER) ? payload .getRenderPlugin() : payload.getRenderPlugin()), new Date(System.currentTimeMillis())); // Push processor entry setProcessorForThread(availThread, processorId); } catch (FreeThreadNotFoundException e) { log.trace(e); log .info("Cannot insert tPayload " + newWork[iter] + " due to lack of free threads. Skipping rest of queue."); return false; } } // Reap finished records which aren't being used anymore from the // database. log.trace("Exiting control thread work cycle"); return true; } /** * Get full namespace for class of plugin for a particular payload for a * particular thread type. * * @param payload * @param tType * @return */ public String resolvePlugin(PayloadDto payload, ThreadType tType) { switch (tType) { case RENDER: return payload.getRenderPlugin(); case TRANSLATION: if (payload == null) { log.error("resolvePlugin(): null payload passed"); return null; } return Configuration.resolveTranslationPlugin(payload .getRenderPlugin(), payload.getRenderOption(), payload .getTransportPlugin(), payload.getTransportOption()); case TRANSPORT: return payload.getTransportPlugin(); default: return null; } } /** * Determine payload to be used by stage from either tPayload or tProcessor, * depending on where the data is supposed to source from. * * @param payload * <PayloadDto> object with information regarding current * processing payload. * @param type * Current thread type. * @return Input payload to be used by plugin/stage. */ public byte[] getPayloadForProcessorStage(PayloadDto payload, ThreadType type) { ProcessorStore s = new ProcessorStore(payload.getId()); byte[] ret = null; if (type == ThreadType.RENDER) { log.info("getPayloadForProcessor using original payload"); ret = payload.getPayload(); log.info("getPayloadForProcessor get payload size = " + ret.length); } if (type == ThreadType.TRANSLATION) { log.info("getPayloadForProcessor using RENDER payload"); ret = s.getProcessorOutputPayload(ThreadType.RENDER); log.info("getPayloadForProcessor get payload size = " + ret.length); } if (type == ThreadType.TRANSPORT) { log.info("getPayloadForProcessor using TRANSLATION payload"); ret = s.getProcessorOutputPayload(ThreadType.TRANSLATION); log.info("getPayloadForProcessor get payload size = " + ret.length); } log.trace("getPayloadForProcessor " + type.toString() + " returned : " + ret); return ret; } /** * "Move" a tProcessor entry into the next queue stage. * * @param tS * @param nextType * @param plugin * @return Success */ public synchronized boolean moveProcessorEntry(JobThreadState tS, ThreadType nextType, String plugin) { Long availThread = null; boolean done = false; while (!done && !isInterrupted()) { try { PayloadDto originalPayload = getPayloadFromProcessor(tS .getProcessorId()); // Attempt to get the next available thread for insertion, which // will "lock" the found thread with a -1 payloadId entry availThread = getNextAvailableThread(nextType, originalPayload .getId()); // ... and populate the appropriate pieces PayloadDto payload = getPayloadFromProcessor(tS .getProcessorId()); Integer processorId = migratePayloadToProcessor( payload.getId(), availThread, getPayloadForProcessorStage(payload, nextType), nextType, resolvePlugin(payload, nextType), new Date( System.currentTimeMillis())); // Push processor entry setProcessorForThread(availThread, processorId); done = true; } catch (FreeThreadNotFoundException e) { log.trace(e); log .info("Cannot insert tPayload due to lack of free threads. Waiting for one to free up."); try { Thread.sleep(SLEEP_TIME); } catch (InterruptedException e1) { log.warn(e1); log.info("Exiting thread " + getId() + " due to interruption"); return false; } } } return done; } }