arsenicum 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/arsenicum.gemspec +25 -0
- data/bin/arsenicum +13 -0
- data/bin/arsenicum_rails +5 -0
- data/lib/arsenicum-rails.rb +10 -0
- data/lib/arsenicum.rb +14 -0
- data/lib/arsenicum/actor.rb +14 -0
- data/lib/arsenicum/cli.rb +54 -0
- data/lib/arsenicum/cli/rails.rb +26 -0
- data/lib/arsenicum/configuration.rb +92 -0
- data/lib/arsenicum/queue.rb +58 -0
- data/lib/arsenicum/queue_proxy.rb +73 -0
- data/lib/arsenicum/rake_tasks.rake +24 -0
- data/lib/arsenicum/serialization.rb +110 -0
- data/lib/arsenicum/server.rb +40 -0
- data/lib/arsenicum/sqs.rb +5 -0
- data/lib/arsenicum/sqs/queue.rb +76 -0
- data/lib/arsenicum/syntax.rb +11 -0
- data/lib/arsenicum/syntax/delayed_job.rb +25 -0
- data/lib/arsenicum/task.rb +53 -0
- data/lib/arsenicum/version.rb +3 -0
- data/lib/arsenicum/watchdog.rb +68 -0
- data/spec/config.example.yml +21 -0
- metadata +115 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 51962f53bdcb4a46365d2e73dc6730fb663b1436
|
4
|
+
data.tar.gz: 75cee77e64c8c6620006b6d6cb6452544319cbbe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2307adda63e934e0eab37770afd623cbd5d81d9fe08ed2c9277ec4b5021f039b42230109cb8fdbb1364526bfa58c5ec9da1fa9b0a71f7c7ffd0f9bdd8c05d31e
|
7
|
+
data.tar.gz: 0cabdc6010016709c9b0ca3dde02838812a7eda4af21837ff202af6b9a4b26d6084468486f51a61c952f33612349b368c25b5a39d1e12f9d5ab26a875d3d6433
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 condor
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Arsenicum
|
2
|
+
|
3
|
+
Asyncronous processing engine that can use any queue storage including Amazon SQS and so on.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'arsenicum'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install arsenicum
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
You can boot arsenicum.
|
22
|
+
|
23
|
+
$ bundle exec arsenicum
|
24
|
+
|
25
|
+
If you use arsenicum with rails, you should boot arsenicum_rails as below:
|
26
|
+
|
27
|
+
$ bundle exec arsenicum_rails
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
1. Fork it
|
32
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
33
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
34
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
35
|
+
5. Create new Pull Request
|
data/arsenicum.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'arsenicum/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "arsenicum"
|
8
|
+
spec.version = Arsenicum::VERSION
|
9
|
+
spec.authors = ["condor"]
|
10
|
+
spec.email = ["condor1226@gmail.com"]
|
11
|
+
spec.description = %q{Arsenicum: multi-backend asyncronous processor.}
|
12
|
+
spec.summary = %q{Arsenicum: multi-backend asyncronous processor.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/).reject{|s|s == 'build.rake'}
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'aws-sdk'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
end
|
data/bin/arsenicum
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'arsenicum'
|
4
|
+
require 'optparse'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
config = {}
|
8
|
+
opt = OptionParser.new
|
9
|
+
|
10
|
+
opt.on('-c', '--config-file=CONFIG_FILE'){|v|config.merge! YAML.load(File.read(v))}
|
11
|
+
# TODO implement other options
|
12
|
+
|
13
|
+
Arsenicum::Server.start config
|
data/bin/arsenicum_rails
ADDED
data/lib/arsenicum.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Arsenicum
|
2
|
+
autoload :Version, 'arsenicum/version'
|
3
|
+
autoload :Queue, 'arsenicum/queue'
|
4
|
+
autoload :Task, 'arsenicum/task'
|
5
|
+
autoload :Configuration, 'arsenicum/configuration'
|
6
|
+
autoload :Serialization, 'arsenicum/serialization'
|
7
|
+
autoload :WatchDog, 'arsenicum/watchdog'
|
8
|
+
autoload :QueueProxy, 'arsenicum/queue_proxy'
|
9
|
+
autoload :Syntax, 'arsenicum/syntax'
|
10
|
+
autoload :Sqs, 'arsenicum/sqs'
|
11
|
+
autoload :CLI, 'arsenicum/cli'
|
12
|
+
autoload :Server, 'arsenicum/server'
|
13
|
+
autoload :Actor, 'arsenicum/actor'
|
14
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Arsenicum
|
5
|
+
class CLI
|
6
|
+
autoload :Rails, 'arsenicum/cli/rails'
|
7
|
+
|
8
|
+
attr_reader :configuration
|
9
|
+
|
10
|
+
def initialize(argv)
|
11
|
+
@configuration = option_parser.parse!(argv)
|
12
|
+
end
|
13
|
+
|
14
|
+
def boot
|
15
|
+
Arsenicum::Server.start(Arsenicum::Configuration.new(configuration))
|
16
|
+
end
|
17
|
+
|
18
|
+
class OptionParser
|
19
|
+
def initialize
|
20
|
+
@values = {}
|
21
|
+
@parser = ::OptionParser.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def register(*args)
|
25
|
+
block = args.pop
|
26
|
+
block = block.to_proc unless block.is_a? Proc
|
27
|
+
|
28
|
+
if block.arity == 2
|
29
|
+
@parser.on(*args) {|v|block.call(v, @values)}
|
30
|
+
else
|
31
|
+
@parser.on(*args) {|v|@values.merge! block.call(v)}
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse!(argv)
|
37
|
+
@parser.parse! argv
|
38
|
+
@values
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def option_parser
|
44
|
+
OptionParser.new.
|
45
|
+
register("-c", "--config-file=YAML", -> v {YAML.load(File.read(v, encoding: "UTF-8"))}).
|
46
|
+
register("-t", "--default-concurrency=VALUE", -> v {{default_concurrency: v.to_i}}).
|
47
|
+
register("-q", "--queue-type=QUEUE_TYPE", -> v{{queue_type: v.to_s}}).
|
48
|
+
register("--queue-engine-config=CONFIGKEY_VALUE", -> v, config {
|
49
|
+
config[:engine_config] ||= {};(key, value) = v.split(':');config[:engine_config][key.to_sym] = value.to_s
|
50
|
+
})
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Arsenicum
|
2
|
+
class CLI::Rails < CLI
|
3
|
+
def boot
|
4
|
+
ENV['RACK_ENV'] = (ENV['RAILS_ENV'] ||= (configuration[:rails_env] || 'development'))
|
5
|
+
rootdir = ENV['RAILS_ROOT'] || configuration[:dir] || Dir.pwd
|
6
|
+
Dir.chdir rootdir
|
7
|
+
|
8
|
+
if configuration[:background] && !configuration[:pidfile]
|
9
|
+
configuration[:pidfile] = "#{rootdir}/tmp/pids/arsenicum.pid"
|
10
|
+
end
|
11
|
+
|
12
|
+
load File.join(rootdir, 'config/environment.rb')
|
13
|
+
Arsenicum::Configuration.reconfigure configuration
|
14
|
+
Arsenicum::Server.start
|
15
|
+
end
|
16
|
+
|
17
|
+
def option_parser
|
18
|
+
OptionParser.new.register("-e", "--environment=ENVIRONMENT", -> v { {rails_env: v} }).
|
19
|
+
register("-d", "--dir=DIRECTORY", -> v { {dir: v} }).
|
20
|
+
register("-p", "--pidfile=PID_FILE", -> v { { pidfile: v } }).
|
21
|
+
register("-l", "--log-file=LOG_FILE", -> v { { log_file: v } }).
|
22
|
+
register("-D", "--daemon", -> v { { background: true } }).
|
23
|
+
register("-L", "--log-level=LOG_LEVEL", -> v { {log_level: v} })
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Arsenicum
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :queue_namespace, :queue_type, :queue_configurations, :engine_configuration, :pidfile, :background, :log_level
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
DEFAULT_QUEUES = {
|
9
|
+
default: {
|
10
|
+
concurrency: 2,
|
11
|
+
},
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configure(configuration)
|
16
|
+
@instance = new(configuration)
|
17
|
+
end
|
18
|
+
attr_reader :instance
|
19
|
+
|
20
|
+
def reconfigure(settings)
|
21
|
+
instance.reconfigure additional_settings: settings
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(settings)
|
26
|
+
@log_level = Logger::INFO
|
27
|
+
@logger = Logger.new(STDOUT)
|
28
|
+
|
29
|
+
@original_settings = {queues: DEFAULT_QUEUES}.merge(normalize_hash_key(settings))
|
30
|
+
raise MisconfigurationError, "queue_type is required" unless @original_settings[:queue_type]
|
31
|
+
|
32
|
+
reconfigure
|
33
|
+
end
|
34
|
+
|
35
|
+
def reconfigure(additional_settings: nil)
|
36
|
+
@original_settings.merge! additional_settings if additional_settings
|
37
|
+
settings = @original_settings.dup
|
38
|
+
|
39
|
+
@pidfile = settings.delete(:pidfile)
|
40
|
+
@background = (settings.delete(:background).to_s.downcase == "true")
|
41
|
+
@queue_type = settings.delete(:queue_type).to_s
|
42
|
+
@engine_configuration = settings[queue_type.to_sym]
|
43
|
+
@queue_namespace = queue_type.gsub(/_([a-z])/){|_|$1.upcase}.gsub(/^([a-z])/){|_|$1.upcase}.to_sym
|
44
|
+
|
45
|
+
queue_settings = settings.delete(:queues)
|
46
|
+
@queue_configurations = queue_settings.inject({}) do |h, kv|
|
47
|
+
(queue_name, queue_setting) = kv
|
48
|
+
h[queue_name] = queue_setting
|
49
|
+
h
|
50
|
+
end
|
51
|
+
|
52
|
+
if log_level = settings.delete(:log_level)
|
53
|
+
@log_level = Logger.const_get(log_level.upcase.to_sym)
|
54
|
+
end
|
55
|
+
|
56
|
+
if log_file = settings.delete(:log_file)
|
57
|
+
self.logger = Logger.new(log_file)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def logger=(new_logger)
|
62
|
+
@logger = new_logger
|
63
|
+
if @logger
|
64
|
+
@logger.level = log_level
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_queues
|
69
|
+
queue_configurations.map do |queue_name_config|
|
70
|
+
(queue_name, queue_config) = queue_name_config
|
71
|
+
queue_class.new(queue_name, engine_configuration.merge(queue_config), logger)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def queue_class
|
76
|
+
Arsenicum.const_get(queue_namespace).const_get(:Queue)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def normalize_hash_key(hash, to_sym: true)
|
81
|
+
hash.inject({}) do |h, kv|
|
82
|
+
(key, value) = kv
|
83
|
+
value = normalize_hash_key(value, to_sym: to_sym) if value.is_a? Hash
|
84
|
+
key = to_sym ? key.to_sym : key.to_s
|
85
|
+
h[key] = value
|
86
|
+
h
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class MisconfigurationError < StandardError;end
|
92
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Arsenicum
|
4
|
+
class Queue
|
5
|
+
DEFAULT_CONCURRENCY = 5
|
6
|
+
|
7
|
+
attr_reader :name, :concurrency, :queue_methods, :queue_classes, :logger
|
8
|
+
|
9
|
+
def initialize(name, config = {}, logger = nil)
|
10
|
+
@name = name
|
11
|
+
@concurrency = (config.delete(:concurrency) || DEFAULT_CONCURRENCY).to_i
|
12
|
+
@queue_methods = config.delete(:methods)
|
13
|
+
@queue_classes = config.delete(:classes)
|
14
|
+
@logger = logger || Logger.new(STDOUT)
|
15
|
+
configure(config)
|
16
|
+
end
|
17
|
+
|
18
|
+
def put(hash)
|
19
|
+
json = JSON(hash.merge(timestamp: (Time.now.to_f * 1000000).to_i.to_s))
|
20
|
+
logger.debug { "Queue Put[#{name}] values #{json}" }
|
21
|
+
put_to_queue(json)
|
22
|
+
end
|
23
|
+
|
24
|
+
######################################################
|
25
|
+
#
|
26
|
+
# Queue must implement the methods as below:
|
27
|
+
# 1. put_to_queue(json): putting the actual message
|
28
|
+
# into the queue backend. The argument of this
|
29
|
+
# method will be the JSON string.
|
30
|
+
# 2. poll: polling the queue and retrieve the
|
31
|
+
# message information. This method is expected to
|
32
|
+
# return the message Hash. Its keys and values
|
33
|
+
# are expected as below:
|
34
|
+
# :message_body: the raw string that was pushed
|
35
|
+
# via the :put_to_queue method.
|
36
|
+
# :message_id: the identifier of this message.
|
37
|
+
# This is usually used to update the status of
|
38
|
+
# message on the queue backend.
|
39
|
+
# 3. update_message_status(message_id, successful, json):
|
40
|
+
# Update the status of the message. Arguments are
|
41
|
+
# as following:
|
42
|
+
# message_id: The identifier of the message to
|
43
|
+
# be updated. This value will be set as the
|
44
|
+
# :message_id of the return value of :poll.
|
45
|
+
# successful: The result of the process done.
|
46
|
+
# If the process complete successfully,
|
47
|
+
# this argument will be set true. Otherwise,
|
48
|
+
# this will be false.
|
49
|
+
# json: the message received.
|
50
|
+
# 4. create_queue_backend - optional
|
51
|
+
# Register the queue itself on its backend.
|
52
|
+
# This will be invoked from the rake task
|
53
|
+
# 'arsenicum:create_queues'.
|
54
|
+
# Note: this method should be implemented idempotently.
|
55
|
+
#
|
56
|
+
#####################################################
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Arsenicum
|
2
|
+
class QueueProxy
|
3
|
+
include Serialization
|
4
|
+
|
5
|
+
def self.instance
|
6
|
+
@instance ||= new(Arsenicum::Configuration.instance)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :configuration
|
10
|
+
attr_reader :queues
|
11
|
+
attr_reader :default_queue
|
12
|
+
attr_reader :method_queue_tables
|
13
|
+
attr_reader :class_queue_tables
|
14
|
+
|
15
|
+
def initialize(configuration)
|
16
|
+
@configuration = configuration
|
17
|
+
queue_class = configuration.queue_class
|
18
|
+
@method_queue_tables = {}
|
19
|
+
@class_queue_tables = {}
|
20
|
+
|
21
|
+
@queues = configuration.queue_configurations.inject({}) do |h, kv|
|
22
|
+
(queue_name, queue_configuration) = kv
|
23
|
+
queue = queue_class.new(queue_name, queue_configuration.merge(configuration.engine_configuration))
|
24
|
+
Array(queue.queue_methods).tap(&:compact!).each do |m|
|
25
|
+
method_queue_tables[m] ||= queue
|
26
|
+
end
|
27
|
+
Array(queue.queue_classes).tap(&:compact!).each do |m|
|
28
|
+
class_queue_tables[m] ||= queue
|
29
|
+
end
|
30
|
+
h[queue_name] = queue
|
31
|
+
|
32
|
+
h
|
33
|
+
end
|
34
|
+
@default_queue = queues[:default]
|
35
|
+
end
|
36
|
+
|
37
|
+
def async(target, method, *arguments)
|
38
|
+
values = {
|
39
|
+
target: prepare_serialization(target),
|
40
|
+
method_name: method,
|
41
|
+
arguments: arguments.map{|arg|prepare_serialization(arg)},
|
42
|
+
}
|
43
|
+
specify_queue(target, method).
|
44
|
+
tap{|q|logger.debug { "Queue #{q.name}: Param #{values.inspect}" }}.
|
45
|
+
put(values)
|
46
|
+
end
|
47
|
+
|
48
|
+
def logger
|
49
|
+
configuration.logger
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def specify_queue(target, method)
|
54
|
+
if target.is_a?(Module)
|
55
|
+
conjunction = '.'
|
56
|
+
klass = target
|
57
|
+
else
|
58
|
+
conjunction = '#'
|
59
|
+
klass = target.class
|
60
|
+
end
|
61
|
+
method_signature = [target.class.name, method].join conjunction
|
62
|
+
if queue = method_queue_tables[method_signature]
|
63
|
+
return queue
|
64
|
+
end
|
65
|
+
klass_signature = target.class.name
|
66
|
+
if queue = class_queue_tables[klass_signature]
|
67
|
+
return queue
|
68
|
+
end
|
69
|
+
|
70
|
+
return default_queue
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'arsenicum'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
namespace :arsenicum do
|
5
|
+
desc 'Create queues defined in the configuration file. Specify configuration with CONFIG=config_file_path.'
|
6
|
+
task :create_queues do
|
7
|
+
config_file = ENV['CONFIG'] || 'config/arsenicum.yml'
|
8
|
+
yaml = YAML.load(Erubis::Eruby.new(File.read(config_file, encoding: 'UTF-8')).result)
|
9
|
+
config_values =
|
10
|
+
if ENV['CONFIG_KEY']
|
11
|
+
ENV['CONFIG_KEY'].split('.').inject(yaml) do |values, key|
|
12
|
+
values[key]
|
13
|
+
end
|
14
|
+
else
|
15
|
+
yaml
|
16
|
+
end
|
17
|
+
|
18
|
+
config = Arsenicum::Configuration.new(config_values)
|
19
|
+
queue_class = config.queue_class
|
20
|
+
raise Arsenicum::MisconfigurationError, "class #{queue_class.name} doesn't support create_queue" unless queue_class.instance_methods.include?(:create_queue_backend)
|
21
|
+
|
22
|
+
config.create_queues.each(&:create_queue_backend)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Arsenicum
|
5
|
+
module Serialization
|
6
|
+
DATE_FORMAT = "%Y-%m-%d".freeze
|
7
|
+
DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S %Z %z".freeze
|
8
|
+
|
9
|
+
def prepare_serialization(value)
|
10
|
+
hash_value = prepare_serialization_specific(value) || prepare_serialization_default(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def prepare_serialization_specific(value)
|
14
|
+
case value
|
15
|
+
when Integer, Float, String, TrueClass, FalseClass, NilClass
|
16
|
+
{
|
17
|
+
type: :raw,
|
18
|
+
value: value.inspect,
|
19
|
+
}
|
20
|
+
when Date
|
21
|
+
{
|
22
|
+
type: 'date',
|
23
|
+
value: value.strftime(DATE_FORMAT),
|
24
|
+
}
|
25
|
+
when DateTime, Time
|
26
|
+
{
|
27
|
+
type: 'time',
|
28
|
+
value: value.strftime(DATE_TIME_FORMAT),
|
29
|
+
}
|
30
|
+
when Class
|
31
|
+
{
|
32
|
+
type: :class,
|
33
|
+
value: value.name,
|
34
|
+
}
|
35
|
+
when Array
|
36
|
+
{
|
37
|
+
type: :array,
|
38
|
+
values: value.map{|v|serialize(v)},
|
39
|
+
}
|
40
|
+
when Hash
|
41
|
+
{
|
42
|
+
type: :hash,
|
43
|
+
values: value.inject({}){|h, kv|(k,v)=kv;h[k.to_s]=serialize(v);h},
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def prepare_serialization_default(value)
|
49
|
+
{
|
50
|
+
type: 'marshal',
|
51
|
+
value: Marshal.dump(value).unpack('H*').first,
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
module_function :prepare_serialization_specific, :prepare_serialization_default, :prepare_serialization
|
56
|
+
|
57
|
+
module WithActiveRecord
|
58
|
+
def self.included(base)
|
59
|
+
base.module_eval do
|
60
|
+
alias_method :prepare_serialization_specific_original, :prepare_serialization_specific
|
61
|
+
|
62
|
+
def prepare_serialization_specific(value)
|
63
|
+
prepare_serialization_specific_original(value) || prepare_serialization_active_record(value)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def prepare_serialization_active_record(value)
|
68
|
+
return {
|
69
|
+
type: :active_record,
|
70
|
+
class: value.class.name,
|
71
|
+
id: value.id,
|
72
|
+
} if value.is_a? ActiveRecord::Base
|
73
|
+
end
|
74
|
+
|
75
|
+
module_function :prepare_serialization_specific_original, :prepare_serialization_active_record
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
include(WithActiveRecord) if defined? ::ActiveRecord::Base
|
81
|
+
|
82
|
+
def restore(value)
|
83
|
+
case value['type']
|
84
|
+
when 'raw'
|
85
|
+
eval value['value']
|
86
|
+
when 'date'
|
87
|
+
Date.strptime(value['value'], DATE_FORMAT)
|
88
|
+
when 'time'
|
89
|
+
Time.strptime(value['value'], DATE_TIME_FORMAT)
|
90
|
+
when 'class'
|
91
|
+
Module.const_get value['value'].to_sym
|
92
|
+
when 'active_record'
|
93
|
+
klass = const_get value['class'].to_sym
|
94
|
+
klass.find value['id']
|
95
|
+
when 'array'
|
96
|
+
value['values'].map do |value|
|
97
|
+
restore(value)
|
98
|
+
end
|
99
|
+
when 'hash'
|
100
|
+
value['values'].inject({}) do |h, key_value|
|
101
|
+
(key, value) = key_value
|
102
|
+
h[key.to_sym] = restore(key_value)
|
103
|
+
h
|
104
|
+
end
|
105
|
+
else
|
106
|
+
Marshal.restore [value['value']].pack('H*')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Arsenicum
|
2
|
+
class Server
|
3
|
+
attr_reader :watchdogs
|
4
|
+
attr_reader :config
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.start(config = Arsenicum::Configuration.instance)
|
11
|
+
Process.daemon(true, true) if config.background
|
12
|
+
File.open(config.pidfile, 'w'){|f|f.puts $$} if config.pidfile
|
13
|
+
new(config).start
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
puts "Booting Arsenicum Server..."
|
18
|
+
Signal.trap(:INT, &method(:trap_interruption))
|
19
|
+
|
20
|
+
queue_class = config.queue_class
|
21
|
+
@watchdogs = config.create_queues.map do |queue|
|
22
|
+
Arsenicum::WatchDog.new(queue, config.logger)
|
23
|
+
end
|
24
|
+
@watchdogs.each(&:boot)
|
25
|
+
|
26
|
+
loop { sleep 10 }
|
27
|
+
end
|
28
|
+
|
29
|
+
def shutdown
|
30
|
+
@watchdogs.each(&:shutdown)
|
31
|
+
File.delete config.pidfile if config.pidfile
|
32
|
+
Thread.current.terminate
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def trap_interruption(*)
|
37
|
+
shutdown
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Arsenicum::Sqs
|
4
|
+
class Queue < Arsenicum::Queue
|
5
|
+
attr_reader :account
|
6
|
+
attr_reader :sqs
|
7
|
+
attr_reader :wait_timeout
|
8
|
+
attr_reader :failure_queue_name
|
9
|
+
attr_reader :queue_configuration
|
10
|
+
|
11
|
+
def configure(config)
|
12
|
+
@account = config.delete :account
|
13
|
+
@sqs = AWS::SQS.new account
|
14
|
+
@wait_timeout =
|
15
|
+
if config.delete(:long_poll)
|
16
|
+
nil
|
17
|
+
elsif timeout = config.delete(:wait_timeout)
|
18
|
+
timeout.to_i
|
19
|
+
end
|
20
|
+
@failure_queue_name = config.delete :failure_queue_name
|
21
|
+
|
22
|
+
@queue_configuration = config
|
23
|
+
end
|
24
|
+
|
25
|
+
def put_to_queue(json, named: name)
|
26
|
+
sqs_queue = sqs.queues.named(named.to_s)
|
27
|
+
sqs_queue.send_message(json)
|
28
|
+
end
|
29
|
+
|
30
|
+
def poll
|
31
|
+
sqs.queues.named(name.to_s).poll(wait_time_out: wait_timeout) do |message|
|
32
|
+
logger.debug { "RAW_MESSAGE #{message.inspect}" }
|
33
|
+
{
|
34
|
+
message_body: message.body,
|
35
|
+
message_id: message.handle,
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_message_status(message_id, successful, json)
|
41
|
+
put_to_queue(json, named: failure_queue_name) unless successful
|
42
|
+
|
43
|
+
sqs_queue = sqs.named(name)
|
44
|
+
sqs.client.delete_message queue_url: sqs_queue.url, receipt_handle: message_id
|
45
|
+
end
|
46
|
+
|
47
|
+
CREATION_OPTIONS = [
|
48
|
+
:visibility_timeout, :maximum_message_size,
|
49
|
+
:delay_seconds, :message_retention_period,
|
50
|
+
].freeze
|
51
|
+
CREATION_OPTIONS_IN_JSON = [
|
52
|
+
:policy,
|
53
|
+
].freeze
|
54
|
+
|
55
|
+
def create_queue_backend
|
56
|
+
creation_options = queue_configuration.values_at(*CREATION_OPTIONS).
|
57
|
+
each_with_index.inject({}) do |opts, vi|
|
58
|
+
|
59
|
+
(value, i) = vi
|
60
|
+
opts[CREATION_OPTIONS[i]] = value if value
|
61
|
+
opts
|
62
|
+
end
|
63
|
+
CREATION_OPTIONS_IN_JSON.each do |opt|
|
64
|
+
creation_options[opt] = JSON(queue_configuration[opt]) if queue_configuration[opt]
|
65
|
+
end
|
66
|
+
|
67
|
+
begin
|
68
|
+
sqs.queues.named(name.to_s)
|
69
|
+
puts "Not Created Queue #{name}:Exists"
|
70
|
+
rescue AWS::SQS::Errors::NonExistentQueue
|
71
|
+
sqs.queues.create name.to_s, creation_options
|
72
|
+
puts "Created Queue #{name}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Arsenucum
|
2
|
+
module Syntax
|
3
|
+
autoload :DelayedJob, 'arsenicum/syntax/delayed_job'
|
4
|
+
|
5
|
+
def self.choose(syntax)
|
6
|
+
syntax_impl =
|
7
|
+
const_get syntax.to_s.gsub(/_([a-z])/){$1.upcase}.gsub(/^([a-z])/){$1.upcase}.to_sym
|
8
|
+
syntax_impl.enable!
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Arsenicum::Syntax
|
2
|
+
module DelayedJob
|
3
|
+
class DelayedObject < BasicObject
|
4
|
+
def initialize(wrapped_object)
|
5
|
+
@wrapped_object = wrapped_object
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing(method_id, *arguments)
|
9
|
+
Arsenicum::QueueProxy.instance.async(@wrapped_object, method_id, *arguments)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ObjectExt
|
14
|
+
def delay
|
15
|
+
DelayedObject.new(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class <<self
|
20
|
+
def enable!
|
21
|
+
Object.__send__(:include, ObjectExt)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Arsenicum
|
4
|
+
class Task
|
5
|
+
include Serialization
|
6
|
+
|
7
|
+
attr_reader :target, :method, :arguments, :timestamp, :message_id, :exception
|
8
|
+
|
9
|
+
def self.parse(raw_message, message_id)
|
10
|
+
message_content = JSON(raw_message)
|
11
|
+
|
12
|
+
timestamp = message_content['timestamp']
|
13
|
+
method = message_content['method_name'].to_sym
|
14
|
+
|
15
|
+
target = restore(message_content['target'])
|
16
|
+
arguments = message_content['arguments'].nil? ? [] :
|
17
|
+
message_content['arguments'].map{|arg|restore(arg)}
|
18
|
+
|
19
|
+
new(target, method, arguments, timestamp, message_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(target, method, arguments, timestamp, message_id)
|
23
|
+
@target = target
|
24
|
+
@method = method
|
25
|
+
@arguments = arguments
|
26
|
+
@timestamp = timestamp
|
27
|
+
@message_id = message_id
|
28
|
+
end
|
29
|
+
|
30
|
+
def prepare_serialization
|
31
|
+
{
|
32
|
+
target: prepare_serialization(target),
|
33
|
+
timestamp: (Time.now.to_f * 1000000).to_i,
|
34
|
+
method_name: method_name,
|
35
|
+
arguments: arguments.nil? ? nil : arguments.map{|arg|prepare_serialization(arg)},
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def serialize
|
40
|
+
JSON(prepare_serialization)
|
41
|
+
end
|
42
|
+
|
43
|
+
def execute!
|
44
|
+
target.__send__ method, *arguments
|
45
|
+
rescue Exception => e
|
46
|
+
@exception = e
|
47
|
+
end
|
48
|
+
|
49
|
+
def successful?
|
50
|
+
!exception
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Arsenicum
|
2
|
+
class WatchDog
|
3
|
+
attr_reader :queue, :logger
|
4
|
+
|
5
|
+
def initialize(queue, logger)
|
6
|
+
@queue = queue
|
7
|
+
@logger = logger
|
8
|
+
|
9
|
+
@task_queue = Array.new
|
10
|
+
@mutex = Mutex.new
|
11
|
+
@workers = queue.concurrency.times.map{|_|create_worker}
|
12
|
+
end
|
13
|
+
|
14
|
+
def boot
|
15
|
+
@main_thread = Thread.new do
|
16
|
+
loop do
|
17
|
+
message = queue.poll
|
18
|
+
logger.debug { "received message #{message.inspect}" }
|
19
|
+
next unless message
|
20
|
+
|
21
|
+
# FIXME: overtime queue stocking.
|
22
|
+
@mutex.synchronize do
|
23
|
+
@task_queue.push Task.parse(message[:message_body], message[:message_id])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def shutdown
|
30
|
+
@workers.each do |worker|
|
31
|
+
begin
|
32
|
+
worker.terminate
|
33
|
+
rescue Exception
|
34
|
+
end
|
35
|
+
end
|
36
|
+
begin
|
37
|
+
@main_thread.terminate
|
38
|
+
rescue Exception
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def create_worker
|
44
|
+
Worker.new(@task_queue, @queue, @mutex)
|
45
|
+
end
|
46
|
+
|
47
|
+
#:nodoc:
|
48
|
+
class Worker < ::Thread
|
49
|
+
attr_reader :running
|
50
|
+
|
51
|
+
def initialize(task_queue, queue, mutex)
|
52
|
+
super do
|
53
|
+
loop do
|
54
|
+
mutex.synchronize { task = task_queue.shift }
|
55
|
+
|
56
|
+
unless task
|
57
|
+
sleep 0.1
|
58
|
+
next
|
59
|
+
end
|
60
|
+
|
61
|
+
task.execute!
|
62
|
+
queue.update_message_status(task.message_id, task.successful?, task.serialize)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
queue_type: sqs
|
2
|
+
queues:
|
3
|
+
default:
|
4
|
+
concurrency: 10
|
5
|
+
sample01:
|
6
|
+
concurrency: 3
|
7
|
+
methods:
|
8
|
+
- 'SampleA#hoge1'
|
9
|
+
- 'SampleB.hoge2'
|
10
|
+
classes:
|
11
|
+
- 'ClassInvocation1'
|
12
|
+
sample02:
|
13
|
+
concurrency: 2
|
14
|
+
methods:
|
15
|
+
- 'SampleA#hoge2'
|
16
|
+
- 'SampleB.hoge1'
|
17
|
+
sqs:
|
18
|
+
account:
|
19
|
+
access_key_id: 'YOUR_ACCESS_KEY_ID'
|
20
|
+
secret_access_key: 'YOUR SECRET ACCESS KEY'
|
21
|
+
region: 'ap-northeast-1'
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: arsenicum
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- condor
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: 'Arsenicum: multi-backend asyncronous processor.'
|
56
|
+
email:
|
57
|
+
- condor1226@gmail.com
|
58
|
+
executables:
|
59
|
+
- arsenicum
|
60
|
+
- arsenicum_rails
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- .gitignore
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- arsenicum.gemspec
|
69
|
+
- bin/arsenicum
|
70
|
+
- bin/arsenicum_rails
|
71
|
+
- lib/arsenicum-rails.rb
|
72
|
+
- lib/arsenicum.rb
|
73
|
+
- lib/arsenicum/actor.rb
|
74
|
+
- lib/arsenicum/cli.rb
|
75
|
+
- lib/arsenicum/cli/rails.rb
|
76
|
+
- lib/arsenicum/configuration.rb
|
77
|
+
- lib/arsenicum/queue.rb
|
78
|
+
- lib/arsenicum/queue_proxy.rb
|
79
|
+
- lib/arsenicum/rake_tasks.rake
|
80
|
+
- lib/arsenicum/serialization.rb
|
81
|
+
- lib/arsenicum/server.rb
|
82
|
+
- lib/arsenicum/sqs.rb
|
83
|
+
- lib/arsenicum/sqs/queue.rb
|
84
|
+
- lib/arsenicum/syntax.rb
|
85
|
+
- lib/arsenicum/syntax/delayed_job.rb
|
86
|
+
- lib/arsenicum/task.rb
|
87
|
+
- lib/arsenicum/version.rb
|
88
|
+
- lib/arsenicum/watchdog.rb
|
89
|
+
- spec/config.example.yml
|
90
|
+
homepage: ''
|
91
|
+
licenses:
|
92
|
+
- MIT
|
93
|
+
metadata: {}
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 2.0.3
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: 'Arsenicum: multi-backend asyncronous processor.'
|
114
|
+
test_files:
|
115
|
+
- spec/config.example.yml
|