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 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