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