qswarm 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.
- 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: []
|