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 ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in qswarm.gemspec
4
+ gemspec
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
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'qswarm'
4
+
5
+ usage = "#{$0} <configuration file>"
6
+ abort usage unless config = ARGV.shift
7
+
8
+ swarm = Qswarm::Swarm.load config
9
+ swarm.log.level = Logger::INFO
10
+ swarm.run
@@ -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,17 @@
1
+ module Qswarm
2
+ module DSL
3
+ def dsl_accessor(*symbols)
4
+ symbols.each { |sym|
5
+ class_eval %{
6
+ def #{sym}(*val)
7
+ if val.empty?
8
+ @#{sym}
9
+ else
10
+ @#{sym} = val.size == 1 ? val[0] : val
11
+ end
12
+ end
13
+ }
14
+ }
15
+ end
16
+ end
17
+ end
@@ -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,13 @@
1
+ require 'logger'
2
+
3
+ module Qswarm
4
+ module Loggable
5
+ def logger
6
+ Loggable.logger
7
+ end
8
+
9
+ def self.logger
10
+ @logger ||= Logger.new(STDOUT)
11
+ end
12
+ end
13
+ 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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Qswarm
2
+ VERSION = "0.0.1"
3
+ end
data/lib/qswarm.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "qswarm/version"
2
+ require 'qswarm/loggable'
3
+ require 'qswarm/swarm'
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: []