qswarm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +1 -0
- data/Rakefile +1 -0
- data/bin/qswarm +10 -0
- data/lib/qswarm/agent.rb +41 -0
- data/lib/qswarm/broker.rb +77 -0
- data/lib/qswarm/dsl.rb +17 -0
- data/lib/qswarm/listener.rb +81 -0
- data/lib/qswarm/loggable.rb +13 -0
- data/lib/qswarm/speaker.rb +62 -0
- data/lib/qswarm/swarm.rb +51 -0
- data/lib/qswarm/version.rb +3 -0
- data/lib/qswarm.rb +3 -0
- data/qswarm.gemspec +30 -0
- metadata +119 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Work in progress
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/qswarm
ADDED
data/lib/qswarm/agent.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'qswarm/broker'
|
2
|
+
require 'qswarm/listener'
|
3
|
+
|
4
|
+
module Qswarm
|
5
|
+
class Agent
|
6
|
+
include Qswarm::Loggable
|
7
|
+
|
8
|
+
attr_reader :swarm, :name
|
9
|
+
|
10
|
+
def initialize(swarm, name, &block)
|
11
|
+
@swarm = swarm
|
12
|
+
@name = name.to_s
|
13
|
+
@brokers = {}
|
14
|
+
@listeners = []
|
15
|
+
|
16
|
+
self.instance_eval(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def listen(name, args = nil, &block)
|
20
|
+
logger.info "Registering listener: #{name}"
|
21
|
+
@listeners << Qswarm::Listener.new(self, name, args, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def broker(name, &block)
|
25
|
+
logger.info "Registering broker: #{name}"
|
26
|
+
@brokers[name] = Qswarm::Broker.new(name, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_broker(name)
|
30
|
+
@brokers[name] || @swarm.get_broker(name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def bind
|
34
|
+
logger.info "Binding to exchange"
|
35
|
+
end
|
36
|
+
|
37
|
+
def run
|
38
|
+
@listeners.map { |l| l.run }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'amqp'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
require 'qswarm/dsl'
|
6
|
+
|
7
|
+
module Qswarm
|
8
|
+
class Broker
|
9
|
+
include Qswarm::Loggable
|
10
|
+
extend Qswarm::DSL
|
11
|
+
|
12
|
+
dsl_accessor :name, :host, :port, :user, :pass, :vhost, :exchange_type, :exchange_name, :durable, :prefetch
|
13
|
+
@@connection = {}
|
14
|
+
|
15
|
+
def initialize(name, &block)
|
16
|
+
@name = name
|
17
|
+
|
18
|
+
# Set some defaults
|
19
|
+
@host = 'localhost'
|
20
|
+
@port = 5672
|
21
|
+
@user = 'guest'
|
22
|
+
@pass = 'guest'
|
23
|
+
@vhost = ''
|
24
|
+
@exchange_type = :direct
|
25
|
+
@exchange_name = ''
|
26
|
+
@durable = true
|
27
|
+
@prefetch = nil
|
28
|
+
|
29
|
+
self.instance_eval(&block)
|
30
|
+
|
31
|
+
@queues = {}
|
32
|
+
@channels = {}
|
33
|
+
@exchange = nil
|
34
|
+
|
35
|
+
Signal.trap("INT") do
|
36
|
+
@@connection["#{@host}:#{@port}#{@vhost}"].close do
|
37
|
+
EM.stop { exit }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def queue name, routing_key = '', args = nil
|
43
|
+
@queues["#{name}/#{routing_key}"] ||= begin
|
44
|
+
logger.debug "Binding queue #{name}/#{routing_key}"
|
45
|
+
@queues["#{name}/#{routing_key}"] = channel(name, routing_key).queue(name, args).bind(exchange(channel(name, routing_key)), :routing_key => routing_key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def exchange(channel = nil)
|
50
|
+
@exchange ||= begin
|
51
|
+
@exchange = AMQP::Exchange.new(channel ||= AMQP::Channel.new(connection), @exchange_type, @exchange_name, :durable => @durable) do |exchange|
|
52
|
+
logger.debug "Declared #{@exchange_type} exchange #{@vhost}/#{@exchange_name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# ruby-amqp currently limits to 1 consumer per queue (to be fixed in future) so can't pool channels
|
58
|
+
def channel name, routing_key = ''
|
59
|
+
@channels["#{name}/#{routing_key}"] ||= begin
|
60
|
+
logger.debug "Opening channel for #{name}/#{routing_key}"
|
61
|
+
@channels["#{name}/#{routing_key}"] = AMQP::Channel.new(connection, :prefetch => @prefetch)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def connection
|
66
|
+
# Pool connections at the class level
|
67
|
+
@@connection["#{@host}:#{@port}#{@vhost}"] ||= begin
|
68
|
+
logger.debug "Connecting to AMQP broker at #{@host}:#{@port}#{@vhost}"
|
69
|
+
@@connection["#{@host}:#{@port}#{@vhost}"] = AMQP.connect("amqp://#{@user}:#{@pass}@#{@host}:#{@port}/#{CGI.escape(@vhost)}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
"amqp://#{@user}:#{@pass}@#{@host}:#{@port}/#{CGI.escape(@vhost)}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/qswarm/dsl.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'uuid'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
require 'qswarm/dsl'
|
5
|
+
require 'qswarm/speaker'
|
6
|
+
|
7
|
+
module Qswarm
|
8
|
+
class Listener
|
9
|
+
include Qswarm::Loggable
|
10
|
+
extend Qswarm::DSL
|
11
|
+
|
12
|
+
dsl_accessor :name, :broker, :format
|
13
|
+
attr_reader :agent
|
14
|
+
|
15
|
+
def initialize(agent, name, args, &block)
|
16
|
+
@agent = agent
|
17
|
+
@name = name.to_s
|
18
|
+
@speakers = []
|
19
|
+
@sinks = []
|
20
|
+
@format = :json
|
21
|
+
|
22
|
+
@queue_args = { :auto_delete => true, :durable => true, :exclusive => true }
|
23
|
+
@subscribe_args = { :exclusive => false, :ack => false }
|
24
|
+
|
25
|
+
# @subscribe_args.merge! args.delete(:subscribe) unless args.nil?
|
26
|
+
@queue_args.merge! args unless args.nil?
|
27
|
+
|
28
|
+
self.instance_eval(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def bind(routing_key, options = nil)
|
32
|
+
@bind = routing_key
|
33
|
+
@queue_args.merge! options unless options.nil?
|
34
|
+
logger.info "Binding listener #{@name} < #{routing_key}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def subscribe(*options)
|
38
|
+
Array[*options].each { |o| @subscribe_args[o] = true }
|
39
|
+
end
|
40
|
+
|
41
|
+
def ack?
|
42
|
+
@subscribe_args[:ack]
|
43
|
+
end
|
44
|
+
|
45
|
+
def swarm(instances = 1)
|
46
|
+
@uuid = '-' + UUID.generate
|
47
|
+
end
|
48
|
+
|
49
|
+
def speak(name = nil, &block)
|
50
|
+
@speakers << Qswarm::Speaker.new(self, name, &block)
|
51
|
+
logger.info "Registering speaker: #{name} < #{@name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_broker(name = nil)
|
55
|
+
name ||= @broker
|
56
|
+
@agent.get_broker(name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def run
|
60
|
+
@bind ||= @name
|
61
|
+
logger.info "Listening on #{@name} < #{@bind}"
|
62
|
+
|
63
|
+
get_broker.queue(@name + @uuid ||= '', @bind, @queue_args).subscribe(@subscribe_args) do |metadata, payload|
|
64
|
+
logger.debug "[#{@agent.name}] Received '#{payload.inspect}' on listener #{@name}/#{metadata.routing_key}"
|
65
|
+
|
66
|
+
running = @speakers.map { |s| s.object_id }
|
67
|
+
callback = proc do |speaker|
|
68
|
+
running.delete speaker.object_id
|
69
|
+
metadata.ack if ack? && running.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
@speakers.map do |speaker|
|
73
|
+
EM.defer nil, callback do
|
74
|
+
speaker.parse(metadata, payload)
|
75
|
+
speaker
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
require 'qswarm/dsl'
|
4
|
+
|
5
|
+
module Qswarm
|
6
|
+
class Speaker
|
7
|
+
include Qswarm::Loggable
|
8
|
+
extend Qswarm::DSL
|
9
|
+
|
10
|
+
dsl_accessor :broker
|
11
|
+
attr_reader :agent, :metadata, :heard, :name
|
12
|
+
|
13
|
+
def initialize(listener, name, &block)
|
14
|
+
@listener = listener
|
15
|
+
@agent = @listener.agent
|
16
|
+
@name = name.to_s unless name.nil?
|
17
|
+
@block = block
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(metadata, payload)
|
22
|
+
@metadata = metadata
|
23
|
+
case @listener.format
|
24
|
+
when :json
|
25
|
+
@heard = JSON.parse(payload)
|
26
|
+
else
|
27
|
+
@heard = payload
|
28
|
+
end
|
29
|
+
|
30
|
+
self.instance_exec(&@block)
|
31
|
+
rescue JSON::ParserError
|
32
|
+
error = "[#{@agent.name}@#{$fqdn}] JSON::ParserError on #{payload.inspect}"
|
33
|
+
logger.error error
|
34
|
+
publish :errors, :text, 'nomad.errors', error
|
35
|
+
end
|
36
|
+
|
37
|
+
def log(msg)
|
38
|
+
logger.info "[#{@agent.name}] #{msg}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def inject(format = :text, msg)
|
42
|
+
logger.debug "[#{@agent.name}] Sending '#{msg}' to broker #{get_broker(@broker).name}/#{@name}"
|
43
|
+
publish @broker, format, @name, msg
|
44
|
+
log msg if format == :text
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_broker(name)
|
48
|
+
@listener.get_broker(name)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def publish(broker_name, format, routing_key, msg)
|
54
|
+
case format
|
55
|
+
when :json
|
56
|
+
get_broker(broker_name).exchange.publish JSON.generate(msg), :routing_key => routing_key
|
57
|
+
when :text
|
58
|
+
get_broker(broker_name).exchange.publish msg, :routing_key => routing_key
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/qswarm/swarm.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'andand'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
require 'qswarm/agent'
|
5
|
+
require 'qswarm/broker'
|
6
|
+
require 'qswarm/dsl'
|
7
|
+
|
8
|
+
module Qswarm
|
9
|
+
class Swarm
|
10
|
+
include Qswarm::Loggable
|
11
|
+
extend Qswarm::DSL
|
12
|
+
|
13
|
+
dsl_accessor :errors_queue
|
14
|
+
|
15
|
+
def self.load(config)
|
16
|
+
dsl = new
|
17
|
+
dsl.instance_eval(File.read(config), config)
|
18
|
+
dsl
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@agents = []
|
23
|
+
$fqdn = Socket.gethostbyname(Socket.gethostname).first
|
24
|
+
@brokers = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def log
|
28
|
+
logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def agent(name, &block)
|
32
|
+
logger.info "Registering agent: #{name}"
|
33
|
+
@agents << Qswarm::Agent.new(self, name, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def broker(name, &block)
|
37
|
+
logger.info "Registering broker: #{name}"
|
38
|
+
@brokers[name] = Qswarm::Broker.new(name, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_broker(name)
|
42
|
+
@brokers[name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
EventMachine.run do
|
47
|
+
@agents.map { |a| a.run }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/qswarm.rb
ADDED
data/qswarm.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "qswarm/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "qswarm"
|
7
|
+
s.version = Qswarm::VERSION
|
8
|
+
s.authors = ["Mark Cheverton"]
|
9
|
+
s.email = ["mark.cheverton@ecafe.org"]
|
10
|
+
s.homepage = "http://github.com/ennui2342/qswarm"
|
11
|
+
s.summary = %q{Distributed queue based agents in Ruby}
|
12
|
+
s.description = %q{Framework for writing distributed agents hanging off an AMQP message bus}
|
13
|
+
|
14
|
+
s.rubyforge_project = "qswarm"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
24
|
+
|
25
|
+
s.add_dependency 'eventmachine'
|
26
|
+
s.add_dependency 'amqp'
|
27
|
+
s.add_dependency 'uuid'
|
28
|
+
s.add_dependency 'json'
|
29
|
+
s.add_dependency 'andand'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: qswarm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mark Cheverton
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-29 00:00:00.000000000 +00:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: eventmachine
|
17
|
+
requirement: &2153397540 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2153397540
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: amqp
|
28
|
+
requirement: &2153397120 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *2153397120
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: uuid
|
39
|
+
requirement: &2153396700 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *2153396700
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: json
|
50
|
+
requirement: &2153396280 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *2153396280
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: andand
|
61
|
+
requirement: &2153395860 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
type: :runtime
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *2153395860
|
70
|
+
description: Framework for writing distributed agents hanging off an AMQP message
|
71
|
+
bus
|
72
|
+
email:
|
73
|
+
- mark.cheverton@ecafe.org
|
74
|
+
executables:
|
75
|
+
- qswarm
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- .gitignore
|
80
|
+
- Gemfile
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/qswarm
|
84
|
+
- lib/qswarm.rb
|
85
|
+
- lib/qswarm/agent.rb
|
86
|
+
- lib/qswarm/broker.rb
|
87
|
+
- lib/qswarm/dsl.rb
|
88
|
+
- lib/qswarm/listener.rb
|
89
|
+
- lib/qswarm/loggable.rb
|
90
|
+
- lib/qswarm/speaker.rb
|
91
|
+
- lib/qswarm/swarm.rb
|
92
|
+
- lib/qswarm/version.rb
|
93
|
+
- qswarm.gemspec
|
94
|
+
has_rdoc: true
|
95
|
+
homepage: http://github.com/ennui2342/qswarm
|
96
|
+
licenses: []
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project: qswarm
|
115
|
+
rubygems_version: 1.6.2
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: Distributed queue based agents in Ruby
|
119
|
+
test_files: []
|