oni 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/.gitignore +4 -0
- data/.travis.yml +23 -0
- data/.yardopts +12 -0
- data/Gemfile +7 -0
- data/LICENSE +19 -0
- data/README.md +170 -0
- data/Rakefile +13 -0
- data/doc/changelog.md +6 -0
- data/doc/css/common.css +69 -0
- data/examples/github_status.rb +75 -0
- data/jenkins.sh +16 -0
- data/lib/oni.rb +7 -1
- data/lib/oni/configurable.rb +107 -0
- data/lib/oni/daemon.rb +190 -0
- data/lib/oni/daemons/sqs.rb +61 -0
- data/lib/oni/mapper.rb +30 -0
- data/lib/oni/version.rb +1 -1
- data/lib/oni/worker.rb +18 -0
- data/oni.gemspec +30 -0
- data/spec/oni/configurable_spec.rb +56 -0
- data/spec/oni/daemon_spec.rb +154 -0
- data/spec/oni/daemons/sqs_spec.rb +31 -0
- data/spec/oni/mapper_spec.rb +25 -0
- data/spec/oni/worker_spec.rb +33 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/simplecov.rb +12 -0
- data/task/coverage.rake +6 -0
- data/task/doc.rake +4 -0
- data/task/jenkins.rake +2 -0
- data/task/tag.rake +6 -0
- data/task/test.rake +4 -0
- metadata +59 -9
data/lib/oni.rb
CHANGED
@@ -0,0 +1,107 @@
|
|
1
|
+
module Oni
|
2
|
+
##
|
3
|
+
# Configurable is a basic configuration mixin that can be used to set options
|
4
|
+
# on class level and easily access them on instance level, optionally only
|
5
|
+
# evaluating the setting when it's accessed.
|
6
|
+
#
|
7
|
+
# Basic usage:
|
8
|
+
#
|
9
|
+
# class SomeClass
|
10
|
+
# include Oni::Configurable
|
11
|
+
#
|
12
|
+
# set :threads, 5
|
13
|
+
# set :logger, proc { Logger.new(STDOUT) }
|
14
|
+
#
|
15
|
+
# def some_method
|
16
|
+
# option(:threads).times do
|
17
|
+
# # ...
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
module Configurable
|
23
|
+
##
|
24
|
+
# @param [Class|Module] into
|
25
|
+
#
|
26
|
+
def self.included(into)
|
27
|
+
into.extend(ClassMethods)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Returns the value of the given option. If the value responds to `#call`
|
32
|
+
# the method is invoked and the return value of this call is returned.
|
33
|
+
#
|
34
|
+
# @param [Symbol|String] name
|
35
|
+
# @param [Mixed] default The default value to return if no custom one was
|
36
|
+
# found.
|
37
|
+
# @return [Mixed]
|
38
|
+
#
|
39
|
+
def option(name, default = nil)
|
40
|
+
value = self.class.options[name.to_sym]
|
41
|
+
|
42
|
+
if default and !value
|
43
|
+
value = default
|
44
|
+
end
|
45
|
+
|
46
|
+
return value.respond_to?(:call) ? value.call : value
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Raises an error if the given option isn't set.
|
51
|
+
#
|
52
|
+
# @param [Symbol|String] option
|
53
|
+
# @raise [ArgumentError]
|
54
|
+
#
|
55
|
+
def require_option!(option)
|
56
|
+
unless option(option)
|
57
|
+
raise ArgumentError, "The option #{option} is required but isn't set"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ClassMethods
|
62
|
+
##
|
63
|
+
# Returns a Hash containing the options of the current class.
|
64
|
+
#
|
65
|
+
# @return [Hash]
|
66
|
+
#
|
67
|
+
def options
|
68
|
+
return @options ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Sets the option to the given value. If a Proc (or any object that
|
73
|
+
# responds to `#call`) is given it's not evaluated until it's accessed.
|
74
|
+
# This makes it possible to for example set a logger that's not created
|
75
|
+
# until an instance of the including class is created.
|
76
|
+
#
|
77
|
+
# @example Setting a regular option
|
78
|
+
# set :number, 10
|
79
|
+
#
|
80
|
+
# @example Setting an option using a proc
|
81
|
+
# # This means the logger won't be shared between different instances of
|
82
|
+
# # the including class.
|
83
|
+
# set :logger, proc { Logger.new(STDOUT) }
|
84
|
+
#
|
85
|
+
# @param [Symbol|String] option
|
86
|
+
# @param [Mixed] value
|
87
|
+
#
|
88
|
+
def set(option, value)
|
89
|
+
options[option.to_sym] = value
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Sets a number of options based on the given Hash.
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# set_multiple(:a => 10, :b => 20)
|
97
|
+
#
|
98
|
+
# @param [Hash] options
|
99
|
+
#
|
100
|
+
def set_multiple(options)
|
101
|
+
options.each do |option, value|
|
102
|
+
set(option, value)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end # ClassMethods
|
106
|
+
end # Configurable
|
107
|
+
end # Oni
|
data/lib/oni/daemon.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
module Oni
|
2
|
+
##
|
3
|
+
# The Daemon class takes care of retrieving work to be processed, scheduling
|
4
|
+
# it and dispatching it to a mapper and worker. In essence a Daemon instance
|
5
|
+
# can be seen as a controller when compared with typical MVC frameworks.
|
6
|
+
#
|
7
|
+
# This daemon starts a number of threads (5 by default) that will each
|
8
|
+
# perform work on their own using the corresponding mapper and worker class.
|
9
|
+
#
|
10
|
+
# @!attribute [r] workers
|
11
|
+
# @return [Array<Thread>]
|
12
|
+
#
|
13
|
+
class Daemon
|
14
|
+
include Configurable
|
15
|
+
|
16
|
+
attr_reader :workers
|
17
|
+
|
18
|
+
##
|
19
|
+
# The default amount of threads to start.
|
20
|
+
#
|
21
|
+
# @return [Fixnum]
|
22
|
+
#
|
23
|
+
DEFAULT_THREAD_AMOUNT = 5
|
24
|
+
|
25
|
+
##
|
26
|
+
# Creates a new instance of the class and calls `#after_initialize` if it
|
27
|
+
# is defined.
|
28
|
+
#
|
29
|
+
def initialize
|
30
|
+
@workers = []
|
31
|
+
|
32
|
+
after_initialize if respond_to?(:after_initialize)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Starts the daemon and waits for all threads to finish execution. This
|
37
|
+
# method is blocking since it will wait for all threads to finish.
|
38
|
+
#
|
39
|
+
# If the current class has a `before_start` method defined it's called
|
40
|
+
# before starting the daemon.
|
41
|
+
#
|
42
|
+
def start
|
43
|
+
before_start if respond_to?(:before_start)
|
44
|
+
|
45
|
+
# If we don't have any threads run in non threaded mode.
|
46
|
+
if threads > 0
|
47
|
+
threads.times do
|
48
|
+
workers << spawn_thread
|
49
|
+
end
|
50
|
+
|
51
|
+
workers.each(&:join)
|
52
|
+
else
|
53
|
+
run_thread
|
54
|
+
end
|
55
|
+
rescue => error
|
56
|
+
error(error)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Terminates all the threads and clears up the list. Note that calling this
|
61
|
+
# method acts much like sending a SIGKILL signal to a process: threads will
|
62
|
+
# be shut down *immediately*.
|
63
|
+
#
|
64
|
+
def stop
|
65
|
+
workers.each(&:kill)
|
66
|
+
workers.clear
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Returns the amount of threads to use.
|
71
|
+
#
|
72
|
+
# @return [Fixnum]
|
73
|
+
#
|
74
|
+
def threads
|
75
|
+
return option(:threads, DEFAULT_THREAD_AMOUNT)
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Processes the given message. Upon completion the `#complete` method is
|
80
|
+
# called and passed the resulting output.
|
81
|
+
#
|
82
|
+
# @param [Mixed] message
|
83
|
+
#
|
84
|
+
def process(message)
|
85
|
+
output = nil
|
86
|
+
timings = Benchmark.measure do
|
87
|
+
output = run_worker(message)
|
88
|
+
end
|
89
|
+
|
90
|
+
complete(message, output, timings)
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Maps the input, runs the worker and then maps the output into something
|
95
|
+
# that the daemon can understand.
|
96
|
+
#
|
97
|
+
# @param [Mixed] message
|
98
|
+
# @return [Mixed]
|
99
|
+
#
|
100
|
+
def run_worker(message)
|
101
|
+
mapper = create_mapper
|
102
|
+
input = mapper.map_input(message)
|
103
|
+
worker = option(:worker).new(*input)
|
104
|
+
output = worker.process
|
105
|
+
|
106
|
+
return mapper.map_output(output)
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Receives a message, by default this method raises an error.
|
111
|
+
#
|
112
|
+
# @raise [NotImplementedError]
|
113
|
+
#
|
114
|
+
def receive
|
115
|
+
raise NotImplementedError, 'You must manually implement #receive'
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Called when a job has been completed, by default this method is a noop.
|
120
|
+
# This method is passed 3 arguments:
|
121
|
+
#
|
122
|
+
# 1. The raw input message.
|
123
|
+
# 2. The output of the worker (remapped by the mapper).
|
124
|
+
# 3. A Benchmark::Tms instance that contains the timings for processing the
|
125
|
+
# message.
|
126
|
+
#
|
127
|
+
# @param [Mixed] message The raw input message (e.g. an AWS SQS message)
|
128
|
+
# @param [Mixed] output The output of the worker.
|
129
|
+
# @param [Benchmark::Tms] timings
|
130
|
+
#
|
131
|
+
def complete(message, output, timings)
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Called whenever an error is raised in the daemon, mapper or worker. By
|
136
|
+
# default this method just re-raises the error.
|
137
|
+
#
|
138
|
+
# Note that this callback method is called from a thread in which the
|
139
|
+
# exception occured, not from the main thread.
|
140
|
+
#
|
141
|
+
# @param [StandardError] error
|
142
|
+
#
|
143
|
+
def error(error)
|
144
|
+
raise error
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Creates a new mapper and passes it a set of arguments as defined in
|
149
|
+
# {Oni::Daemon#mapper_arguments}.
|
150
|
+
#
|
151
|
+
# @return [Oni::Mapper]
|
152
|
+
#
|
153
|
+
def create_mapper
|
154
|
+
unless option(:mapper)
|
155
|
+
raise ArgumentError, 'No mapper has been set in the `:mapper` option'
|
156
|
+
end
|
157
|
+
|
158
|
+
return option(:mapper).new
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Spawns a new thread that waits for daemon input.
|
163
|
+
#
|
164
|
+
# @return [Thread]
|
165
|
+
#
|
166
|
+
def spawn_thread
|
167
|
+
thread = Thread.new { run_thread }
|
168
|
+
|
169
|
+
thread.abort_on_exception = true
|
170
|
+
|
171
|
+
return thread
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# The main code to execute in individual threads.
|
176
|
+
#
|
177
|
+
# If an error occurs in the receive method or processing a job the error
|
178
|
+
# handler is executed and the process is retried. It's the responsibility
|
179
|
+
# of the `error` method to determine if the process should fail only once
|
180
|
+
# (and fail hard) or if it should continue running.
|
181
|
+
#
|
182
|
+
def run_thread
|
183
|
+
receive { |message| process(message) }
|
184
|
+
rescue => error
|
185
|
+
error(error)
|
186
|
+
|
187
|
+
retry
|
188
|
+
end
|
189
|
+
end # Daemon
|
190
|
+
end # Oni
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Oni
|
4
|
+
module Daemons
|
5
|
+
##
|
6
|
+
# The SQS daemon is a basic daemon skeleton that can be used to process
|
7
|
+
# jobs from an Amazon SQS queue.
|
8
|
+
#
|
9
|
+
# Basic usage:
|
10
|
+
#
|
11
|
+
# class MyDaemon < Oni::Daemons::SQS
|
12
|
+
# set :queue_name, 'my_queue'
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# The following options can be set:
|
16
|
+
#
|
17
|
+
# * `queue_name` (required): the name of the queue to poll as a String.
|
18
|
+
# * `poll_options`: a Hash of options to pass to the `poll` method of the
|
19
|
+
# AWS SQS queue. See the documentation of `AWS::SQS::Queue#poll` for more
|
20
|
+
# information on the available options.
|
21
|
+
#
|
22
|
+
class SQS < Daemon
|
23
|
+
##
|
24
|
+
# Checks if the `queue_name` option is set.
|
25
|
+
#
|
26
|
+
def after_initialize
|
27
|
+
require_option!(:queue_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Polls an SQS queue for a message and processes it.
|
32
|
+
#
|
33
|
+
def receive
|
34
|
+
queue.poll(poll_options) do |message|
|
35
|
+
yield message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Returns a Hash containing the options to use for the `poll` method of
|
41
|
+
# the SQS queue.
|
42
|
+
#
|
43
|
+
# @return [Hash]
|
44
|
+
#
|
45
|
+
def poll_options
|
46
|
+
return option(:poll_options, {})
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Returns the queue to use for the current thread.
|
51
|
+
#
|
52
|
+
# @return [AWS::SQS::Queue]
|
53
|
+
#
|
54
|
+
#:nocov:
|
55
|
+
def queue
|
56
|
+
return AWS::SQS.new.queues.named(option(:queue_name))
|
57
|
+
end
|
58
|
+
#:nocov:
|
59
|
+
end # SQS
|
60
|
+
end # Daemons
|
61
|
+
end # Oni
|
data/lib/oni/mapper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Oni
|
2
|
+
##
|
3
|
+
# Abstract mapper class that takes care of some common boilerplate code.
|
4
|
+
#
|
5
|
+
class Mapper
|
6
|
+
include Configurable
|
7
|
+
|
8
|
+
##
|
9
|
+
# Remaps the input of the daemon into a format that's easy to use for the
|
10
|
+
# worker.
|
11
|
+
#
|
12
|
+
# @param [Mixed] input
|
13
|
+
# @return [Mixed]
|
14
|
+
#
|
15
|
+
def map_input(input)
|
16
|
+
return input
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Remaps the output of the working into a format that's easy to use for the
|
21
|
+
# daemon layer.
|
22
|
+
#
|
23
|
+
# @param [Mixed] output
|
24
|
+
# @return [Mixed]
|
25
|
+
#
|
26
|
+
def map_output(output)
|
27
|
+
return output
|
28
|
+
end
|
29
|
+
end # Mapper
|
30
|
+
end # Oni
|
data/lib/oni/version.rb
CHANGED
data/lib/oni/worker.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Oni
|
2
|
+
##
|
3
|
+
# An abstract worker class that takes care of some common boilerplate code.
|
4
|
+
#
|
5
|
+
class Worker
|
6
|
+
include Configurable
|
7
|
+
|
8
|
+
##
|
9
|
+
# Runs the worker and returns some kind of output.
|
10
|
+
#
|
11
|
+
# @return [Mixed]
|
12
|
+
# @raise [NotImplementedError]
|
13
|
+
#
|
14
|
+
def process
|
15
|
+
raise NotImplementedError, 'You must implement #process yourself'
|
16
|
+
end
|
17
|
+
end # Worker
|
18
|
+
end # Oni
|
data/oni.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.expand_path('../lib/oni/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = 'oni'
|
5
|
+
gem.version = Oni::VERSION
|
6
|
+
gem.authors = [
|
7
|
+
'Yorick Peterse',
|
8
|
+
'Wilco van Duinkerken'
|
9
|
+
]
|
10
|
+
|
11
|
+
gem.summary = 'Framework for building concurrent daemons in Ruby.'
|
12
|
+
gem.description = gem.summary
|
13
|
+
gem.has_rdoc = 'yard'
|
14
|
+
gem.license = 'MIT'
|
15
|
+
|
16
|
+
gem.required_ruby_version = '>= 1.9.3'
|
17
|
+
|
18
|
+
gem.files = `git ls-files`.split("\n").sort
|
19
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
gem.add_development_dependency 'bundler'
|
24
|
+
gem.add_development_dependency 'rspec'
|
25
|
+
gem.add_development_dependency 'yard'
|
26
|
+
gem.add_development_dependency 'simplecov'
|
27
|
+
gem.add_development_dependency 'kramdown'
|
28
|
+
gem.add_development_dependency 'ci_reporter'
|
29
|
+
gem.add_development_dependency 'aws-sdk'
|
30
|
+
end
|