dakwak 1.0.3 → 1.6.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|