arsenicum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ spec/config.yml
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in arsenicum.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arsenicum'
4
+
5
+ Arsenicum::CLI::Rails.new(ARGV).boot
@@ -0,0 +1,10 @@
1
+ require 'arsenicum'
2
+ require 'rails'
3
+
4
+ module Arsenicum
5
+ class Railtie < ::Rails::Railtie
6
+ rake_tasks do
7
+ load 'arsenicum/rake_tasks.rake'
8
+ end
9
+ end
10
+ end
@@ -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,14 @@
1
+ module Arsenicum
2
+ class Actor
3
+ attr_reader :queue
4
+
5
+ def initialize(queue)
6
+ @queue = queue
7
+ end
8
+
9
+ def process(task)
10
+ task.execute
11
+ queue.update_message_status(task.message_id, task.successful?)
12
+ end
13
+ end
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,5 @@
1
+ module Arsenicum
2
+ module Sqs
3
+ autoload :Queue, 'arsenicum/sqs/queue'
4
+ end
5
+ 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,3 @@
1
+ module Arsenicum
2
+ VERSION = "0.0.1"
3
+ 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