├── .gitignore ├── LICENSE ├── README ├── example-config.yml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── urbanairship │ │ └── octobot │ │ ├── Beanstalk.java │ │ ├── Introspector.java │ │ ├── MailQueue.java │ │ ├── Metrics.java │ │ ├── Octobot.java │ │ ├── Queue.java │ │ ├── QueueConsumer.java │ │ ├── RabbitMQ.java │ │ ├── Settings.java │ │ └── TaskExecutor.java └── resources │ └── log4j.properties └── test ├── java └── com │ └── urbanairship │ └── octobot │ └── tasks │ ├── SampleNonRunnableTask.java │ └── SampleTask.java └── scala └── com └── urbanairship └── octobot ├── SettingsSpec.scala ├── TaskSpec.scala └── TestUtils.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, C. Scott Andreas 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the organization nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Introduction – 2 | 3 | Octobot is a task queue worker designed for reliability, ease of use, and 4 | throughput. 5 | 6 | Octobot can listen on any number of queues, with any number of workers 7 | processing messages from each. Each queue can be set at a custom priority to 8 | ensure that more system resources are available for more important tasks. AMQP 9 | / RabbitMQ, Redis, and Beanstalk are supported as backends, with an extensible 10 | architecture to allow for additional backends to be added as needed. 11 | 12 | 13 | Architecture – 14 | 15 | Octobotʼs internal architecture is a shared-nothing, threadsafe design that 16 | allows for any number of concurrent queues and tasks to be processed at once, 17 | limited only by system resources. Taking full advantage of parallel execution, 18 | Octobotʼs ability to spawn multiple workers on multiple queues can be used to 19 | more efficiently process tasks at a higher level of parallelism to offset the 20 | cost of IO-bound tasks on remote systems such as database reads, writes, and 21 | queue IO. Octobot also provides for connection pooling to datastores such as 22 | Cassandra and MongoDB, as well as outbound connection management for AMQP. 23 | 24 | As an isolated worker with support for multiple queues, Octobot instances can 25 | be geographically distributed across multiple datacenters and availability 26 | regions, assuring that so long as a queue is available, work will get done. 27 | 28 | Octobotʼs modular design also makes it easy to add system introspection and 29 | reporting tools, providing metrics such as job throughput per queue, total 30 | messages processed per second, and time per message, which can be passed onto 31 | an external system if additional time calculations are required to determine 32 | end-to-end messaging performance. 33 | -------------------------------------------------------------------------------- /example-config.yml: -------------------------------------------------------------------------------- 1 | Octobot: 2 | queues: 3 | - { name: tacotruck, 4 | protocol: AMQP, 5 | host: localhost, 6 | port: 5672, 7 | vhost: /, 8 | priority: 5, 9 | workers: 1, 10 | username: cilantro, 11 | password: burrito 12 | } 13 | 14 | metrics_port: 1228 15 | 16 | email_enabled: false 17 | email_from: ohai@example.com 18 | email_to: ohno@itsbroke.com 19 | email_hostname: localhost 20 | email_server: smtp.gmail.com 21 | email_port: 465 22 | email_ssl: true 23 | email_auth: true 24 | email_username: username 25 | email_password: password 26 | 27 | # startup_hook: org.example.taquito.StartupHook 28 | # shutdown_hook: org.example.taquito.ShutdownHook -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.urbanairship 6 | octobot 7 | 0.1-SNAPSHOT 8 | Octobot distributed task queue 9 | 10 | 11 | 2.9.1 12 | 13 | 14 | 15 | 16 | cscotta 17 | C. Scott Andreas 18 | 19 | 20 | jarreds 21 | Jarred Ward 22 | 23 | 24 | 25 | 26 | 27 | repo.codahale.com 28 | http://repo.codahale.com 29 | 30 | 31 | 32 | 33 | 34 | org.scala-lang 35 | scala-library 36 | ${scala.version} 37 | 38 | 39 | com.codahale 40 | simplespec_${scala.version} 41 | 0.5.2 42 | test 43 | 44 | 45 | com.rabbitmq 46 | amqp-client 47 | 2.7.1 48 | 49 | 50 | log4j 51 | log4j 52 | 1.2.16 53 | 54 | 55 | redis.clients 56 | jedis 57 | 2.0.0 58 | 59 | 60 | com.surftools 61 | BeanstalkClient 62 | 1.4.6 63 | 64 | 65 | com.googlecode.json-simple 66 | json-simple 67 | 1.1 68 | 69 | 70 | net.java.dev 71 | jvyaml 72 | 0.2.1 73 | 74 | 75 | javax.mail 76 | mail 77 | 1.4.5-rc1 78 | 79 | 80 | com.yammer.metrics 81 | metrics-core 82 | 2.0.3 83 | 84 | 85 | com.yammer.metrics 86 | metrics-servlet 87 | 2.0.3 88 | 89 | 90 | org.eclipse.jetty 91 | jetty-server 92 | 8.1.1.v20120215 93 | 94 | 95 | org.eclipse.jetty 96 | jetty-servlet 97 | 8.1.1.v20120215 98 | 99 | 100 | 101 | 102 | 103 | sign 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-gpg-plugin 109 | 1.2 110 | 111 | 112 | sign-artifacts 113 | verify 114 | 115 | sign 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | net.alchim31.maven 129 | scala-maven-plugin 130 | 3.0.2 131 | 132 | 133 | 134 | compile 135 | testCompile 136 | 137 | 138 | 139 | 140 | 141 | -optimise 142 | -unchecked 143 | -deprecation 144 | 145 | UTF-8 146 | 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-compiler-plugin 151 | 2.3.2 152 | 153 | 1.6 154 | 1.6 155 | UTF-8 156 | 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-source-plugin 161 | 2.1.2 162 | 163 | 164 | attach-sources 165 | 166 | jar 167 | 168 | 169 | 170 | 171 | 172 | org.apache.maven.plugins 173 | maven-resources-plugin 174 | 2.5 175 | 176 | UTF-8 177 | 178 | 179 | 180 | org.apache.maven.plugins 181 | maven-surefire-plugin 182 | 2.8.1 183 | 184 | false 185 | -Xmx1024m 186 | 187 | **/*Spec.java 188 | 189 | 190 | **/*Test.java 191 | 192 | 193 | 194 | 195 | maven-assembly-plugin 196 | 2.3 197 | 198 | 199 | jar-with-dependencies 200 | 201 | 202 | 203 | true 204 | com.urbanairship.octobot.Octobot 205 | 206 | 207 | 208 | 209 | 210 | make-assembly 211 | package 212 | 213 | single 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | org.apache.maven.wagon 222 | wagon-ssh 223 | 1.0-beta-7 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/Beanstalk.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import com.surftools.BeanstalkClientImpl.ClientImpl; 4 | 5 | import org.apache.log4j.Logger; 6 | 7 | // This class handles all interfacing with a Beanstalk in Octobot. 8 | // It is responsible for connection initialization and management. 9 | 10 | public class Beanstalk { 11 | 12 | private static final Logger logger = Logger.getLogger("Beanstalk"); 13 | 14 | public static ClientImpl getBeanstalkChannel(String host, Integer port, String tube) { 15 | int attempts = 0; 16 | ClientImpl client = null; 17 | logger.info("Opening connection to Beanstalk tube: '" + tube + "'..."); 18 | 19 | while (true) { 20 | attempts++; 21 | logger.debug("Attempt #" + attempts); 22 | try { 23 | client = new ClientImpl(host, port); 24 | client.useTube(tube); 25 | client.watch(tube); 26 | logger.info("Connected to Beanstalk"); 27 | break; 28 | } catch (Exception e) { 29 | logger.error("Unable to connect to Beanstalk. Retrying in 5 seconds", e); 30 | try { Thread.sleep(1000 * 5); } 31 | catch (InterruptedException ex) { } 32 | } 33 | } 34 | 35 | return client; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/Introspector.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import org.eclipse.jetty.server.Server; 4 | import org.eclipse.jetty.servlet.ServletHolder; 5 | import org.eclipse.jetty.servlet.ServletContextHandler; 6 | 7 | import com.yammer.metrics.reporting.MetricsServlet; 8 | 9 | import org.apache.log4j.Logger; 10 | 11 | public class Introspector implements Runnable { 12 | 13 | private static final Logger logger = Logger.getLogger("Introspector"); 14 | 15 | public void run() { 16 | /*HealthChecks.register(new RedisHealthcheck) 17 | HealthChecks.register(new RabbitHealthcheck) 18 | HealthChecks.register(new BeanstalkHealthcheck)*/ 19 | 20 | int port = Settings.getAsInt("Octobot", "metrics_port"); 21 | if (port < 1) port = 1228; 22 | 23 | Server server = new Server(port); 24 | ServletHolder holder = new ServletHolder(MetricsServlet.class); 25 | ServletContextHandler context = new ServletContextHandler(); 26 | 27 | context.setContextPath(""); 28 | context.addServlet(holder, "/*"); 29 | 30 | server.setHandler(context); 31 | 32 | logger.info("Introspector launching on port: " + port); 33 | try { 34 | server.start(); 35 | server.join(); 36 | } 37 | catch (Exception e) { 38 | logger.error("Introspector: Unable to listen on port: " + port + 39 | ". Introspector will be unavailable on this instance."); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/MailQueue.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import org.apache.log4j.Logger; 4 | import java.util.concurrent.ArrayBlockingQueue; 5 | 6 | // E-mail Imports 7 | import javax.mail.Message; 8 | import javax.mail.Session; 9 | import javax.mail.Transport; 10 | import javax.mail.MessagingException; 11 | import javax.mail.PasswordAuthentication; 12 | import javax.mail.internet.InternetAddress; 13 | import javax.mail.internet.MimeMessage; 14 | import java.util.Properties; 15 | 16 | 17 | // This singleton class provides an internal queue allowing us to asynchronously 18 | // send email notifications rather than processing them in main app loop. 19 | 20 | public class MailQueue implements Runnable { 21 | 22 | private static final Logger logger = Logger.getLogger("Email Queue"); 23 | private static String from = Settings.get("Octobot", "email_from"); 24 | private static String recipient = Settings.get("Octobot", "email_to"); 25 | private static String server = Settings.get("Octobot", "email_server"); 26 | private static String username = Settings.get("Octobot", "email_username"); 27 | private static String password = Settings.get("Octobot", "email_password"); 28 | private static Integer port = Settings.getAsInt("Octobot", "email_port"); 29 | private static Boolean useSSL = Settings.getAsBoolean("Octobot", "email_ssl"); 30 | private static Boolean useAuth = Settings.getAsBoolean("Octobot", "email_auth"); 31 | 32 | // This internal queue is backed by an ArrayBlockingQueue. By specifying the 33 | // number of messages to be held here before the queue blocks (below), we 34 | // provide ourselves a safety threshold in terms of how many messages could 35 | // be backed up before we force the delivery of all current waiting messages. 36 | 37 | private static ArrayBlockingQueue messages; 38 | 39 | // Initialize the queue's singleton instance. 40 | private static final MailQueue INSTANCE = new MailQueue(); 41 | 42 | private MailQueue() { 43 | messages = new ArrayBlockingQueue(100); 44 | } 45 | 46 | public static MailQueue get() { 47 | return INSTANCE; 48 | } 49 | 50 | public static void put(String message) throws InterruptedException { 51 | messages.put(message); 52 | } 53 | 54 | public static int size() { 55 | return messages.size(); 56 | } 57 | 58 | public static int remainingCapacity() { 59 | return messages.remainingCapacity(); 60 | } 61 | 62 | // As this thread runs, it consumes messages from the internal queue and 63 | // delivers each to the recipients configured in the YML file. 64 | public void run() { 65 | 66 | if (!validSettings()) { 67 | logger.error("Email settings invalid; check your configuration."); 68 | return; 69 | } 70 | 71 | while (true) { 72 | try { 73 | String message = messages.take(); 74 | deliverMessage(message); 75 | } catch (InterruptedException e) { 76 | // Pass 77 | } 78 | } 79 | } 80 | 81 | // Delivers email error notificiations. 82 | private void deliverMessage(String message) { 83 | 84 | logger.info("Sending error notification to: " + recipient); 85 | 86 | try { 87 | MimeMessage email = prepareEmail(); 88 | email.setFrom(new InternetAddress(from)); 89 | email.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient)); 90 | 91 | email.setSubject("Task Error Notification"); 92 | email.setText(message); 93 | 94 | // Send message 95 | Transport.send(email); 96 | logger.info("Sent error e-mail to " + recipient + ". " 97 | + "Message: \n\n" + message); 98 | 99 | } catch (MessagingException e) { 100 | logger.error("Error delivering error notification.", e); 101 | } 102 | } 103 | 104 | 105 | // Prepares a sendable email object based on Octobot's SMTP, SSL, and 106 | // Authentication configuration. 107 | private MimeMessage prepareEmail() { 108 | // Prepare our configuration. 109 | Properties properties = System.getProperties(); 110 | properties.setProperty("mail.smtp.host", server); 111 | properties.put("mail.smtp.auth", "true"); 112 | Session session = null; 113 | 114 | // Configure SSL. 115 | if (useSSL) { 116 | properties.put("mail.smtp.socketFactory.port", port); 117 | properties.put("mail.smtp.starttls.enable","true"); 118 | properties.put("mail.smtp.socketFactory.fallback", "false"); 119 | properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); 120 | } 121 | 122 | // Configure authentication. 123 | if (useAuth) { 124 | properties.setProperty("mail.smtp.submitter", username); 125 | Authenticator authenticator = new Authenticator(username, password); 126 | session = Session.getInstance(properties, authenticator); 127 | } else { 128 | session = Session.getDefaultInstance(properties); 129 | } 130 | 131 | return new MimeMessage(session); 132 | } 133 | 134 | 135 | // Provides an SMTP authenticator for messages sent with auth. 136 | private class Authenticator extends javax.mail.Authenticator { 137 | private PasswordAuthentication authentication; 138 | 139 | public Authenticator(String user, String pass) { 140 | String username = user; 141 | String password = pass; 142 | authentication = new PasswordAuthentication(username, password); 143 | } 144 | 145 | protected PasswordAuthentication getPasswordAuthentication() { 146 | return authentication; 147 | } 148 | } 149 | 150 | private boolean validSettings() { 151 | boolean result = true; 152 | 153 | Object[] settings = new Object[]{from, recipient, server, port}; 154 | 155 | // Validate base settings. 156 | for (Object setting : settings) 157 | if (setting == null) result = false; 158 | 159 | // Validate authentication. 160 | if (useAuth && (username == null || password == null)) 161 | result = false; 162 | 163 | return result; 164 | } 165 | 166 | } 167 | 168 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/Metrics.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import com.yammer.metrics.core.Counter; 4 | import com.yammer.metrics.core.MetricName; 5 | import com.yammer.metrics.core.MetricsRegistry; 6 | import com.yammer.metrics.core.Timer; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.LinkedList; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public class Metrics { 14 | 15 | protected static final MetricsRegistry registry = new MetricsRegistry(); 16 | 17 | // Updates internal metrics following task execution. 18 | public static void update(String task, long time, boolean status, int retries) { 19 | updateExecutionTimes(task, time); 20 | updateTaskRetries(task, retries); 21 | updateTaskResults(task, status); 22 | } 23 | 24 | 25 | // Update the list of execution times, keeping the last 10,000 per task. 26 | private static void updateExecutionTimes(String task, long time) { 27 | MetricName timerName = new MetricName("Octobot", "Metrics", task + ":Timer"); 28 | Timer timer = registry.newTimer(timerName, TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS); 29 | timer.update(time, TimeUnit.NANOSECONDS); 30 | } 31 | 32 | 33 | // Update the number of times this task has been retried. 34 | private static void updateTaskRetries(String task, int retries) { 35 | MetricName counterRetriesName = new MetricName("Octobot", "Metrics", task + ":Retries"); 36 | Counter counterRetries = registry.newCounter(counterRetriesName); 37 | counterRetries.inc(); 38 | } 39 | 40 | 41 | // Update the number of times this task has succeeded or failed. 42 | private static void updateTaskResults(String task, boolean status) { 43 | if (status == true) { 44 | MetricName counterSuccessName = new MetricName("Octobot", "Metrics", task + ":Success"); 45 | Counter counterSuccess = registry.newCounter(counterSuccessName); 46 | counterSuccess.inc(); 47 | } else { 48 | MetricName counterFailureName = new MetricName("Octobot", "Metrics", task + ":Failure"); 49 | Counter counterFailure = registry.newCounter(counterFailureName); 50 | counterFailure.inc(); 51 | } 52 | } 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/Octobot.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import java.util.List; 4 | import java.util.HashMap; 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.InvocationTargetException; 7 | 8 | import org.apache.log4j.Logger; 9 | import org.apache.log4j.PropertyConfigurator; 10 | import org.apache.log4j.BasicConfigurator; 11 | 12 | 13 | // The fun starts here! 14 | 15 | // This class is the main entry point to the application. 16 | // It initializes (a) queue consumer thread(s) reponsible for 17 | // receiving and passing messages on to tasks for execution. 18 | 19 | public class Octobot { 20 | 21 | private static final Logger logger = Logger.getLogger("Octobot"); 22 | 23 | public static void main(String[] args) { 24 | 25 | // Initialize logging from a log4j configuration file. 26 | String configFile = System.getProperty("log4j.configuration"); 27 | if (configFile != null && !configFile.equals("")) { 28 | PropertyConfigurator.configure(configFile); 29 | } else { 30 | BasicConfigurator.configure(); 31 | logger.warn("log4j.configuration not set - logging to stdout."); 32 | } 33 | 34 | // Force settings to initialize before loading application components. 35 | Settings.get(); 36 | 37 | // If a startup hook is configured, call it before launching workers. 38 | String startupHook = Settings.get("Octobot", "startup_hook"); 39 | if (startupHook != null && !startupHook.equals("")) 40 | launchStartupHook(startupHook); 41 | 42 | // If a shutdown hook is configured, register it. 43 | String shutdownHook = Settings.get("Octobot", "shutdown_hook"); 44 | if (shutdownHook != null && !shutdownHook.equals("")) 45 | registerShutdownHook(shutdownHook); 46 | 47 | boolean enableEmailErrors = Settings.getAsBoolean("Octobot", "email_enabled"); 48 | if (enableEmailErrors) { 49 | logger.info("Launching email notification queue..."); 50 | new Thread(MailQueue.get(), "Email Queue").start(); 51 | } 52 | 53 | logger.info("Launching Introspector..."); 54 | new Thread(new Introspector(), "Introspector").start(); 55 | 56 | logger.info("Launching Workers..."); 57 | List> queues = null; 58 | try { 59 | queues = getQueues(); 60 | } catch (NullPointerException e) { 61 | logger.fatal("Error: No valid queues found in Settings. Exiting."); 62 | throw new Error("Error: No valid queues found in Settings. Exiting."); 63 | } 64 | 65 | // Start a thread for each queue Octobot is configured to listen on. 66 | for (HashMap queueConf : queues) { 67 | 68 | // Fetch the number of workers to spawn and their priority. 69 | int numWorkers = Settings.getIntFromYML(queueConf.get("workers"), 1); 70 | int priority = Settings.getIntFromYML(queueConf.get("priority"), 5); 71 | 72 | Queue queue = new Queue(queueConf); 73 | 74 | // Spawn worker threads for each queue in our configuration. 75 | for (int i = 0; i < numWorkers; i++) { 76 | QueueConsumer consumer = new QueueConsumer(queue); 77 | Thread worker = new Thread(consumer, "Worker"); 78 | 79 | logger.info("Attempting to connect to " + queueConf.get("protocol") + 80 | " queue: " + queueConf.get("name") + " with priority " + 81 | priority + "/10 " + "(Worker " + (i+1) + "/" + numWorkers + ")."); 82 | 83 | worker.setPriority(priority); 84 | worker.start(); 85 | } 86 | } 87 | 88 | logger.info("Octobot ready to rock!"); 89 | } 90 | 91 | 92 | // Invokes a startup hook registered from the YML config on launch. 93 | private static void launchStartupHook(String className) { 94 | logger.info("Calling Startup Hook: " + className); 95 | 96 | try { 97 | Class startupHook = Class.forName(className); 98 | Method method = startupHook.getMethod("run", (Class[]) null); 99 | method.invoke(startupHook.newInstance(), (Object[]) null); 100 | } catch (ClassNotFoundException e) { 101 | logger.error("Could not find class: " + className + " for the " + 102 | "startup hook specified. Please ensure that it exists in your" + 103 | " classpath and launch Octobot again. Continuing without" + 104 | " executing this hook..."); 105 | } catch (NoSuchMethodException e) { 106 | logger.error("Your startup hook: " + className + " does not " + 107 | " properly implement the Runnable interface. Your startup hook must " + 108 | " contain a method with the signature: public void run()." + 109 | " Continuing without executing this hook..."); 110 | } catch (InvocationTargetException e) { 111 | logger.error("Your startup hook: " + className + " caused an error" + 112 | " in execution. Please correct this error and re-launch Octobot." + 113 | " Continuing without executing this hook...", e.getCause()); 114 | } catch (Exception e) { 115 | logger.error("Your startup hook: " + className + " caused an unknown" + 116 | " error. Please see the following stacktrace for information.", e); 117 | } 118 | } 119 | 120 | 121 | // Registers a Runnable to be ran as a shutdown hook when Octobot stops. 122 | private static void registerShutdownHook(String className) { 123 | logger.info("Registering Shutdown Hook: " + className); 124 | 125 | try { 126 | Class startupHook = Class.forName(className); 127 | Runtime.getRuntime().addShutdownHook(new Thread((Runnable) startupHook.newInstance())); 128 | } catch (ClassNotFoundException e) { 129 | logger.error("Could not find class: " + className + " for the " + 130 | "shutdown hook specified. Please ensure that it exists in your" + 131 | " classpath and launch Octobot again. Continuing without" + 132 | " registering this hook..."); 133 | } catch (ClassCastException e) { 134 | logger.error("Your shutdown hook: " + className + " could not be " 135 | + "registered due because it does not implement the Runnable " 136 | + "interface. Continuing without registering this hook..."); 137 | } catch (Exception e) { 138 | logger.error("Your shutdown hook: " + className + " could not be " 139 | + "registered due to an unknown error. Please see the " + 140 | "following stacktrace for debugging information.", e); 141 | } 142 | } 143 | 144 | @SuppressWarnings("unchecked") 145 | private static List> getQueues() { 146 | return (List>) 147 | Settings.configuration.get("Octobot").get("queues"); 148 | } 149 | 150 | } 151 | 152 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/Queue.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import java.util.HashMap; 4 | 5 | public class Queue { 6 | 7 | public String queueType; 8 | public String queueName; 9 | 10 | public String host; 11 | public Integer port; 12 | public String username; 13 | public String password; 14 | public String vhost; 15 | 16 | public Queue(String queueType, String queueName, String host, Integer port, 17 | String username, String password) { 18 | this.queueType = queueType.toLowerCase(); 19 | this.queueName = queueName; 20 | this.host = host; 21 | this.port = port; 22 | this.username = username; 23 | this.password = password; 24 | this.vhost = "/"; 25 | } 26 | 27 | public Queue(String queueType, String queueName, String host, Integer port) { 28 | this.queueType = queueType.toLowerCase(); 29 | this.queueName = queueName; 30 | this.host = host; 31 | this.port = port; 32 | } 33 | 34 | public Queue(HashMap config) { 35 | this.queueName = (String) config.get("name"); 36 | this.queueType = ((String) config.get("protocol")).toLowerCase(); 37 | this.host = (String) config.get("host"); 38 | this.vhost = (String) config.get("vhost"); 39 | this.username = (String) config.get("username"); 40 | this.password = (String) config.get("password"); 41 | 42 | if (config.get("port") != null) 43 | this.port = Integer.parseInt(((Long) config.get("port")).toString()); 44 | } 45 | 46 | 47 | @Override 48 | public String toString() { 49 | return queueType + "/" + queueName + "/" + host + "/" + port + "/" + 50 | username + "/" + password + "/" + vhost; 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/QueueConsumer.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | // AMQP Support 4 | import java.io.IOException; 5 | import com.rabbitmq.client.Channel; 6 | import com.rabbitmq.client.Connection; 7 | import com.rabbitmq.client.QueueingConsumer; 8 | 9 | // Beanstalk Support 10 | import com.surftools.BeanstalkClient.BeanstalkException; 11 | import com.surftools.BeanstalkClient.Job; 12 | import com.surftools.BeanstalkClientImpl.ClientImpl; 13 | import java.io.PrintWriter; 14 | import java.io.StringWriter; 15 | 16 | import org.json.simple.JSONObject; 17 | import org.json.simple.JSONValue; 18 | import org.apache.log4j.Logger; 19 | import redis.clients.jedis.Jedis; 20 | import redis.clients.jedis.JedisPubSub; 21 | import redis.clients.jedis.exceptions.JedisConnectionException; 22 | 23 | 24 | // This thread opens a streaming connection to a queue, which continually 25 | // pushes messages to Octobot queue workers. The tasks contained within these 26 | // messages are invoked, then acknowledged and removed from the queue. 27 | 28 | public class QueueConsumer implements Runnable { 29 | 30 | Queue queue = null; 31 | Channel channel = null; 32 | Connection connection = null; 33 | QueueingConsumer consumer = null; 34 | 35 | private final Logger logger = Logger.getLogger("Queue Consumer"); 36 | private boolean enableEmailErrors = Settings.getAsBoolean("Octobot", "email_enabled"); 37 | 38 | // Initialize the consumer with a queue object (AMQP, Beanstalk, or Redis). 39 | public QueueConsumer(Queue queue) { 40 | this.queue = queue; 41 | } 42 | 43 | // Fire up the appropriate queue listener and begin invoking tasks!. 44 | public void run() { 45 | if (queue.queueType.equals("amqp")) { 46 | channel = getAMQPChannel(queue); 47 | consumeFromAMQP(); 48 | } else if (queue.queueType.equals("beanstalk")) { 49 | consumeFromBeanstalk(); 50 | } else if (queue.queueType.equals("redis")) { 51 | consumeFromRedis(); 52 | } else { 53 | logger.error("Invalid queue type specified: " + queue.queueType); 54 | } 55 | } 56 | 57 | 58 | // Attempts to register to receive streaming messages from RabbitMQ. 59 | // In the event that RabbitMQ is unavailable the call to getChannel() 60 | // will attempt to reconnect. If it fails, the loop simply repeats. 61 | private void consumeFromAMQP() { 62 | 63 | while (true) { 64 | QueueingConsumer.Delivery task = null; 65 | try { task = consumer.nextDelivery(); } 66 | catch (Exception e){ 67 | logger.error("Error in AMQP connection; reconnecting.", e); 68 | channel = getAMQPChannel(queue); 69 | continue; 70 | } 71 | 72 | // If we've got a message, fetch the body and invoke the task. 73 | // Then, send an acknowledgement back to RabbitMQ that we got it. 74 | if (task != null && task.getBody() != null) { 75 | invokeTask(new String(task.getBody())); 76 | try { channel.basicAck(task.getEnvelope().getDeliveryTag(), false); } 77 | catch (IOException e) { logger.error("Error ack'ing message.", e); } 78 | } 79 | } 80 | } 81 | 82 | 83 | // Attempt to register to receive messages from Beanstalk and invoke tasks. 84 | private void consumeFromBeanstalk() { 85 | ClientImpl beanstalkClient = new ClientImpl(queue.host, queue.port); 86 | beanstalkClient.watch(queue.queueName); 87 | beanstalkClient.useTube(queue.queueName); 88 | logger.info("Connected to Beanstalk; waiting for jobs."); 89 | 90 | while (true) { 91 | Job job = null; 92 | try { job = beanstalkClient.reserve(1); } 93 | catch (BeanstalkException e) { 94 | logger.error("Beanstalk connection error.", e); 95 | beanstalkClient = Beanstalk.getBeanstalkChannel(queue.host, 96 | queue.port, queue.queueName); 97 | continue; 98 | } 99 | 100 | if (job != null) { 101 | String message = new String(job.getData()); 102 | 103 | try { invokeTask(message); } 104 | catch (Exception e) { logger.error("Error handling message.", e); } 105 | 106 | try { beanstalkClient.delete(job.getJobId()); } 107 | catch (BeanstalkException e) { 108 | logger.error("Error sending message receipt.", e); 109 | beanstalkClient = Beanstalk.getBeanstalkChannel(queue.host, 110 | queue.port, queue.queueName); 111 | } 112 | } 113 | } 114 | } 115 | 116 | 117 | private void consumeFromRedis() { 118 | logger.info("Connecting to Redis..."); 119 | Jedis jedis = new Jedis(queue.host, queue.port); 120 | try { 121 | jedis.connect(); 122 | } catch (JedisConnectionException e) { 123 | logger.error("Unable to connect to Redis.", e); 124 | } 125 | 126 | logger.info("Connected to Redis."); 127 | 128 | jedis.subscribe(new JedisPubSub() { 129 | @Override 130 | public void onMessage(String channel, String message) { 131 | invokeTask(message); 132 | } 133 | 134 | @Override 135 | public void onPMessage(String string, String string1, String string2) { 136 | logger.info("onPMessage Triggered - Not implemented."); 137 | } 138 | 139 | @Override 140 | public void onSubscribe(String string, int i) { 141 | logger.info("onSubscribe called - Not implemented."); 142 | } 143 | 144 | @Override 145 | public void onUnsubscribe(String string, int i) { 146 | logger.info("onUnsubscribe Called - Not implemented."); 147 | } 148 | 149 | @Override 150 | public void onPUnsubscribe(String string, int i) { 151 | logger.info("onPUnsubscribe called - Not implemented."); 152 | } 153 | 154 | @Override 155 | public void onPSubscribe(String string, int i) { 156 | logger.info("onPSubscribe Triggered - Not implemented."); 157 | } 158 | }, queue.queueName); 159 | 160 | } 161 | 162 | 163 | // Invokes a task based on the name of the task passed in the message via 164 | // reflection, accounting for non-existent tasks and errors while running. 165 | public boolean invokeTask(String rawMessage) { 166 | String taskName = ""; 167 | JSONObject message; 168 | int retryCount = 0; 169 | long retryTimes = 0; 170 | 171 | long startedAt = System.nanoTime(); 172 | String errorMessage = null; 173 | Throwable lastException = null; 174 | boolean executedSuccessfully = false; 175 | 176 | while (retryCount < retryTimes + 1) { 177 | if (retryCount > 0) 178 | logger.info("Retrying task. Attempt " + retryCount + " of " + retryTimes); 179 | 180 | try { 181 | message = (JSONObject) JSONValue.parse(rawMessage); 182 | taskName = (String) message.get("task"); 183 | if (message.containsKey("retries")) 184 | retryTimes = (Long) message.get("retries"); 185 | } catch (Exception e) { 186 | logger.error("Error: Invalid message received: " + rawMessage); 187 | return executedSuccessfully; 188 | } 189 | 190 | // Locate the task, then invoke it, supplying our message. 191 | // Cache methods after lookup to avoid unnecessary reflection lookups. 192 | try { 193 | 194 | TaskExecutor.execute(taskName, message); 195 | executedSuccessfully = true; 196 | 197 | } catch (ClassNotFoundException e) { 198 | lastException = e; 199 | errorMessage = "Error: Task requested not found: " + taskName; 200 | logger.error(errorMessage); 201 | } catch (NoClassDefFoundError e) { 202 | lastException = e; 203 | errorMessage = "Error: Task requested not found: " + taskName; 204 | logger.error(errorMessage, e); 205 | } catch (NoSuchMethodException e) { 206 | lastException = e; 207 | errorMessage = "Error: Task requested does not have a static run method."; 208 | logger.error(errorMessage); 209 | } catch (Throwable e) { 210 | lastException = e; 211 | errorMessage = "An error occurred while running the task."; 212 | logger.error(errorMessage, e); 213 | } 214 | 215 | if (executedSuccessfully) break; 216 | else retryCount++; 217 | } 218 | 219 | // Deliver an e-mail error notification if enabled. 220 | if (enableEmailErrors && !executedSuccessfully) { 221 | String email = "Error running task: " + taskName + ".\n\n" 222 | + "Attempted executing " + retryCount + " times as specified.\n\n" 223 | + "The original input was: \n\n" + rawMessage + "\n\n" 224 | + "Here's the error that resulted while running the task:\n\n" 225 | + stackToString(lastException); 226 | 227 | try { MailQueue.put(email); } 228 | catch (InterruptedException e) { } 229 | } 230 | 231 | long finishedAt = System.nanoTime(); 232 | Metrics.update(taskName, finishedAt - startedAt, executedSuccessfully, retryCount); 233 | 234 | return executedSuccessfully; 235 | } 236 | 237 | // Opens up a connection to RabbitMQ, retrying every five seconds 238 | // if the queue server is unavailable. 239 | private Channel getAMQPChannel(Queue queue) { 240 | int attempts = 0; 241 | logger.info("Opening connection to AMQP " + queue.vhost + " " + queue.queueName + "..."); 242 | 243 | while (true) { 244 | attempts++; 245 | logger.debug("Attempt #" + attempts); 246 | try { 247 | connection = new RabbitMQ(queue).getConnection(); 248 | channel = connection.createChannel(); 249 | consumer = new QueueingConsumer(channel); 250 | channel.exchangeDeclare(queue.queueName, "direct", true); 251 | channel.queueDeclare(queue.queueName, true, false, false, null); 252 | channel.queueBind(queue.queueName, queue.queueName, queue.queueName); 253 | channel.basicConsume(queue.queueName, false, consumer); 254 | logger.info("Connected to RabbitMQ"); 255 | return channel; 256 | } catch (Exception e) { 257 | logger.error("Cannot connect to AMQP. Retrying in 5 sec.", e); 258 | try { Thread.sleep(1000 * 5); } 259 | catch (InterruptedException ex) { } 260 | } 261 | } 262 | } 263 | 264 | // Converts a stacktrace from task invocation to a string for error logging. 265 | public String stackToString(Throwable e) { 266 | if (e == null) return "(Null)"; 267 | 268 | StringWriter stringWriter = new StringWriter(); 269 | PrintWriter printWriter = new PrintWriter(stringWriter); 270 | 271 | e.printStackTrace(printWriter); 272 | return stringWriter.toString(); 273 | } 274 | } 275 | 276 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/RabbitMQ.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import org.apache.log4j.Logger; 4 | import com.rabbitmq.client.Channel; 5 | import java.io.IOException; 6 | import com.rabbitmq.client.Connection; 7 | import com.rabbitmq.client.ConnectionFactory; 8 | 9 | 10 | // This class handles all interfacing with AMQP / RabbitMQ in Octobot. 11 | // It provides basic connection management and returns task channels 12 | // for placing messages into a remote queue. 13 | 14 | public class RabbitMQ { 15 | 16 | private static final Logger logger = Logger.getLogger("RabbitMQ"); 17 | public static final ConnectionFactory factory = new ConnectionFactory(); 18 | 19 | public RabbitMQ(Queue queue) { 20 | factory.setHost(queue.host); 21 | factory.setPort(queue.port); 22 | factory.setUsername(queue.username); 23 | factory.setPassword(queue.password); 24 | factory.setVirtualHost(queue.vhost); 25 | } 26 | 27 | // Returns a new connection to an AMQP queue. 28 | public Connection getConnection() throws IOException { 29 | return factory.newConnection(); 30 | } 31 | 32 | // Returns a live channel for publishing messages. 33 | public Channel getTaskChannel() { 34 | Channel taskChannel = null; 35 | 36 | int attempts = 0; 37 | while (true) { 38 | attempts++; 39 | logger.info("Attempting to connect to queue: attempt " + attempts); 40 | try { 41 | Connection connection = getConnection(); 42 | taskChannel = connection.createChannel(); 43 | break; 44 | } catch (IOException e) { 45 | logger.error("Error creating AMQP channel, retrying in 5 sec", e); 46 | try { Thread.sleep(1000 * 5); } 47 | catch (InterruptedException ex) { } 48 | } 49 | } 50 | return taskChannel; 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/Settings.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import org.jvyaml.YAML; 4 | import java.util.HashMap; 5 | import java.io.FileReader; 6 | 7 | import org.apache.log4j.Logger; 8 | 9 | 10 | // This class is responsible for loading in configuration data for the 11 | // application. By default, it searches for a YAML file located at 12 | // /usr/local/octobot/octobot.yml, unless the JVM environment variable 13 | // "-DconfigFile=/absolute/path" is specified. These values are accessed 14 | // as a standard map by calling Settings.get("Octobot", "queues"). 15 | 16 | // Implemented as a singleton to avoid reading the file in multiple times. 17 | // Changes to application configuration require a restart to take effect. 18 | 19 | public class Settings { 20 | 21 | private static final Logger logger = Logger.getLogger("Settings"); 22 | public static HashMap> configuration = null; 23 | 24 | // Load the settings once on initialization, and hang onto them. 25 | private static final Settings INSTANCE = new Settings(); 26 | 27 | @SuppressWarnings("unchecked") 28 | private Settings() { 29 | String settingsFile = System.getProperty("configFile"); 30 | if (settingsFile == null) settingsFile = "/usr/local/octobot/octobot.yml"; 31 | 32 | try { 33 | configuration = (HashMap>) 34 | YAML.load(new FileReader(settingsFile)); 35 | } catch (Exception e) { 36 | // Logging to Stdout here because Log4J not yet initialized. 37 | logger.warn("Warning: No valid config at " + settingsFile); 38 | logger.warn("Please create this file, or set the " + 39 | "-DconfigFile=/foo/bar/octobot.yml JVM variable to its location."); 40 | logger.warn("Continuing launch with internal defaults."); 41 | } 42 | 43 | } 44 | 45 | public static Settings get() { 46 | return INSTANCE; 47 | } 48 | 49 | 50 | /** 51 | * Fetches a setting from YAML configuration. 52 | * If unset in YAML, use the default value specified above. 53 | * 54 | * @param category Category to retrieve setting from (eg PosgreSQL) 55 | * @param key Actual setting to retrieve 56 | * @return value of setting as a String or null 57 | */ 58 | public static String get(String category, String key) { 59 | String result = null; 60 | 61 | try { 62 | HashMap configCategory = configuration.get(category); 63 | result = configCategory.get(key).toString(); 64 | } catch (NullPointerException e) { 65 | logger.warn("Warning - unable to load " + category + " / " + 66 | key + " from configuration file."); 67 | } 68 | 69 | return result; 70 | } 71 | 72 | /** 73 | * Fetches a setting from YAML configuration. 74 | * 75 | * @param category Category to retrieve setting from (eg PosgreSQL) 76 | * @param key Actual setting to retrieve 77 | * @param defaultValue value to return if setting doesn't exist 78 | * @return value of setting as a String or null 79 | */ 80 | public static String get(String category, String key, String defaultValue) { 81 | String result = get(category, key); 82 | 83 | if (result == null) { 84 | return defaultValue; 85 | } else { 86 | return result; 87 | } 88 | } 89 | 90 | /** 91 | * Fetches a setting from YAML config and converts it to an integer. No 92 | * integer settings are autodetected, so that logic is not needed here. 93 | * 94 | * @param category Category to retrieve setting from (eg PosgreSQL) 95 | * @param key Actual setting to retrieve 96 | * @return value of setting as an Integer or null 97 | */ 98 | public static Integer getAsInt(String category, String key) { 99 | Integer result = null; 100 | Object value = null; 101 | HashMap configCategory = null; 102 | 103 | try { 104 | configCategory = configuration.get(category); 105 | value = configCategory.get(key); 106 | 107 | if (value instanceof Long) { 108 | result = ((Long) configCategory.get(key)).intValue(); 109 | } else if (value instanceof Integer) { 110 | result = (Integer) configCategory.get(key); 111 | } 112 | 113 | } catch (NullPointerException e) { 114 | logger.warn("Warning - unable to load " + category + " / " + key + 115 | " from config file, autodetection, or default settings."); 116 | } 117 | 118 | return result; 119 | } 120 | 121 | /** 122 | * Fetches a setting from YAML config and converts it to an integer. 123 | * No integer settings are autodetected, so that logic is not needed here. 124 | * 125 | * @param category Category to retrieve setting from (eg PosgreSQL) 126 | * @param key Actual setting to retrieve 127 | * @param defaultValue value to return if setting doesn't exist 128 | * @return value of setting as an Integer or defaultValue 129 | */ 130 | public static Integer getAsInt(String category, String key, int defaultValue) { 131 | Integer result = getAsInt(category, key); 132 | if (result == null) { 133 | return defaultValue; 134 | } else { 135 | return result; 136 | } 137 | } 138 | 139 | // Fetches a value from settings as an integer, with a default value. 140 | public static Integer getIntFromYML(Object obj, Integer defaultValue) { 141 | int result = defaultValue; 142 | 143 | if (obj != null) { 144 | try { result = Integer.parseInt(obj.toString()); } 145 | catch (NumberFormatException e) { logger.info("Error reading settings."); } 146 | } 147 | 148 | return result; 149 | } 150 | 151 | // Fetches a setting from YAML config and converts it to a boolean. 152 | // No boolean settings are autodetected, so that logic is not needed here. 153 | public static boolean getAsBoolean(String category, String key) { 154 | return Boolean.valueOf(get(category, key)).booleanValue(); 155 | } 156 | 157 | } 158 | 159 | -------------------------------------------------------------------------------- /src/main/java/com/urbanairship/octobot/TaskExecutor.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot; 2 | 3 | import java.util.HashMap; 4 | import java.lang.reflect.Method; 5 | import org.json.simple.JSONObject; 6 | import java.lang.reflect.InvocationTargetException; 7 | 8 | public class TaskExecutor { 9 | 10 | private static final HashMap taskCache = 11 | new HashMap(); 12 | 13 | @SuppressWarnings("unchecked") 14 | public static void execute(String taskName, JSONObject message) 15 | throws ClassNotFoundException, 16 | NoSuchMethodException, 17 | IllegalAccessException, 18 | InvocationTargetException { 19 | 20 | Method method = null; 21 | 22 | if (taskCache.containsKey(taskName)) { 23 | method = taskCache.get(taskName); 24 | } else { 25 | Class task = Class.forName(taskName); 26 | method = task.getMethod("run", new Class[]{ JSONObject.class }); 27 | taskCache.put(taskName, method); 28 | } 29 | 30 | method.invoke(null, new Object[]{ message }); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Set root logger level to DEBUG and its only appender to A1. 2 | log4j.rootLogger=DEBUG, A1 3 | 4 | # A1 is set to be a ConsoleAppender. 5 | log4j.appender.A1=org.apache.log4j.ConsoleAppender 6 | 7 | # A1 uses a PatternLayout. 8 | log4j.appender.A1.layout=org.apache.log4j.PatternLayout 9 | log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n 10 | -------------------------------------------------------------------------------- /src/test/java/com/urbanairship/octobot/tasks/SampleNonRunnableTask.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot.tasks; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.json.simple.JSONObject; 5 | 6 | // This is a sample test used for execution verification in the test suite. 7 | 8 | public class SampleNonRunnableTask { 9 | 10 | private static final Logger logger = Logger.getLogger("Sample Unexecutable Task"); 11 | 12 | // Stores a notification for later delivery. 13 | public static void fun(JSONObject task) { 14 | logger.info("There is no run method here!"); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/test/java/com/urbanairship/octobot/tasks/SampleTask.java: -------------------------------------------------------------------------------- 1 | package com.urbanairship.octobot.tasks; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.json.simple.JSONObject; 5 | 6 | // This is a sample test used for execution verification in the test suite. 7 | 8 | public class SampleTask { 9 | 10 | private static final Logger logger = Logger.getLogger("Sample Task"); 11 | 12 | // Stores a notification for later delivery. 13 | public static void run(JSONObject task) { 14 | logger.info("== Successfully ran SampleTask."); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/test/scala/com/urbanairship/octobot/SettingsSpec.scala: -------------------------------------------------------------------------------- 1 | import com.codahale.simplespec._ 2 | 3 | import com.urbanairship.octobot.Settings 4 | 5 | import org.apache.log4j.Logger; 6 | import org.apache.log4j.BasicConfigurator; 7 | 8 | import org.junit._ 9 | 10 | class SettingsSpec extends Spec { 11 | // Initialize Log4J 12 | BasicConfigurator.configure(); 13 | 14 | class `Settings` { 15 | @Test def `should return a value for a setting in /usr/local/octobot/octobot.yml` { 16 | val startupHook = Settings.get("Octobot", "startup_hook") 17 | startupHook must be("com.urbanairship.tasks.StartupHook") 18 | } 19 | 20 | @Test def `should return a default value for a setting` { 21 | val nonexistentSetting = Settings.getAsInt("Octobot", "legs", 3) 22 | nonexistentSetting must be(3) 23 | val anotherBadSetting = Settings.get("Octobot", "name", "Charles") 24 | anotherBadSetting must be("Charles") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/scala/com/urbanairship/octobot/TaskSpec.scala: -------------------------------------------------------------------------------- 1 | import com.codahale.simplespec.Spec 2 | 3 | import com.urbanairship.octobot.QueueConsumer 4 | 5 | import org.apache.log4j.Logger 6 | import org.apache.log4j.BasicConfigurator 7 | 8 | import org.junit.Test 9 | 10 | class TaskSpec extends Spec { 11 | 12 | class `Tasks` { 13 | 14 | // Our sample tasks to be executed below. 15 | val shouldSucceed = "{\"task\":\"com.urbanairship.octobot.tasks.SampleTask\"}" 16 | val noRunMethod = "{\"task\":\"com.urbanairship.octobot.tasks.SampleNonRunnableTask\"}" 17 | val nonExistent = "{\"task\":\"this.does.not.Exist\"}" 18 | val retry3x = "{\"task\":\"this.does.not.Exist\", \"retries\":3}" 19 | 20 | @Test def `should execute a task successfully` { 21 | val queueConsumer = new QueueConsumer(null) 22 | queueConsumer.invokeTask(shouldSucceed) must be(true) 23 | } 24 | 25 | @Test def `should fail to run a task with a non-existent run method gracefully` { 26 | val queueConsumer = new QueueConsumer(null) 27 | println("Following expected to fail due to lack of a static run method.") 28 | queueConsumer.invokeTask(noRunMethod) must be(false) 29 | } 30 | 31 | @Test def `should fail to run a non-existent task gracefully` { 32 | val queueConsumer = new QueueConsumer(null) 33 | println("Following task is expected to fail because it does not exist.") 34 | queueConsumer.invokeTask(nonExistent) must be(false) 35 | } 36 | 37 | @Test def `should fail a task, then retry it 3 times when instructed by JSON` { 38 | val queueConsumer = new QueueConsumer(null) 39 | println("Following task is expected to run and fail three times.") 40 | queueConsumer.invokeTask(retry3x) must be(false) 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/com/urbanairship/octobot/TestUtils.scala: -------------------------------------------------------------------------------- 1 | import java.io._ 2 | import java.net._ 3 | 4 | import org.apache.log4j.Logger; 5 | import org.apache.log4j.BasicConfigurator; 6 | 7 | object TestUtils { 8 | 9 | } 10 | --------------------------------------------------------------------------------