arsenicum 0.0.1

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.
@@ -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