dakwak 1.0.3 → 1.6.5
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.
- checksums.yaml +7 -0
- data/lib/dakwak/admin/bot.rb +0 -0
- data/lib/dakwak/admin/console.rb +0 -0
- data/lib/dakwak/admin/message.rb +0 -0
- data/lib/dakwak/analytics/sheet.rb +77 -0
- data/lib/dakwak/configurable.rb +22 -0
- data/lib/dakwak/configurator.rb +61 -0
- data/lib/dakwak/log_manager.rb +52 -0
- data/lib/dakwak/logger.rb +127 -0
- data/lib/dakwak/messaging/channel.rb +215 -0
- data/lib/dakwak/messaging/communicator.rb +101 -0
- data/lib/dakwak/messaging/message.rb +68 -0
- data/lib/dakwak/messaging/private_channel.rb +143 -0
- data/lib/dakwak/messaging/station.rb +140 -0
- data/lib/dakwak/mongo_adapter.rb +141 -0
- data/lib/dakwak/utility.rb +8 -0
- data/lib/dakwak.rb +239 -8
- metadata +76 -41
- data/.gitignore +0 -18
- data/Gemfile +0 -4
- data/LICENSE +0 -22
- data/README.md +0 -28
- data/Rakefile +0 -2
- data/bin/dakwak +0 -5
- data/dakwak.gemspec +0 -22
- data/lib/dakwak/version.rb +0 -3
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 71fc14535973c5eeecdc876e3cb5c067599f044fa390c647fbafc0244e1e5c45
|
4
|
+
data.tar.gz: 79ffd7b76486f20b769074099e18188c48a425763c4571b1dd68d9850bb6809e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3c589b38d8eb433f629e828d6eae6068d78a1f3ae3aa5216abb137417e87713a83e4186518c040197fbb808c8e130e6ffc9ed680579d61430fa699f03e9a0abf
|
7
|
+
data.tar.gz: 5f06e1f3a4f11b27d1743e1af801d930295ed24784cd297146daf2725e118bd8684c9ce59eb30e55be0389cc3aeb4889a33760cb291b4a587735c1e4343ba974
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Dakwak
|
4
|
+
module Analytics # :nodoc:
|
5
|
+
|
6
|
+
##
|
7
|
+
# An analytics sheet is a container of stats that can be submitted
|
8
|
+
# to the analytics platform, dakana.
|
9
|
+
#
|
10
|
+
# The usage is very simple. [] and []= operators are overloaded
|
11
|
+
# to allow you to access sheets just like you would a Hash. When
|
12
|
+
# your done collecting stats (the work is over), submit it using
|
13
|
+
# #commit!
|
14
|
+
#
|
15
|
+
# Here's an example:
|
16
|
+
#
|
17
|
+
# class FlowerHunter
|
18
|
+
# def initialize
|
19
|
+
# # Prepare the sheet here
|
20
|
+
# @sheet = Sheet.new({
|
21
|
+
# :zombie_threat_eradicated => false,
|
22
|
+
# :flowers => {
|
23
|
+
# :watered => 0,
|
24
|
+
# :plucked => 0
|
25
|
+
# }
|
26
|
+
# })
|
27
|
+
# super()
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# def tend_to_flowers()
|
31
|
+
# # water a flower
|
32
|
+
# @sheet[:flowers][:watered] += 1
|
33
|
+
# # pluck 5 for your special someone
|
34
|
+
# @sheet[:flowers][:plucked] = 5
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def kill_zombies()
|
38
|
+
# # ...
|
39
|
+
# # BFG picked up
|
40
|
+
# @sheet[:zombie_threat_eradicated] = true
|
41
|
+
# # our work is done, let's submit the stats
|
42
|
+
# @sheet.commit!
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
class Sheet
|
46
|
+
attr_accessor :content # :nodoc:
|
47
|
+
|
48
|
+
# Convenience constructor for building a sheet with initial stats
|
49
|
+
def initialize(content = {})
|
50
|
+
@content = content
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the value of the stat titled _key_ (if any)
|
54
|
+
def [](key)
|
55
|
+
@content[key]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Defines a new stat as _key_ with any kind of value in _value_
|
59
|
+
def []=(key, value)
|
60
|
+
@content[key] = value
|
61
|
+
end
|
62
|
+
|
63
|
+
# Submits the sheet to the analytics platform.
|
64
|
+
def commit!
|
65
|
+
Dakwak.logger.log_debug "Comitting dakana sheet: #{serialize}" if Dakwak.debugging?
|
66
|
+
Communicator.new.publish(Message.new(serialize, {}), "analytics", Dakwak.app_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
#
|
71
|
+
def serialize()
|
72
|
+
@content.to_json
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Dakwak
|
2
|
+
class Configurable
|
3
|
+
attr_reader :cfg
|
4
|
+
|
5
|
+
module Impl
|
6
|
+
def set_option(k, v)
|
7
|
+
@cfg ||= {}
|
8
|
+
|
9
|
+
# convert string keys to symbols
|
10
|
+
if v.is_a?(Hash) then
|
11
|
+
new_v = {}
|
12
|
+
v.each_pair { |_k, _v| new_v[_k.to_sym] = v[_k] }
|
13
|
+
v = new_v
|
14
|
+
end
|
15
|
+
|
16
|
+
@cfg[k.to_sym] = v
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
include Impl
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'dakwak/logger'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Dakwak # :nodoc: all
|
5
|
+
class Configurator
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :subs
|
9
|
+
|
10
|
+
def subscribe(context, configurable)
|
11
|
+
if not configurable.respond_to?("set_option")
|
12
|
+
raise "Subscribing as a configurable must respond_to method 'set_option'"
|
13
|
+
end
|
14
|
+
|
15
|
+
@@subs ||= {}
|
16
|
+
@@subs[context] ||= []; @@subs[context] << configurable
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
include Logger
|
21
|
+
# include Silencable
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
logging_context("Configurator")
|
25
|
+
@@subs ||= {}
|
26
|
+
@@opts ||= {}
|
27
|
+
|
28
|
+
super()
|
29
|
+
end
|
30
|
+
|
31
|
+
def consume(json_feed)
|
32
|
+
begin
|
33
|
+
cfg = JSON.parse(json_feed)
|
34
|
+
rescue JSON::ParserError => e
|
35
|
+
log_error "Unable to parse JSON configuration feed; #{e.message}"
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
cfg.each_pair { |ctx, options|
|
41
|
+
|
42
|
+
if not @@subs[ctx] or @@subs[ctx].empty? then
|
43
|
+
log_warn "Context #{ctx} has no subscribers, ignoring." unless silent?
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
log_debug "Configuring context #{ctx} (#{@@subs[ctx].size} subscribers)" unless silent?
|
48
|
+
|
49
|
+
options.each_pair { |name, val|
|
50
|
+
@@subs[ctx] ||= []
|
51
|
+
@@subs[ctx].each { |configurable|
|
52
|
+
configurable.set_option(name, val)
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'syslog'
|
3
|
+
|
4
|
+
module Dakwak # :nodoc: all
|
5
|
+
class LogManager
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@log_mtx = Mutex.new
|
9
|
+
@@opts = { timestamp: true, vanilla: false }
|
10
|
+
# @sl = Syslog.open(Dakwak.app_name, Syslog::LOG_PID, Syslog::LOG_DAEMON)
|
11
|
+
@sl = Syslog.open(Dakwak.app_name, 0, Syslog::LOG_DAEMON)
|
12
|
+
@@levels ||= { }
|
13
|
+
LogLevel.constants.each { |c| @@levels[LogLevel.const_get(c)] = c.to_s.downcase }
|
14
|
+
# for syslog compatibility
|
15
|
+
@@levels["W"] = "warning"
|
16
|
+
@@levels["E"] = "err"
|
17
|
+
super()
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def disable_timestamps
|
22
|
+
@@opts[:timestamp] = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def enable_timestamps
|
26
|
+
@@opts[:timestamp] = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Available options:
|
31
|
+
# => :vanilla Message will be logged with no formatting (default: false )
|
32
|
+
# => :timestamp Whether to prefix the message with a timestamp (default: true)
|
33
|
+
# opts: deprecated, use accessors instead
|
34
|
+
def log(msg, level, opts = {})
|
35
|
+
@log_mtx.synchronize do
|
36
|
+
puts "#{msg}" or return if @@opts[:vanilla]
|
37
|
+
|
38
|
+
stamp = Time.now.strftime("%m-%d-%Y %H:%M:%S ") unless @@opts[:timestamp] == false
|
39
|
+
puts "#{stamp}#{Socket.gethostbyname(Socket.gethostname).first} #{Dakwak.app_name} [#{level}] #{msg}"
|
40
|
+
|
41
|
+
expanded_level = @@levels[level]
|
42
|
+
# disabling syslog support for now
|
43
|
+
# if @sl.respond_to?(expanded_level) then
|
44
|
+
# @sl.send :"#{expanded_level}", "[#{level}] #{msg}"
|
45
|
+
# end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Dakwak
|
2
|
+
|
3
|
+
class LogLevel
|
4
|
+
Debug = 'D'
|
5
|
+
Info = 'I'
|
6
|
+
Notice = 'N'
|
7
|
+
Warn = 'W'
|
8
|
+
Error = 'E'
|
9
|
+
Alert = 'A'
|
10
|
+
Crit = 'C'
|
11
|
+
end
|
12
|
+
|
13
|
+
module Silencable
|
14
|
+
module Impl
|
15
|
+
class << self
|
16
|
+
def silence(toggle)
|
17
|
+
@@opts ||= {}
|
18
|
+
@@opts[:silent] = toggle
|
19
|
+
end
|
20
|
+
|
21
|
+
def silent?
|
22
|
+
@@opts ||= {}
|
23
|
+
@@opts[:silent]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def silent?
|
29
|
+
if @opts && @opts.has_key?(:silent)
|
30
|
+
return @opts[:silent]
|
31
|
+
else
|
32
|
+
return Silencable::Impl.silent?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def silence(toggle)
|
37
|
+
@opts ||= {}
|
38
|
+
@opts[:silent] = toggle
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# A thread-safe logging interface.
|
44
|
+
module Logger
|
45
|
+
include Silencable
|
46
|
+
|
47
|
+
# The logging context (NDC) of this instance (defaults to "Unnamed")
|
48
|
+
def logging_context
|
49
|
+
@__log_ctx
|
50
|
+
end
|
51
|
+
|
52
|
+
# Assigns the logging context (NDC) of this logger instance.
|
53
|
+
def logging_context(context)
|
54
|
+
@__log_ctx = context
|
55
|
+
@__log_indent ||= 0
|
56
|
+
|
57
|
+
unless context.empty?
|
58
|
+
@__log_prefix = "#{@__log_ctx}: "
|
59
|
+
else
|
60
|
+
@__log_prefix = ""
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Logs a message.
|
65
|
+
#
|
66
|
+
# @param msg The message to log
|
67
|
+
# @param level The logging level (see LogLevel)
|
68
|
+
# @param opts Logging options (see LogManager::log)
|
69
|
+
#
|
70
|
+
def log(msg, level, opts = {})
|
71
|
+
|
72
|
+
print("#{@__log_prefix}#{"\t" * (@__log_indent || 0)}#{msg}")
|
73
|
+
Dakwak.log_manager.log("#{@__log_prefix}#{"\t" * (@__log_indent || 0)}#{msg}", level, opts) unless silent?
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.included(base) # :nodoc:
|
77
|
+
if defined?(Rails) then
|
78
|
+
@@levels ||= { }
|
79
|
+
LogLevel.constants.each { |c| @@levels[LogLevel.const_get(c)] = c.to_s.downcase }
|
80
|
+
send :define_method, :"log" do |msg, level, _|
|
81
|
+
Rails.logger.send(@@levels[level], msg)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
LogLevel.constants.each { |level|
|
86
|
+
method = :"log_#{level.to_s.downcase}"
|
87
|
+
return if method_defined? method
|
88
|
+
|
89
|
+
send :define_method, :"log_#{level.to_s.downcase}" do |*args|
|
90
|
+
msg = args[0]
|
91
|
+
opts = args[1] || {}
|
92
|
+
log(msg, LogLevel.const_get(level), opts)
|
93
|
+
end
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Indents all messages logged in the future with whitespace by 1 step.
|
98
|
+
def log_indent_inc
|
99
|
+
@__log_indent ||= 0
|
100
|
+
@__log_indent += 1
|
101
|
+
end
|
102
|
+
|
103
|
+
# Resets 1 step of whitespace indentation from all messages logged in the future.
|
104
|
+
def log_indent_dec
|
105
|
+
@__log_indent ||= 0
|
106
|
+
@__log_indent -= 1
|
107
|
+
end
|
108
|
+
|
109
|
+
# Messages logged within the given block will be indented by 1 step
|
110
|
+
def log_indent(&block) # :yield:
|
111
|
+
log_indent_inc
|
112
|
+
if block_given? then yield end
|
113
|
+
log_indent_dec
|
114
|
+
end
|
115
|
+
|
116
|
+
def initialize
|
117
|
+
@__log_ctx ||= "unnamed"
|
118
|
+
@__log_indent ||= 0
|
119
|
+
@__log_prefix ||= ""
|
120
|
+
super()
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
attr_reader :__log_ctx, :__log_indent, :__log_prefix
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'dakwak/logger'
|
2
|
+
|
3
|
+
module Dakwak
|
4
|
+
|
5
|
+
# Channels bind a number of queues which are consumed for messages sent by communicators.
|
6
|
+
#
|
7
|
+
# Dakwak::Communicator instances can _subscribe_ to channel queues to be notified
|
8
|
+
# of messages received, and can publish messages through channels directly.
|
9
|
+
#
|
10
|
+
# *Note*:
|
11
|
+
# Channels should not be managed directly, see Dakwak::Station for more info.
|
12
|
+
#
|
13
|
+
# *Note*:
|
14
|
+
# A channel is internally bound to an AMQP exchange entity.
|
15
|
+
class Channel
|
16
|
+
include Logger
|
17
|
+
|
18
|
+
attr_reader :session, :channel, :exchange
|
19
|
+
|
20
|
+
def name()
|
21
|
+
@__name
|
22
|
+
end
|
23
|
+
|
24
|
+
def opened?
|
25
|
+
@session && @session.connected?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Initiates connection with the AMQP broker using connection info
|
29
|
+
# stored in the Dakwak::Station and binds to a direct exchange
|
30
|
+
# identified by the name of this channel.
|
31
|
+
#
|
32
|
+
# Params:
|
33
|
+
# &on_open::
|
34
|
+
# A proc that will be called if and when the connection
|
35
|
+
# with the broker succeeds and the exchange is bound.
|
36
|
+
#
|
37
|
+
# *Note*:
|
38
|
+
# This is an asynchronous method.
|
39
|
+
def open(opts = { }, &on_open)
|
40
|
+
if opened?
|
41
|
+
on_open.call(self) if on_open
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
opts = { :passive => true, :durable => true }.merge(opts)
|
46
|
+
|
47
|
+
AMQP.connect(Station.instance.connection_options) do |session|
|
48
|
+
@session = session
|
49
|
+
AMQP::Channel.new(@session, :auto_recovery => true) do |channel|
|
50
|
+
@channel = channel
|
51
|
+
@exchange = @channel.direct(@__name, opts)
|
52
|
+
on_open.call(self) if on_open
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Terminates the connection with the AMQP broker and destroys
|
58
|
+
# all queue consumers.
|
59
|
+
#
|
60
|
+
# Attempting to close an already closed channel will log a warning
|
61
|
+
# and reject the request. However, if a block was passed, it will
|
62
|
+
# still be called.
|
63
|
+
#
|
64
|
+
# *Note*:
|
65
|
+
# This is an asynchronous method.
|
66
|
+
def close(&on_close)
|
67
|
+
unless opened?
|
68
|
+
log_warn "Attempting to close a closed channel."
|
69
|
+
on_close.call(self) if on_close
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
|
73
|
+
EventMachine.next_tick do
|
74
|
+
@session.close {
|
75
|
+
@queues = []
|
76
|
+
@session = nil
|
77
|
+
|
78
|
+
on_close.call(self) if on_close
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
# Sends a message over this channel to the destination queue.
|
86
|
+
#
|
87
|
+
# *Note*:
|
88
|
+
# Messages published over a queue that is being consumed by this
|
89
|
+
# application will be IGNORED. The effect is analogous to AMQP's
|
90
|
+
# no-local queue binding option. What this means to you is that
|
91
|
+
# you won't have to worry about receiving self-messages.
|
92
|
+
#
|
93
|
+
# *Note*:
|
94
|
+
# Messages are automatically stamped with the current application's
|
95
|
+
# id and set in the message.meta.app_id so there's no need to do
|
96
|
+
# it manually.
|
97
|
+
def publish(msg, queue, &callback)
|
98
|
+
if not opened? then
|
99
|
+
return open { |c| c.publish(msg, queue) }
|
100
|
+
end
|
101
|
+
|
102
|
+
@exchange.publish(msg.payload, msg.meta.merge!({
|
103
|
+
:routing_key => queue,
|
104
|
+
:app_id => Dakwak.app_fqn(),
|
105
|
+
:timestamp => Time.now.to_i
|
106
|
+
})) { callback.call(msg) if callback }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Subscribes a Communicator instance to messages received by a queue.
|
110
|
+
# The queue will automatically be consumed if it had not explicitly
|
111
|
+
# been marked for consumption.
|
112
|
+
#
|
113
|
+
# *Remark*:
|
114
|
+
# The subscriber doesn't necessarily have to be an instance of a
|
115
|
+
# Communicator, only that it responds to a method with the following signature:
|
116
|
+
# on_message_received(msg)
|
117
|
+
def subscribe(comm, queue, opts = {})
|
118
|
+
consume(queue, opts)
|
119
|
+
@queues[queue][:subscribers] << comm
|
120
|
+
end
|
121
|
+
|
122
|
+
# Messages received by this queue will no longer be dispatched to the
|
123
|
+
# Communicator.
|
124
|
+
def unsubscribe(comm, queue)
|
125
|
+
return unless consuming?(queue)
|
126
|
+
|
127
|
+
@queues[queue][:subscribers].delete_if { |v| v == comm }
|
128
|
+
end
|
129
|
+
|
130
|
+
# Removes all queue subscriptions for this Communicator. Messages
|
131
|
+
# over this channel will no longer be dispatched to it.
|
132
|
+
def unsubscribe_all(comm)
|
133
|
+
@queues.each { |q| unsubscribe(comm, q) }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Binds the specified queue and consumes any messages routed to it.
|
137
|
+
#
|
138
|
+
# *Note*:
|
139
|
+
# There's little point in consuming a queue with no subscribed Dakwak::Communicator
|
140
|
+
# instances to it. To pull messages from a queue, use Communicator::subscribe.
|
141
|
+
#
|
142
|
+
# Params:
|
143
|
+
# routing_key::
|
144
|
+
# The AMQP routing key to use when binding the new queue to the exchange.
|
145
|
+
# opts::
|
146
|
+
# - :exclusive
|
147
|
+
# The queue consumption will be exclusive to this consumer.
|
148
|
+
# - :nowait
|
149
|
+
# Does not wait for the broker to reply. If a binding error
|
150
|
+
# occurs, a Channel exception will be thrown.
|
151
|
+
def consume(key, opts = {})
|
152
|
+
return if consuming?(key)
|
153
|
+
|
154
|
+
|
155
|
+
queue_name = "#{key}_queue"
|
156
|
+
if opts.has_key?(:queue_name) then
|
157
|
+
queue_name = opts[:queue_name]
|
158
|
+
opts.delete(:queue_name)
|
159
|
+
end
|
160
|
+
log_info "Consuming '#{key}##{queue_name}'"
|
161
|
+
|
162
|
+
# assign some defaults
|
163
|
+
opts = {
|
164
|
+
:exclusive => false,
|
165
|
+
:nowait => true,
|
166
|
+
:durable => true,
|
167
|
+
:passive => true
|
168
|
+
}.merge(opts)
|
169
|
+
|
170
|
+
@queues[key] = { :queue => nil, :subscribers => [], :consumer => nil }
|
171
|
+
entry = @queues[key]
|
172
|
+
entry[:queue] = @channel.queue(queue_name, opts)
|
173
|
+
# entry[:queue].bind(@exchange, { :routing_key => key })
|
174
|
+
entry[:queue].subscribe { |meta, payload|
|
175
|
+
msg = Message.new(payload, meta, key)
|
176
|
+
dispatch(msg)
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
# Is this queue being consumed?
|
181
|
+
def consuming?(queue)
|
182
|
+
return @queues.has_key?(queue)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Creates a new channel with name as the AMQP exchange name. Do not create
|
186
|
+
# channel objects directly, use Dakwak::Station instead.
|
187
|
+
def initialize(name)
|
188
|
+
@__name = name # the name of the exchange this channel represents
|
189
|
+
logging_context("Channel[#{@__name}]")
|
190
|
+
|
191
|
+
@queues = {}
|
192
|
+
super()
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
# Dispatches the received message to subscribed Communicator instances
|
198
|
+
# if eligible.
|
199
|
+
#
|
200
|
+
# The following conditions must be met for a message to be eligible for
|
201
|
+
# dispatching:
|
202
|
+
# => 1. The message does not have an app_id equal to this application's (self-sent)
|
203
|
+
# => 2. The message has no reply_to field
|
204
|
+
# => 3. The message has a reply_to field that points to this application's id
|
205
|
+
def dispatch(msg)
|
206
|
+
# discard self-sent messages
|
207
|
+
return if msg.meta.app_id == Dakwak.app_fqn()
|
208
|
+
# discard messages that were not directed at this instance
|
209
|
+
return if msg.meta.reply_to && !msg.meta.reply_to.empty? && msg.meta.reply_to != DakTM.app_fqn()
|
210
|
+
|
211
|
+
log_info "Dispatching message #{msg}"
|
212
|
+
@queues[msg.queue][:subscribers].each { |c| c.on_message_received(msg) }
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'dakwak/logger'
|
2
|
+
require 'bson'
|
3
|
+
|
4
|
+
module Dakwak
|
5
|
+
|
6
|
+
# Communicator instances can send and receive messages across the platform.
|
7
|
+
class Communicator
|
8
|
+
|
9
|
+
# Subscribes this communicator to a number of queues in a channel. Messages routed
|
10
|
+
# through the indicated queues will be passed on to this instance.
|
11
|
+
#
|
12
|
+
# Params:
|
13
|
+
# channel:: The name of the channel to subscribe to
|
14
|
+
# queues:: An array of strings containing names of the queues to consume
|
15
|
+
#
|
16
|
+
# *Warning*:
|
17
|
+
# Chaining calls to subscribe will only work within blocks as this method
|
18
|
+
# is asynchronous; when it returns, it doesn't mean the subscription is active,
|
19
|
+
# only when the block is called.
|
20
|
+
def subscribe(channel, queues, exchange_opts = {}, queue_opts = {}, &callback)
|
21
|
+
return if !channel or channel.empty? or !queues
|
22
|
+
|
23
|
+
queues = [queues] if queues.is_a? String
|
24
|
+
|
25
|
+
if queues.empty?
|
26
|
+
callback.call if callback
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
Proc.new { |channel, queue, &block|
|
31
|
+
Station.instance.open_channel(channel, exchange_opts) { |c|
|
32
|
+
c.subscribe(self, queue, queue_opts)
|
33
|
+
block.call(c) if block
|
34
|
+
}
|
35
|
+
}.call(channel, queues.pop) { |c| subscribe(channel, queues) { callback.call if callback } }
|
36
|
+
end
|
37
|
+
|
38
|
+
def subscribe_exclusively(opts = {}, &callback)
|
39
|
+
return @private_channel if @private_channel
|
40
|
+
|
41
|
+
@private_channel = PrivateChannel.new
|
42
|
+
@private_channel.open do
|
43
|
+
@private_channel.consume(self, opts, &callback)
|
44
|
+
end
|
45
|
+
|
46
|
+
return @private_channel
|
47
|
+
end
|
48
|
+
|
49
|
+
def private_channel
|
50
|
+
@private_channel
|
51
|
+
end
|
52
|
+
|
53
|
+
def reply_to(original_message, payload = "")
|
54
|
+
publish(original_message.prepare_reply(payload), "")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Removes the subscription to a given queue in a channel.
|
58
|
+
def unsubscribe(channel, queue)
|
59
|
+
Station.instance.get_channel(channel) { |c| c.unsubscribe(self, queue) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Removes the subscription to all queues in a channel.
|
63
|
+
def unsubscribe(channel)
|
64
|
+
Station.instance.get_channel(channel) { |c| c.unsubscribe_all(self) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sends a message to a queue over a channel.
|
68
|
+
#
|
69
|
+
# If the queue isn't specified, it is taken from the message. This is used
|
70
|
+
# when acknowledging requests or sending responses. See Job::ack() for more info.
|
71
|
+
def publish(msg, channel, queue = nil, &callback)
|
72
|
+
queue ||= msg.queue
|
73
|
+
|
74
|
+
Station.instance.get_channel(channel) { |c|
|
75
|
+
c.publish(msg, queue, &callback)
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def publish_and_expect_reply(payload, channel, queue = nil, &callback)
|
80
|
+
m = Message.new(payload)
|
81
|
+
m.meta[:message_id] ||= BSON::ObjectId.new.to_s
|
82
|
+
m.meta[:headers]["queue_name"] = private_channel.name
|
83
|
+
|
84
|
+
publish(m, channel, queue, &callback)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Handler called whenever a message is received over one of the subscribed
|
88
|
+
# queues.
|
89
|
+
#
|
90
|
+
# Override this in your implementation.
|
91
|
+
def on_message_received(msg)
|
92
|
+
@callbacks.each { |c| c.call(msg) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def attach_message_handler(callback)
|
96
|
+
@callbacks ||= []
|
97
|
+
@callbacks << callback
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|