/* * Copyright 2013 David Tinker * * 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 io.qdb.server.output; import com.fasterxml.jackson.databind.util.BeanUtil; import io.qdb.buffer.MessageBuffer; import io.qdb.buffer.MessageCursor; import io.qdb.server.ExpectedIOException; import io.qdb.server.controller.JsonService; import io.qdb.server.databind.DataBinder; import io.qdb.server.filter.MessageFilter; import io.qdb.server.filter.MessageFilterFactory; import io.qdb.server.model.Database; import io.qdb.server.model.ModelObject; import io.qdb.server.model.Output; import io.qdb.server.model.Queue; import io.qdb.server.queue.QueueManager; import io.qdb.server.repo.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * Watches a queue for new messages and processes them. */ public class OutputJob implements Runnable { private static final Logger log = LoggerFactory.getLogger(OutputJob.class); private final OutputManager outputManager; private final OutputHandlerFactory handlerFactory; private final MessageFilterFactory messageFilterFactory; private final QueueManager queueManager; private final Repository repo; private final JsonService jsonService; private final String oid; private Thread thread; private String outputPath; private Output output; private int errorCount; private boolean stopFlag; public OutputJob(OutputManager outputManager, OutputHandlerFactory handlerFactory, MessageFilterFactory messageFilterFactory, QueueManager queueManager, Repository repo, JsonService jsonService, String oid) { this.outputManager = outputManager; this.handlerFactory = handlerFactory; this.messageFilterFactory = messageFilterFactory; this.queueManager = queueManager; this.repo = repo; this.jsonService = jsonService; this.oid = oid; } public String getOid() { return oid; } @Override public void run() { thread = Thread.currentThread(); try { mainLoop(); } catch (Exception x) { logError(x); } finally { if (log.isDebugEnabled()) log.debug(this + " exit"); outputManager.onOutputJobExit(this); } } private void logError(Throwable t) { String msg; if (t instanceof ExpectedIOException) { msg = t.getMessage(); if (msg == null) msg = t.toString(); } else { msg = t.toString(); } log.error(this + ": " + msg, t instanceof ExpectedIOException ? null : t); } private void mainLoop() { while (!isStopFlag()) { Queue q; Database db; try { output = repo.findOutput(oid); if (output == null) { if (log.isDebugEnabled()) log.debug("Output [" + oid + "] does not exist"); return; } if (!output.isEnabled()) return; q = repo.findQueue(output.getQueue()); if (q == null) { if (log.isDebugEnabled()) log.debug("Queue [" + output.getQueue() + "] does not exist"); return; } db = repo.findDatabase(q.getDatabase()); if (db == null) { if (log.isDebugEnabled()) log.debug("Database [" + q.getDatabase() + "] does not exist"); return; } } catch (IOException e) { // this shouldn't happen but trap anyway as we don't want this job to die log.error(this + ": " + e, e); try { Thread.sleep(1000); } catch (InterruptedException ignore) { } continue; // hope the error has gone away } outputPath = toPath(db, q, output); MessageFilter messageFilter; try { messageFilter = messageFilterFactory.createFilter(output.toMap(), q); } catch (IllegalArgumentException e) { log.error("Error creating filter for " + outputPath + ": " + e.getMessage()); return; } OutputHandler handler; try { handler = handlerFactory.createHandler(output.getType()); } catch (IllegalArgumentException e) { log.error("Error creating handler for " + outputPath + ": " + e.getMessage()); return; } boolean initOk = false; try { // give the handler clones so modifications to q or output don't cause trouble Output oc = output.deepCopy(); Queue qc = q.deepCopy(); Map<String, Object> p = oc.getParams(); if (p != null) new DataBinder(jsonService).ignoreInvalidFields(true).bind(p, handler).check(); handler.init(qc, oc, outputPath); initOk = true; } catch (IllegalArgumentException e) { log.error(this + ": " + e.getMessage()); return; } catch (Exception e) { logError(e); ++errorCount; } try { if (initOk) { MessageBuffer buffer = queueManager.getBuffer(q); if (buffer == null) { // we might be busy starting up or something if (log.isDebugEnabled()) log.debug("Queue [" + q.getId() + "] does not have a buffer"); ++errorCount; } else { try { processMessages(buffer, handler, messageFilter); } catch (Exception e) { ++errorCount; logError(e); } } } } finally { try { handler.close(); } catch (Exception e) { log.error(this + ": Error closing handler: " + e, e); } } // todo use backoff policy from output int sleepMs = errorCount * 1000; if (sleepMs > 0) { try { Thread.sleep(sleepMs); } catch (InterruptedException ignore) { } } } } /** * Feed messages to our handler until we are closed, reach our to or toId or limit or our output is changed by * someone else. */ public void processMessages(MessageBuffer buffer, OutputHandler handler, MessageFilter mf) throws Exception { if (log.isDebugEnabled()) log.debug(outputPath + ": processing messages"); MessageCursor cursor = null; try { long atId = output.getAtId(); cursor = atId < 0 ? buffer.cursorByTimestamp(output.getAt()) : buffer.cursor(atId); long completedId = atId; long timestamp = 0; long lastUpdate = System.currentTimeMillis(); long to = output.getTo(); long toId = output.getToId(); long limit = output.getLimit(); boolean reachedTo = false; boolean reachedLimit = false; int updateIntervalMs = output.getUpdateIntervalMs(); boolean exitLoop = false; while (!exitLoop && !isStopFlag()) { boolean haveMsg; try { haveMsg = cursor.next(1000); } catch (IOException e) { haveMsg = false; exitLoop = true; logError(e); } catch (InterruptedException e) { haveMsg = false; exitLoop = true; } if (haveMsg) { try { long currentId = cursor.getId(); timestamp = cursor.getTimestamp(); reachedTo = to > 0 && timestamp >= to || toId > 0 && currentId >= toId; if (reachedTo) { long id = handler.flushMessages(); completedId = id <= 0 ? currentId - 1 : id; exitLoop = true; } else { String routingKey = cursor.getRoutingKey(); byte[] payload = null; MessageFilter.Result result = mf.accept(currentId, timestamp, routingKey, null); if (result == MessageFilter.Result.CHECK_PAYLOAD) { result = mf.accept(currentId, timestamp, routingKey, payload = cursor.getPayload()); } if (result == MessageFilter.Result.ACCEPT) { completedId = handler.processMessage(currentId, routingKey, timestamp, payload == null ? cursor.getPayload() : payload); if (completedId == currentId) completedId = cursor.getNextId(); else ++completedId; // limit must be checked after processing so cannot combine this code with reachedTo reachedLimit = limit > 0 && (--limit == 0); if (reachedLimit) { long id = handler.flushMessages(); completedId = id <= 0 ? currentId - 1 : id; exitLoop = true; } } else { // don't reprocess rejected messages if output restarts if (completedId == currentId) completedId = cursor.getNextId(); } } errorCount = 0; // we successfully processed a message } catch (Exception e) { exitLoop = true; ++errorCount; logError(e); } } Output o = repo.findOutput(oid); if (o != output) { exitLoop = true; // output has been changed by someone else errorCount = 0; } if ((completedId != atId || reachedTo || reachedLimit) && (exitLoop || updateIntervalMs <= 0 || System.currentTimeMillis() - lastUpdate >= updateIntervalMs)) { synchronized (repo) { o = repo.findOutput(oid); // don't record our progress if we are now supposed to be processing from a different point in buffer if (o.getAtId() != output.getAtId() || o.getAt() != output.getAt()) break; output = o.deepCopy(); output.setAt(timestamp); handler.updateOutput(output); output.setAtId(completedId); output.setLimit(limit); if (reachedTo || reachedLimit) output.setEnabled(false); repo.updateOutput(output); atId = completedId; lastUpdate = System.currentTimeMillis(); } } } } finally { if (cursor != null) { try { cursor.close(); } catch (IOException e) { log.error(this + ": Error closing cursor: " + e); } } } } public void outputChanged(Output o) { // if o is the same object as our current Output instance then we made the change so don't stop if (o != output && thread != null) thread.interrupt(); } public synchronized void stop() { stopFlag = true; if (thread != null) thread.interrupt(); } private synchronized boolean isStopFlag() { return stopFlag; } /** * Create user friendly identifier for the output for error messages and so on. */ private String toPath(Database db, Queue q, Output o) { StringBuilder b = new StringBuilder(); String dbId = db.getId(); if (!"default".equals(dbId)) b.append("/db/").append(dbId); String s = db.getQueueForQid(q.getId()); if (s != null) { b.append("/q/").append(s); s = q.getOutputForOid(o.getId()); if (s != null) { return b.append("/out/").append(s).toString(); } } return o.toString(); } @Override public String toString() { return outputPath == null ? "output[" + oid + "]" : outputPath; } }