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
@@ -0,0 +1,68 @@
|
|
1
|
+
module Dakwak
|
2
|
+
|
3
|
+
# Messages are the AMQP entities exchanged by communicators. All messages
|
4
|
+
# are sent through queues bound to certain channels. To exchange messages,
|
5
|
+
# see Dakwak::Communicator.
|
6
|
+
#
|
7
|
+
# Messages contain a payload (which can be empty) and might contain the following properties:
|
8
|
+
#
|
9
|
+
# * meta.content_type
|
10
|
+
# * meta.content_encoding
|
11
|
+
# * meta.priority
|
12
|
+
# * meta.type
|
13
|
+
# * meta.delivery_mode
|
14
|
+
# * meta.app_id
|
15
|
+
# * meta.message_id
|
16
|
+
# * meta.user_id
|
17
|
+
# * meta.correlation_id
|
18
|
+
# * meta.headers
|
19
|
+
#
|
20
|
+
# For a full reference, consult with the amqp-client Ruby gem documentation here:
|
21
|
+
# http://rdoc.info/github/ruby-amqp/amqp/master/frames
|
22
|
+
#
|
23
|
+
# It is also helpful to review the supported message properties by libdakwak at:
|
24
|
+
# https://github.com/dakwak/libdakwak/blob/master/include/dakwak/messaging/message.hpp
|
25
|
+
#
|
26
|
+
# *Warning*:
|
27
|
+
# When a property was not set by the sender, it evaluates to nil and NOT to an empty
|
28
|
+
# string or so.
|
29
|
+
class Message
|
30
|
+
attr_reader :payload, :meta, :queue
|
31
|
+
|
32
|
+
# Creates a new message with the given payload and meta attributes.
|
33
|
+
def initialize(payload, meta = {}, queue = "")
|
34
|
+
@payload, @meta, @queue = payload, meta, queue
|
35
|
+
@meta[:headers] ||= {} if @meta.class == Hash
|
36
|
+
end
|
37
|
+
|
38
|
+
# If the message_id property was set by the sender, then a reply is expected.
|
39
|
+
#
|
40
|
+
# The address to send back to will be specified in this message's meta.reply_to,
|
41
|
+
# and the meta.message_id should be used as the reply's meta.correlation_id
|
42
|
+
# so the recepient can route the reply.
|
43
|
+
def expects_reply?()
|
44
|
+
!@meta.message_id.nil? && !@meta.headers["queue_name"].nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Builds a message with the proper headers and attributes to mark it
|
48
|
+
# as a reply to the current one.
|
49
|
+
#
|
50
|
+
# You can use the returned message, set its payload if needed, to publish the reply.
|
51
|
+
def prepare_reply(payload = "")
|
52
|
+
Message.new(payload, { :reply_to => @meta.app_id, :correlation_id => @meta.message_id },
|
53
|
+
@meta.headers["queue_name"])
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
"Sender: '#{@meta.app_id}@#{(@meta.headers || {})["queue_name"]}' -- #{@payload[0..30]}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def dump
|
61
|
+
str = ""
|
62
|
+
@meta.attributes.each_pair { |k,v| str << "#{k} => #{v}\n" }
|
63
|
+
str
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,143 @@
|
|
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 PrivateChannel
|
16
|
+
include Logger
|
17
|
+
|
18
|
+
attr_reader :session, :channel, :exchange
|
19
|
+
|
20
|
+
def opened?
|
21
|
+
@session && @session.connected?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Initiates connection with the AMQP broker using connection info
|
25
|
+
# stored in the Dakwak::Station and binds to a direct exchange
|
26
|
+
# identified by the name of this channel.
|
27
|
+
#
|
28
|
+
# Params:
|
29
|
+
# &on_open::
|
30
|
+
# A proc that will be called if and when the connection
|
31
|
+
# with the broker succeeds and the exchange is bound.
|
32
|
+
#
|
33
|
+
# *Note*:
|
34
|
+
# This is an asynchronous method.
|
35
|
+
def open(&on_open)
|
36
|
+
if opened?
|
37
|
+
on_open.call(self) if on_open
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
AMQP.connect(Station.instance.connection_options) do |session|
|
42
|
+
@session = session
|
43
|
+
AMQP::Channel.new(@session, :auto_recovery => true) do |channel|
|
44
|
+
@channel = channel
|
45
|
+
@exchange = @channel.default_exchange
|
46
|
+
on_open.call(self) if on_open
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Terminates the connection with the AMQP broker and destroys
|
52
|
+
# all queue consumers.
|
53
|
+
#
|
54
|
+
# Attempting to close an already closed channel will log a warning
|
55
|
+
# and reject the request. However, if a block was passed, it will
|
56
|
+
# still be called.
|
57
|
+
#
|
58
|
+
# *Note*:
|
59
|
+
# This is an asynchronous method.
|
60
|
+
def close(&on_close)
|
61
|
+
unless opened?
|
62
|
+
log_warn "Attempting to close a closed channel."
|
63
|
+
on_close.call(self) if on_close
|
64
|
+
return nil
|
65
|
+
end
|
66
|
+
|
67
|
+
EventMachine.next_tick do
|
68
|
+
@session.close {
|
69
|
+
@queue = nil
|
70
|
+
@session = nil
|
71
|
+
|
72
|
+
on_close.call(self) if on_close
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def name()
|
80
|
+
@queue.name
|
81
|
+
end
|
82
|
+
|
83
|
+
# Binds the specified queue and consumes any messages routed to it.
|
84
|
+
#
|
85
|
+
# *Note*:
|
86
|
+
# There's little point in consuming a queue with no subscribed Dakwak::Communicator
|
87
|
+
# instances to it. To pull messages from a queue, use Communicator::subscribe.
|
88
|
+
#
|
89
|
+
# Params:
|
90
|
+
# routing_key::
|
91
|
+
# The AMQP routing key to use when binding the new queue to the exchange.
|
92
|
+
# opts::
|
93
|
+
# - :exclusive
|
94
|
+
# The queue consumption will be exclusive to this consumer.
|
95
|
+
# - :nowait
|
96
|
+
# Does not wait for the broker to reply. If a binding error
|
97
|
+
# occurs, a Channel exception will be thrown.
|
98
|
+
def consume(comm, opts = {})
|
99
|
+
return if consuming?
|
100
|
+
|
101
|
+
@comlink = comm
|
102
|
+
|
103
|
+
# assign some defaults
|
104
|
+
opts = {
|
105
|
+
exclusive: true,
|
106
|
+
durable: false,
|
107
|
+
passive: false,
|
108
|
+
auto_delete: true
|
109
|
+
}.merge(opts)
|
110
|
+
|
111
|
+
@channel.queue("", opts) do |queue, declare_ok|
|
112
|
+
# TODO: handle declaration failure
|
113
|
+
|
114
|
+
@queue = queue
|
115
|
+
|
116
|
+
yield if block_given?
|
117
|
+
|
118
|
+
# log_info "Consuming #{@queue.name}"
|
119
|
+
@queue.subscribe { |meta, payload|
|
120
|
+
msg = Message.new(payload, meta)
|
121
|
+
# log_info "Dispatching message #{msg}"
|
122
|
+
@comlink.on_message_received(msg)
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Is the queue being consumed?
|
128
|
+
def consuming?
|
129
|
+
return !@queue.nil?
|
130
|
+
end
|
131
|
+
|
132
|
+
# Creates a new channel with name as the AMQP exchange name. Do not create
|
133
|
+
# channel objects directly, use Dakwak::Station instead.
|
134
|
+
def initialize()
|
135
|
+
logging_context("PrivateChannel")
|
136
|
+
|
137
|
+
@channel, @exchange, @queue, @comlink = nil, nil, nil, nil
|
138
|
+
super()
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'dakwak/logger'
|
2
|
+
require 'dakwak/configurator'
|
3
|
+
require 'dakwak/messaging/channel'
|
4
|
+
|
5
|
+
module Dakwak
|
6
|
+
|
7
|
+
# The station is the interface for opening and closing communication channels.
|
8
|
+
class Station
|
9
|
+
include Logger
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def instance
|
13
|
+
Dakwak.station
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :channels
|
18
|
+
|
19
|
+
# Opens a channel identified by 'name' if it's not already open.
|
20
|
+
#
|
21
|
+
# This method is asynchronous.
|
22
|
+
#
|
23
|
+
# Aliases: get_channel
|
24
|
+
def open_channel(name, opts = {}, &callback) # :yields: channel
|
25
|
+
|
26
|
+
if shutting_down?
|
27
|
+
log_warn "Station is shutting down, channels will not be opened."
|
28
|
+
return nil
|
29
|
+
end
|
30
|
+
|
31
|
+
if @channels[name]
|
32
|
+
callback.call(@channels[name]) if callback
|
33
|
+
return @channels[name]
|
34
|
+
end
|
35
|
+
|
36
|
+
c = Channel.new(name)
|
37
|
+
@channels[name] = c
|
38
|
+
c.open(opts) { |c|
|
39
|
+
state(:active)
|
40
|
+
|
41
|
+
log_debug "Channel #{c.name} is now open"
|
42
|
+
callback.call(c) if callback
|
43
|
+
}
|
44
|
+
|
45
|
+
return c
|
46
|
+
end
|
47
|
+
|
48
|
+
alias :get_channel :open_channel
|
49
|
+
|
50
|
+
# Turns off the station and all open channels. A callback must be provided
|
51
|
+
# that will be called when the station is shut down.
|
52
|
+
def shutdown(&callback)
|
53
|
+
raise RuntimeError.new "Station::shutdown() must be called with a callback block" unless callback
|
54
|
+
|
55
|
+
if inactive?
|
56
|
+
log_warn "Rejecting shutdown request; the station is currently inactive."
|
57
|
+
callback.call
|
58
|
+
return nil
|
59
|
+
end
|
60
|
+
|
61
|
+
if shutting_down?
|
62
|
+
log_warn "Rejecting shutdown request; the station is already shutting down!"
|
63
|
+
callback.call
|
64
|
+
return nil
|
65
|
+
end
|
66
|
+
|
67
|
+
state(:shutting_down)
|
68
|
+
|
69
|
+
# register the callback
|
70
|
+
@on_shutdown = callback
|
71
|
+
|
72
|
+
log_info "Closing #{@channels.size} open channels..."
|
73
|
+
|
74
|
+
channels_closed = 0
|
75
|
+
@channels.each_pair { |name, c|
|
76
|
+
c.close {
|
77
|
+
channels_closed += 1
|
78
|
+
if channels_closed == @channels.size then
|
79
|
+
state(:inactive)
|
80
|
+
|
81
|
+
log_info "All channels are cleanly closed. Shutdown complete."
|
82
|
+
@on_shutdown.call
|
83
|
+
end
|
84
|
+
}
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# The station is active if it has any channels currently open.
|
89
|
+
def active?
|
90
|
+
@state == :active
|
91
|
+
end
|
92
|
+
|
93
|
+
# The station is inactive if there are no open channels.
|
94
|
+
def inactive?
|
95
|
+
@state == :inactive
|
96
|
+
end
|
97
|
+
|
98
|
+
# When shutting down, it is not permitted to attempt to open any channels.
|
99
|
+
def shutting_down?
|
100
|
+
@state == :shutting_down
|
101
|
+
end
|
102
|
+
|
103
|
+
def connection_options()
|
104
|
+
@config
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_option(k, v)
|
108
|
+
@config[k.to_sym] = v
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialize
|
112
|
+
logging_context( "Station" )
|
113
|
+
|
114
|
+
@channels = {}
|
115
|
+
@config = {
|
116
|
+
:host => "127.0.0.1",
|
117
|
+
:port => 5672,
|
118
|
+
:user => "guest",
|
119
|
+
:pass => "guest",
|
120
|
+
:vhost => "/",
|
121
|
+
:ssl => false,
|
122
|
+
:frame_max => 131072
|
123
|
+
}
|
124
|
+
|
125
|
+
state(:inactive)
|
126
|
+
|
127
|
+
Configurator.subscribe("Station", self)
|
128
|
+
|
129
|
+
# log_info "engaged"
|
130
|
+
|
131
|
+
super()
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def state(in_state)
|
137
|
+
@state = in_state
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
gem 'mongo'
|
2
|
+
|
3
|
+
require 'mongo'
|
4
|
+
|
5
|
+
module Dakwak
|
6
|
+
class MongoAdapter < Configurable
|
7
|
+
include Logger
|
8
|
+
|
9
|
+
attr_reader :dba, :dbh
|
10
|
+
|
11
|
+
def initialize(id = "MongoDB")
|
12
|
+
logging_context(id)
|
13
|
+
@cfg = { host: "localhost", port: 27017, db: "test" }
|
14
|
+
Configurator.subscribe(id, self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def connect()
|
18
|
+
rescue_connection_failure do
|
19
|
+
if @cfg[:mode] == "single" then
|
20
|
+
conn_info = "#{@cfg[:single][:host]}:#{@cfg[:single][:port]}"
|
21
|
+
log_info "Connecting to MongoDB in Single mode to #{conn_info}"
|
22
|
+
@dba = Mongo::Client.new([@cfg[:single][:host]+":"+@cfg[:single][:port].to_s], ssl: @cfg[:ssl][:enable], ssl_cert: @cfg[:ssl][:ssl_cert], ssl_key: @cfg[:ssl][:ssl_key], ssl_verify: @cfg[:ssl][:ssl_verify], connect: :direct, :database => @cfg[:database])
|
23
|
+
else
|
24
|
+
conn_info = "#{@cfg[:rs][:seeds].inspect}, set: #{@cfg[:rs][:set]}"
|
25
|
+
log_info "Connecting to MongoDB in RS mode to #{conn_info}"
|
26
|
+
@dba = Mongo::Client.new([@cfg[:rs][:seeds]], ssl: @cfg[:ssl][:enable], ssl_cert: @cfg[:ssl][:ssl_cert], ssl_key: @cfg[:ssl][:ssl_key], ssl_verify: @cfg[:ssl][:ssl_verify], connect: :replica_set, :database => @cfg[:database] || "dakwak_production")
|
27
|
+
end
|
28
|
+
@dbh = @dba
|
29
|
+
#@dbh = @dba.db(@cfg[:database] || "dakwak_production")
|
30
|
+
end
|
31
|
+
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def disconnect()
|
36
|
+
@dba.close
|
37
|
+
@dba = nil
|
38
|
+
|
39
|
+
log_info "disconnected"
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def find(collection, cnd)
|
44
|
+
rescue_connection_failure do
|
45
|
+
entries = @dbh[collection].find(cnd).sort(:created_at => Mongo::Index::DESCENDING).entries
|
46
|
+
return entries if entries.length > 0
|
47
|
+
end
|
48
|
+
|
49
|
+
return []
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_one(collection, cnd)
|
53
|
+
rescue_connection_failure do
|
54
|
+
return @dbh[collection].find(cnd).sort(:created_at => Mongo::Index::DESCENDING).first
|
55
|
+
end
|
56
|
+
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_or_create(collection, args, creation_args = {})
|
61
|
+
rescue_connection_failure do
|
62
|
+
doc = find_one(collection, args)
|
63
|
+
if !doc
|
64
|
+
doc = create!(collection, args.merge(creation_args))
|
65
|
+
end
|
66
|
+
return doc
|
67
|
+
end
|
68
|
+
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
def create(collection, args)
|
73
|
+
rescue_connection_failure do
|
74
|
+
args.delete("_id")
|
75
|
+
args.merge!({:created_at => Time.now.utc, :updated_at => Time.now.utc })
|
76
|
+
id = @dbh[collection].insert_one(args)
|
77
|
+
doc = find_one(collection,:_id => id.inserted_ids[0])
|
78
|
+
return doc
|
79
|
+
end
|
80
|
+
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
|
84
|
+
def create!(collection, args)
|
85
|
+
rescue_connection_failure do
|
86
|
+
args.delete("_id")
|
87
|
+
args.merge!({:created_at => Time.now.utc, :updated_at => Time.now.utc })
|
88
|
+
id = @dbh[collection].insert_one(args)
|
89
|
+
doc = find_one(collection,:_id => id.inserted_ids[0])
|
90
|
+
return doc
|
91
|
+
end
|
92
|
+
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
|
96
|
+
def update(collection, record, args)
|
97
|
+
rescue_connection_failure do
|
98
|
+
# args.merge!({ :updated_at => Time.now.utc })
|
99
|
+
id = @dbh[collection].update_many({ "_id" => record['_id'] }, args)
|
100
|
+
doc = find(collection, "_id" => record['_id'])
|
101
|
+
end
|
102
|
+
|
103
|
+
return false
|
104
|
+
end
|
105
|
+
|
106
|
+
def update_where(collection, conds, args)
|
107
|
+
rescue_connection_failure do
|
108
|
+
return update(collection, conds ,args)
|
109
|
+
end
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def update_all(collection, find_args, update_args)
|
115
|
+
rescue_connection_failure do
|
116
|
+
return @dbh[collection].update(find_args, update_args)
|
117
|
+
end
|
118
|
+
|
119
|
+
return false
|
120
|
+
end
|
121
|
+
|
122
|
+
def remove(collection, args)
|
123
|
+
@dbh[collection].remove(args)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Ensure retry upon failure
|
127
|
+
# Shamelessly stolen from http://api.mongodb.org/ruby/current/file.REPLICA_SETS.html
|
128
|
+
def rescue_connection_failure(max_retries=10)
|
129
|
+
retries = 0
|
130
|
+
begin
|
131
|
+
yield
|
132
|
+
rescue Mongo::Error::NoServerAvailable => ex
|
133
|
+
log_warn "connection to MongoDB was lost, retrying for the #{retries} time"
|
134
|
+
retries += 1
|
135
|
+
raise ex if retries > max_retries
|
136
|
+
sleep(0.5)
|
137
|
+
retry
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|