dakwak 1.0.3 → 1.6.5

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