├── .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 |
--------------------------------------------------------------------------------