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.
@@ -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
@@ -0,0 +1,8 @@
1
+ module Dakwak
2
+ module Utility
3
+
4
+ class << self
5
+ end
6
+
7
+ end
8
+ end