skein 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +69 -0
- data/README.md +57 -0
- data/RELEASES.md +4 -0
- data/Rakefile +30 -0
- data/VERSION +1 -0
- data/bin/skein +186 -0
- data/config/.gitignore +3 -0
- data/config/skein.yml.example +11 -0
- data/lib/skein.rb +24 -0
- data/lib/skein/client.rb +51 -0
- data/lib/skein/client/publisher.rb +14 -0
- data/lib/skein/client/rpc.rb +96 -0
- data/lib/skein/client/subscriber.rb +25 -0
- data/lib/skein/client/worker.rb +51 -0
- data/lib/skein/config.rb +87 -0
- data/lib/skein/connected.rb +52 -0
- data/lib/skein/context.rb +38 -0
- data/lib/skein/handler.rb +86 -0
- data/lib/skein/handler/async.rb +9 -0
- data/lib/skein/handler/threaded.rb +7 -0
- data/lib/skein/rabbitmq.rb +44 -0
- data/lib/skein/reporter.rb +11 -0
- data/lib/skein/rpc.rb +24 -0
- data/lib/skein/rpc/base.rb +23 -0
- data/lib/skein/rpc/error.rb +34 -0
- data/lib/skein/rpc/notification.rb +2 -0
- data/lib/skein/rpc/request.rb +62 -0
- data/lib/skein/rpc/response.rb +38 -0
- data/lib/skein/support.rb +67 -0
- data/skein.gemspec +95 -0
- data/test/data/sample_config.yml +13 -0
- data/test/helper.rb +42 -0
- data/test/script/em_example +28 -0
- data/test/unit/test_skein_client.rb +18 -0
- data/test/unit/test_skein_client_publisher.rb +10 -0
- data/test/unit/test_skein_client_subscriber.rb +41 -0
- data/test/unit/test_skein_client_worker.rb +61 -0
- data/test/unit/test_skein_config.rb +33 -0
- data/test/unit/test_skein_context.rb +44 -0
- data/test/unit/test_skein_rabbitmq.rb +14 -0
- data/test/unit/test_skein_reporter.rb +4 -0
- data/test/unit/test_skein_rpc_error.rb +10 -0
- data/test/unit/test_skein_rpc_request.rb +93 -0
- data/test/unit/test_skein_support.rb +95 -0
- metadata +148 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
class Skein::Client::Publisher < Skein::Connected
|
2
|
+
# == Instance Methods =====================================================
|
3
|
+
|
4
|
+
def initialize(queue_name, connection: nil, context: nil)
|
5
|
+
super(connection: connection, context: context)
|
6
|
+
|
7
|
+
@queue = self.channel.topic(queue_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def publish!(message, routing_key = nil)
|
11
|
+
@queue.publish(JSON.dump(message), routing_key: routing_key)
|
12
|
+
end
|
13
|
+
alias_method :<<, :publish!
|
14
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'fiber'
|
3
|
+
|
4
|
+
class Skein::Client::RPC < Skein::Connected
|
5
|
+
# == Constants ============================================================
|
6
|
+
|
7
|
+
QUEUE_NAME_DEFAULT = 'skein_rpc'.freeze
|
8
|
+
|
9
|
+
# == Properties ===========================================================
|
10
|
+
|
11
|
+
# == Instance Methods =====================================================
|
12
|
+
|
13
|
+
def initialize(queue_name = nil, connection: nil, context: nil)
|
14
|
+
super(connection: connection, context: context)
|
15
|
+
|
16
|
+
@rpc_queue = self.channel.queue(queue_name || QUEUE_NAME_DEFAULT, durable: true)
|
17
|
+
@response_queue = self.channel.queue(@ident, durable: true, header: true, auto_delete: true)
|
18
|
+
|
19
|
+
@callback = { }
|
20
|
+
|
21
|
+
@consumer = @response_queue.subscribe do |metadata, payload, extra|
|
22
|
+
# FIX: Deal with mixup between Bunny and MarchHare
|
23
|
+
# puts [metadata,payload,extra].inspect
|
24
|
+
|
25
|
+
# puts [metadata,payload,extra].map(&:class).inspect
|
26
|
+
|
27
|
+
if (extra)
|
28
|
+
payload = extra
|
29
|
+
elsif (!payload)
|
30
|
+
payload = metadata
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
response = JSON.load(payload)
|
35
|
+
|
36
|
+
if (callback = @callback.delete(response['id']))
|
37
|
+
case (callback)
|
38
|
+
when Queue
|
39
|
+
callback << response['result']
|
40
|
+
when Proc
|
41
|
+
callback.call
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue => e
|
45
|
+
# FIX: Error handling
|
46
|
+
puts e.inspect
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def close
|
52
|
+
@consumer and @consumer.cancel
|
53
|
+
@consumer = nil
|
54
|
+
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_missing(name, *args)
|
59
|
+
name = name.to_s
|
60
|
+
|
61
|
+
blocking = !name.sub!(/!\z/, '')
|
62
|
+
|
63
|
+
message_id = SecureRandom.uuid
|
64
|
+
request = JSON.dump(
|
65
|
+
method: name,
|
66
|
+
params: args,
|
67
|
+
id: message_id
|
68
|
+
)
|
69
|
+
|
70
|
+
@channel.default_exchange.publish(
|
71
|
+
request,
|
72
|
+
routing_key: @rpc_queue.name,
|
73
|
+
reply_to: blocking ? @ident : nil,
|
74
|
+
message_id: message_id
|
75
|
+
)
|
76
|
+
|
77
|
+
if (block_given?)
|
78
|
+
@callback[message_id] =
|
79
|
+
if (defined?(EventMachine))
|
80
|
+
EventMachine.next_tick do
|
81
|
+
yield
|
82
|
+
end
|
83
|
+
else
|
84
|
+
lambda do
|
85
|
+
yield
|
86
|
+
end
|
87
|
+
end
|
88
|
+
elsif (blocking)
|
89
|
+
queue = Queue.new
|
90
|
+
|
91
|
+
@callback[message_id] = queue
|
92
|
+
|
93
|
+
queue.pop
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Skein::Client::Subscriber < Skein::Connected
|
2
|
+
# == Instance Methods =====================================================
|
3
|
+
|
4
|
+
def initialize(queue_name, routing_key = nil, connection: nil, context: nil)
|
5
|
+
super(connection: connection, context: context)
|
6
|
+
|
7
|
+
@queue = self.channel.topic(queue_name)
|
8
|
+
@subscribe_queue = self.channel.queue('', exclusive: true)
|
9
|
+
|
10
|
+
@subscribe_queue.bind(@queue, routing_key: routing_key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def listen
|
14
|
+
case (@subscribe_queue.class.to_s.split(/::/)[0])
|
15
|
+
when 'Bunny'
|
16
|
+
@subscribe_queue.subscribe(block: true) do |delivery_info, properties, payload|
|
17
|
+
yield(JSON.load(payload), delivery_info, properties)
|
18
|
+
end
|
19
|
+
when 'MarchHare'
|
20
|
+
@subscribe_queue.subscribe(block: true) do |metadata, payload|
|
21
|
+
yield(JSON.load(payload), metadata)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Skein::Client::Worker < Skein::Connected
|
4
|
+
# == Instance Methods =====================================================
|
5
|
+
|
6
|
+
def initialize(queue_name, connection: nil, context: nil)
|
7
|
+
super(connection: connection, context: context)
|
8
|
+
|
9
|
+
lock do
|
10
|
+
queue = self.channel.queue(queue_name, durable: true)
|
11
|
+
|
12
|
+
@handler = Skein::Handler.for(self)
|
13
|
+
|
14
|
+
@subscriber = Skein::Client::Subscriber.new(queue_name, connection: self.connection)
|
15
|
+
|
16
|
+
@thread = Thread.new do
|
17
|
+
@subscriber.listen do |payload, delivery_tag, reply_to|
|
18
|
+
@handler.handle(payload) do |reply_json|
|
19
|
+
channel.acknowledge(delivery_tag, true)
|
20
|
+
|
21
|
+
if (reply_to)
|
22
|
+
channel.default_exchange.publish(
|
23
|
+
reply_json,
|
24
|
+
routing_key: reply_to
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def close
|
34
|
+
@thread.kill
|
35
|
+
@thread.join
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def join
|
41
|
+
@thread and @thread.join
|
42
|
+
end
|
43
|
+
|
44
|
+
def async?
|
45
|
+
# Define this method as `true` in any subclass that requires async
|
46
|
+
# callback-style delegation.
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
end
|
data/lib/skein/config.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
class Skein::Config < OpenStruct
|
5
|
+
# == Constants ============================================================
|
6
|
+
|
7
|
+
CONFIG_PATH_DEFAULT = 'config/skein.yml'.freeze
|
8
|
+
|
9
|
+
ENV_DEFAULT = 'development'.freeze
|
10
|
+
|
11
|
+
DRIVERS = {
|
12
|
+
bunny: 'Bunny',
|
13
|
+
march_hare: 'MarchHare'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
DRIVER_PLATFORM_DEFAULT = Hash.new(:bunny).merge(
|
17
|
+
"java" => :march_hare
|
18
|
+
).freeze
|
19
|
+
|
20
|
+
DRIVER_DEFAULT = (
|
21
|
+
DRIVERS.find do |name, const|
|
22
|
+
const_defined?(const)
|
23
|
+
end || [ ]
|
24
|
+
)[0] || DRIVER_PLATFORM_DEFAULT[RUBY_PLATFORM]
|
25
|
+
|
26
|
+
DEFAULTS = {
|
27
|
+
host: '127.0.0.1',
|
28
|
+
port: 5672,
|
29
|
+
username: 'guest',
|
30
|
+
password: 'guest',
|
31
|
+
driver: DRIVER_DEFAULT,
|
32
|
+
namespace: nil
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
# == Class Methods ========================================================
|
36
|
+
|
37
|
+
def self.root
|
38
|
+
if (defined?(Rails))
|
39
|
+
Rails.root
|
40
|
+
else
|
41
|
+
Dir.pwd
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.env
|
46
|
+
if (defined?(Rails))
|
47
|
+
Rails.env.to_s
|
48
|
+
else
|
49
|
+
ENV['RAILS_ENV'] || ENV_DEFAULT
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.path
|
54
|
+
File.expand_path(CONFIG_PATH_DEFAULT, self.root)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.exist?
|
58
|
+
File.exist?(self.path)
|
59
|
+
end
|
60
|
+
|
61
|
+
# == Instance Methods =====================================================
|
62
|
+
|
63
|
+
def initialize(options = nil)
|
64
|
+
config_path = nil
|
65
|
+
|
66
|
+
case (options)
|
67
|
+
when String
|
68
|
+
if (File.exist?(options))
|
69
|
+
config_path = options
|
70
|
+
end
|
71
|
+
when Hash
|
72
|
+
super(options)
|
73
|
+
|
74
|
+
return
|
75
|
+
when false, :default
|
76
|
+
# Ignore configuration file, use defaults
|
77
|
+
else
|
78
|
+
config_path = File.expand_path('config/skein.yml', self.class.root)
|
79
|
+
end
|
80
|
+
|
81
|
+
if (config_path and File.exist?(config_path))
|
82
|
+
super(DEFAULTS.merge(YAML.load_file(config_path)[self.class.env] || { }))
|
83
|
+
else
|
84
|
+
super(DEFAULTS)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Skein::Connected
|
2
|
+
# == Properties ===========================================================
|
3
|
+
|
4
|
+
attr_reader :context
|
5
|
+
attr_reader :ident
|
6
|
+
attr_reader :connection
|
7
|
+
attr_reader :channel
|
8
|
+
|
9
|
+
# == Instance Methods =====================================================
|
10
|
+
|
11
|
+
def initialize(connection: nil, context: nil)
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@shared_connection = !!connection
|
14
|
+
|
15
|
+
@connection = connection || Skein::RabbitMQ.connect
|
16
|
+
@channel = @connection.create_channel
|
17
|
+
|
18
|
+
@context = context || Skein::Context.new
|
19
|
+
@ident = @context.ident(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def lock
|
23
|
+
@mutex.synchronize do
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
lock do
|
30
|
+
begin
|
31
|
+
@channel and @channel.close
|
32
|
+
|
33
|
+
rescue => e
|
34
|
+
if (defined?(MarchHare))
|
35
|
+
case e
|
36
|
+
when MarchHare::ChannelLevelException, MarchHare::ChannelAlreadyClosed
|
37
|
+
# Ignored since we're finished with the channel anyway
|
38
|
+
else
|
39
|
+
raise e
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@channel = nil
|
45
|
+
|
46
|
+
unless (@shared_connection)
|
47
|
+
@connection and @connection.close
|
48
|
+
@connection = nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Skein::Context
|
2
|
+
# == Properties ===========================================================
|
3
|
+
|
4
|
+
attr_reader :hostname
|
5
|
+
attr_reader :process_name
|
6
|
+
attr_reader :process_id
|
7
|
+
attr_accessor :reporter
|
8
|
+
|
9
|
+
# == Class Methods ========================================================
|
10
|
+
|
11
|
+
def self.default
|
12
|
+
@default ||= self.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# == Instance Methods =====================================================
|
16
|
+
|
17
|
+
def initialize(hostname: nil, process_name: nil, process_id: nil, config: nil)
|
18
|
+
@hostname = (hostname || Skein::Support.hostname).dup.freeze
|
19
|
+
@process_name = (process_name || Skein::Support.process_name).dup.freeze
|
20
|
+
@process_id = process_id || Skein::Support.process_id
|
21
|
+
end
|
22
|
+
|
23
|
+
def ident(object)
|
24
|
+
# FUTURE: Add pack/unpack methods for whatever format this ends up being
|
25
|
+
# so the components can be extracted by another application for
|
26
|
+
# diagnostic reasons.
|
27
|
+
'%s#%d+%s@%s' % [
|
28
|
+
@process_name,
|
29
|
+
@process_id,
|
30
|
+
object.object_id,
|
31
|
+
@hostname
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
def exception!(*args)
|
36
|
+
@reporter and @reporter.exception!(*args)
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
class Skein::Handler
|
2
|
+
# == Class Methods ========================================================
|
3
|
+
|
4
|
+
def self.for(target)
|
5
|
+
case (target.respond_to?(:async?) and target.async?)
|
6
|
+
when true
|
7
|
+
Skein::Handler::Async.new(target)
|
8
|
+
else
|
9
|
+
Skein::Handler::Threaded.new(target)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# == Instance Methods =====================================================
|
14
|
+
|
15
|
+
def initialize(target)
|
16
|
+
@target = target
|
17
|
+
end
|
18
|
+
|
19
|
+
def handle(message_json)
|
20
|
+
# REFACTOR: Roll this into a module to keep it more contained.
|
21
|
+
# REFACTOR: Use Skein::RPC::Request
|
22
|
+
request =
|
23
|
+
begin
|
24
|
+
JSON.load(message_json)
|
25
|
+
|
26
|
+
rescue Object => e
|
27
|
+
@context.exception!(e, message_json)
|
28
|
+
|
29
|
+
return yield(JSON.dump(
|
30
|
+
result: nil,
|
31
|
+
error: '[%s] %s' % [ e.class, e ],
|
32
|
+
id: request['id']
|
33
|
+
))
|
34
|
+
end
|
35
|
+
|
36
|
+
case (request)
|
37
|
+
when Hash
|
38
|
+
# Acceptable
|
39
|
+
else
|
40
|
+
return yield(JSON.dump(
|
41
|
+
result: nil,
|
42
|
+
error: 'Request does not conform to the JSON-RPC format.',
|
43
|
+
id: nil
|
44
|
+
))
|
45
|
+
end
|
46
|
+
|
47
|
+
request['params'] =
|
48
|
+
case (params = request['params'])
|
49
|
+
when Array
|
50
|
+
params
|
51
|
+
when nil
|
52
|
+
request.key?('params') ? [ nil ] : [ ]
|
53
|
+
else
|
54
|
+
[ request['params'] ]
|
55
|
+
end
|
56
|
+
|
57
|
+
unless (request['method'] and request['method'].is_a?(String) and request['method'].match(/\S/))
|
58
|
+
return yield(JSON.dump(
|
59
|
+
result: nil,
|
60
|
+
error: 'Request does not conform to the JSON-RPC format, missing valid method.',
|
61
|
+
id: request['id']
|
62
|
+
))
|
63
|
+
end
|
64
|
+
|
65
|
+
begin
|
66
|
+
delegate(request['method'], *request['params']) do |result, error = nil|
|
67
|
+
yield(JSON.dump(
|
68
|
+
result: result,
|
69
|
+
error: error,
|
70
|
+
id: request['id']
|
71
|
+
))
|
72
|
+
end
|
73
|
+
rescue Object => e
|
74
|
+
@reporter and @reporter.exception!(e, message_json)
|
75
|
+
|
76
|
+
yield(JSON.dump(
|
77
|
+
result: nil,
|
78
|
+
error: '[%s] %s' % [ e.class, e ],
|
79
|
+
id: request['id']
|
80
|
+
))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
require_relative './handler/async'
|
86
|
+
require_relative './handler/threaded'
|