gorgon 0.5.0.rc1 → 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/Gemfile.lock +2 -4
  2. data/gorgon.gemspec +0 -1
  3. data/lib/gorgon/amqp_service.rb +5 -5
  4. data/lib/gorgon/gem_command_handler.rb +2 -1
  5. data/lib/gorgon/listener.rb +7 -5
  6. data/lib/gorgon/originator_protocol.rb +1 -0
  7. data/lib/gorgon/version.rb +1 -1
  8. data/lib/gorgon/worker_manager.rb +5 -2
  9. data/lib/gorgon_amq-protocol/.gitignore +15 -0
  10. data/lib/gorgon_amq-protocol/.gitmodules +3 -0
  11. data/lib/gorgon_amq-protocol/.rspec +3 -0
  12. data/lib/gorgon_amq-protocol/.travis.yml +19 -0
  13. data/lib/gorgon_amq-protocol/lib/gorgon_amq/bit_set.rb +82 -0
  14. data/lib/gorgon_amq-protocol/lib/gorgon_amq/endianness.rb +15 -0
  15. data/lib/gorgon_amq-protocol/lib/gorgon_amq/int_allocator.rb +96 -0
  16. data/lib/gorgon_amq-protocol/lib/gorgon_amq/pack.rb +53 -0
  17. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol.rb +4 -0
  18. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/client.rb +2322 -0
  19. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/constants.rb +22 -0
  20. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/exceptions.rb +60 -0
  21. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/float_32bit.rb +14 -0
  22. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/frame.rb +210 -0
  23. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table.rb +142 -0
  24. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_decoder.rb +190 -0
  25. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_encoder.rb +123 -0
  26. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/type_constants.rb +26 -0
  27. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/version.rb +5 -0
  28. data/lib/gorgon_amq-protocol/lib/gorgon_amq/settings.rb +114 -0
  29. data/lib/gorgon_amq-protocol/lib/gorgon_amq/uri.rb +37 -0
  30. data/lib/gorgon_bunny/lib/gorgon_amq/protocol/extensions.rb +16 -0
  31. data/lib/gorgon_bunny/lib/gorgon_bunny.rb +89 -0
  32. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/credentials_encoder.rb +55 -0
  33. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/external_mechanism_encoder.rb +27 -0
  34. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/plain_mechanism_encoder.rb +19 -0
  35. data/lib/gorgon_bunny/lib/gorgon_bunny/channel.rb +1875 -0
  36. data/lib/gorgon_bunny/lib/gorgon_bunny/channel_id_allocator.rb +80 -0
  37. data/lib/gorgon_bunny/lib/gorgon_bunny/compatibility.rb +24 -0
  38. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/atomic_fixnum.rb +74 -0
  39. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/condition.rb +66 -0
  40. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/continuation_queue.rb +41 -0
  41. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/linked_continuation_queue.rb +61 -0
  42. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/synchronized_sorted_set.rb +56 -0
  43. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer.rb +123 -0
  44. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_tag_generator.rb +23 -0
  45. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_work_pool.rb +94 -0
  46. data/lib/gorgon_bunny/lib/gorgon_bunny/delivery_info.rb +93 -0
  47. data/lib/gorgon_bunny/lib/gorgon_bunny/exceptions.rb +236 -0
  48. data/lib/gorgon_bunny/lib/gorgon_bunny/exchange.rb +271 -0
  49. data/lib/gorgon_bunny/lib/gorgon_bunny/framing.rb +56 -0
  50. data/lib/gorgon_bunny/lib/gorgon_bunny/heartbeat_sender.rb +70 -0
  51. data/lib/gorgon_bunny/lib/gorgon_bunny/message_properties.rb +119 -0
  52. data/lib/gorgon_bunny/lib/gorgon_bunny/queue.rb +387 -0
  53. data/lib/gorgon_bunny/lib/gorgon_bunny/reader_loop.rb +116 -0
  54. data/lib/gorgon_bunny/lib/gorgon_bunny/return_info.rb +74 -0
  55. data/lib/gorgon_bunny/lib/gorgon_bunny/session.rb +1044 -0
  56. data/lib/gorgon_bunny/lib/gorgon_bunny/socket.rb +83 -0
  57. data/lib/gorgon_bunny/lib/gorgon_bunny/ssl_socket.rb +57 -0
  58. data/lib/gorgon_bunny/lib/gorgon_bunny/system_timer.rb +20 -0
  59. data/lib/gorgon_bunny/lib/gorgon_bunny/test_kit.rb +27 -0
  60. data/lib/gorgon_bunny/lib/gorgon_bunny/timeout.rb +18 -0
  61. data/lib/gorgon_bunny/lib/gorgon_bunny/transport.rb +398 -0
  62. data/lib/gorgon_bunny/lib/gorgon_bunny/version.rb +6 -0
  63. data/lib/gorgon_bunny/lib/gorgon_bunny/versioned_delivery_tag.rb +28 -0
  64. data/spec/crash_reporter_spec.rb +1 -1
  65. data/spec/gem_command_handler_spec.rb +2 -2
  66. data/spec/listener_spec.rb +5 -5
  67. data/spec/worker_manager_spec.rb +3 -3
  68. metadata +56 -17
@@ -0,0 +1,271 @@
1
+ require "gorgon_bunny/compatibility"
2
+
3
+ module GorgonBunny
4
+ # Represents AMQP 0.9.1 exchanges.
5
+ #
6
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
7
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
8
+ class Exchange
9
+
10
+ include GorgonBunny::Compatibility
11
+
12
+
13
+ #
14
+ # API
15
+ #
16
+
17
+ # @return [GorgonBunny::Channel]
18
+ attr_reader :channel
19
+
20
+ # @return [String]
21
+ attr_reader :name
22
+
23
+ # Type of this exchange (one of: :direct, :fanout, :topic, :headers).
24
+ # @return [Symbol]
25
+ attr_reader :type
26
+
27
+ # @return [Symbol]
28
+ # @api plugin
29
+ attr_reader :status
30
+
31
+ # Options hash this exchange instance was instantiated with
32
+ # @return [Hash]
33
+ attr_accessor :opts
34
+
35
+
36
+ # The default exchange. Default exchange is a direct exchange that is predefined.
37
+ # It cannot be removed. Every queue is bind to this (direct) exchange by default with
38
+ # the following routing semantics: messages will be routed to the queue withe same
39
+ # same name as message's routing key. In other words, if a message is published with
40
+ # a routing key of "weather.usa.ca.sandiego" and there is a queue Q with this name,
41
+ # that message will be routed to Q.
42
+ #
43
+ # @param [GorgonBunny::Channel] channel_or_connection Channel to use. {GorgonBunny::Session} instances
44
+ # are only supported for backwards compatibility.
45
+ #
46
+ # @example Publishing a messages to the tasks queue
47
+ # channel = GorgonBunny::Channel.new(connection)
48
+ # tasks_queue = channel.queue("tasks")
49
+ # GorgonBunny::Exchange.default(channel).publish("make clean", routing_key => "tasks")
50
+ #
51
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
52
+ # @see http://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.2.4)
53
+ # @note Do not confuse default exchange with amq.direct: amq.direct is a pre-defined direct
54
+ # exchange that doesn't have any special routing semantics.
55
+ # @return [Exchange] An instance that corresponds to the default exchange (of type direct).
56
+ # @api public
57
+ def self.default(channel_or_connection)
58
+ self.new(channel_from(channel_or_connection), :direct, GorgonAMQ::Protocol::EMPTY_STRING, :no_declare => true)
59
+ end
60
+
61
+ # @param [GorgonBunny::Channel] channel_or_connection Channel this exchange will use. {GorgonBunny::Session} instances are supported only for
62
+ # backwards compatibility with 0.8.
63
+ # @param [Symbol,String] type Exchange type
64
+ # @param [String] name Exchange name
65
+ # @param [Hash] opts Exchange properties
66
+ #
67
+ # @option opts [Boolean] :durable (false) Should this exchange be durable?
68
+ # @option opts [Boolean] :auto_delete (false) Should this exchange be automatically deleted when it is no longer used?
69
+ # @option opts [Boolean] :arguments ({}) Additional optional arguments (typically used by RabbitMQ extensions and plugins)
70
+ #
71
+ # @see GorgonBunny::Channel#topic
72
+ # @see GorgonBunny::Channel#fanout
73
+ # @see GorgonBunny::Channel#direct
74
+ # @see GorgonBunny::Channel#headers
75
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
76
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
77
+ # @api public
78
+ def initialize(channel_or_connection, type, name, opts = {})
79
+ # old GorgonBunny versions pass a connection here. In that case,
80
+ # we just use default channel from it. MK.
81
+ @channel = channel_from(channel_or_connection)
82
+ @name = name
83
+ @type = type
84
+ @options = self.class.add_default_options(name, opts)
85
+
86
+ @durable = @options[:durable]
87
+ @auto_delete = @options[:auto_delete]
88
+ @arguments = @options[:arguments]
89
+
90
+ declare! unless opts[:no_declare] || predeclared? || (@name == GorgonAMQ::Protocol::EMPTY_STRING)
91
+
92
+ @channel.register_exchange(self)
93
+ end
94
+
95
+ # @return [Boolean] true if this exchange was declared as durable (will survive broker restart).
96
+ # @api public
97
+ def durable?
98
+ @durable
99
+ end # durable?
100
+
101
+ # @return [Boolean] true if this exchange was declared as automatically deleted (deleted as soon as last consumer unbinds).
102
+ # @api public
103
+ def auto_delete?
104
+ @auto_delete
105
+ end # auto_delete?
106
+
107
+ # @return [Hash] Additional optional arguments (typically used by RabbitMQ extensions and plugins)
108
+ # @api public
109
+ def arguments
110
+ @arguments
111
+ end
112
+
113
+
114
+ # Publishes a message
115
+ #
116
+ # @param [String] payload Message payload. It will never be modified by GorgonBunny or RabbitMQ in any way.
117
+ # @param [Hash] opts Message properties (metadata) and delivery settings
118
+ #
119
+ # @option opts [String] :routing_key Routing key
120
+ # @option opts [Boolean] :persistent Should the message be persisted to disk?
121
+ # @option opts [Boolean] :mandatory Should the message be returned if it cannot be routed to any queue?
122
+ # @option opts [Integer] :timestamp A timestamp associated with this message
123
+ # @option opts [Integer] :expiration Expiration time after which the message will be deleted
124
+ # @option opts [String] :type Message type, e.g. what type of event or command this message represents. Can be any string
125
+ # @option opts [String] :reply_to Queue name other apps should send the response to
126
+ # @option opts [String] :content_type Message content type (e.g. application/json)
127
+ # @option opts [String] :content_encoding Message content encoding (e.g. gzip)
128
+ # @option opts [String] :correlation_id Message correlated to this one, e.g. what request this message is a reply for
129
+ # @option opts [Integer] :priority Message priority, 0 to 9. Not used by RabbitMQ, only applications
130
+ # @option opts [String] :message_id Any message identifier
131
+ # @option opts [String] :user_id Optional user ID. Verified by RabbitMQ against the actual connection username
132
+ # @option opts [String] :app_id Optional application ID
133
+ #
134
+ # @return [GorgonBunny::Exchange] Self
135
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
136
+ # @api public
137
+ def publish(payload, opts = {})
138
+ @channel.basic_publish(payload, self.name, (opts.delete(:routing_key) || opts.delete(:key)), opts)
139
+
140
+ self
141
+ end
142
+
143
+
144
+ # Deletes the exchange unless it is predeclared
145
+ #
146
+ # @param [Hash] opts Options
147
+ #
148
+ # @option opts [Boolean] if_unused (false) Should this exchange be deleted only if it is no longer used
149
+ #
150
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
151
+ # @api public
152
+ def delete(opts = {})
153
+ @channel.deregister_exchange(self)
154
+ @channel.exchange_delete(@name, opts) unless predeclared?
155
+ end
156
+
157
+ # Binds an exchange to another (source) exchange using exchange.bind AMQP 0.9.1 extension
158
+ # that RabbitMQ provides.
159
+ #
160
+ # @param [String] source Source exchange name
161
+ # @param [Hash] opts Options
162
+ #
163
+ # @option opts [String] routing_key (nil) Routing key used for binding
164
+ # @option opts [Hash] arguments ({}) Optional arguments
165
+ #
166
+ # @return [GorgonBunny::Exchange] Self
167
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
168
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
169
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
170
+ # @api public
171
+ def bind(source, opts = {})
172
+ @channel.exchange_bind(source, self, opts)
173
+
174
+ self
175
+ end
176
+
177
+ # Unbinds an exchange from another (source) exchange using exchange.unbind AMQP 0.9.1 extension
178
+ # that RabbitMQ provides.
179
+ #
180
+ # @param [String] source Source exchange name
181
+ # @param [Hash] opts Options
182
+ #
183
+ # @option opts [String] routing_key (nil) Routing key used for binding
184
+ # @option opts [Hash] arguments ({}) Optional arguments
185
+ #
186
+ # @return [GorgonBunny::Exchange] Self
187
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
188
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
189
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
190
+ # @api public
191
+ def unbind(source, opts = {})
192
+ @channel.exchange_unbind(source, self, opts)
193
+
194
+ self
195
+ end
196
+
197
+ # Defines a block that will handle returned messages
198
+ # @see http://rubybunny.info/articles/exchanges.html
199
+ # @api public
200
+ def on_return(&block)
201
+ @on_return = block
202
+
203
+ self
204
+ end
205
+
206
+ # Waits until all outstanding publisher confirms on the channel
207
+ # arrive.
208
+ #
209
+ # This is a convenience method that delegates to {GorgonBunny::Channel#wait_for_confirms}
210
+ #
211
+ # @api public
212
+ def wait_for_confirms
213
+ @channel.wait_for_confirms
214
+ end
215
+
216
+ # @private
217
+ def recover_from_network_failure
218
+ # puts "Recovering exchange #{@name} from network failure"
219
+ declare! unless predefined?
220
+ end
221
+
222
+
223
+ #
224
+ # Implementation
225
+ #
226
+
227
+ # @private
228
+ def handle_return(basic_return, properties, content)
229
+ if @on_return
230
+ @on_return.call(basic_return, properties, content)
231
+ else
232
+ # TODO: log a warning
233
+ end
234
+ end
235
+
236
+ # @return [Boolean] true if this exchange is a pre-defined one (amq.direct, amq.fanout, amq.match and so on)
237
+ def predefined?
238
+ (@name == GorgonAMQ::Protocol::EMPTY_STRING) || !!(@name =~ /^amq\.(direct|fanout|topic|headers|match)/i)
239
+ end # predefined?
240
+ alias predeclared? predefined?
241
+
242
+ protected
243
+
244
+ # @private
245
+ def declare!
246
+ @channel.exchange_declare(@name, @type, @options)
247
+ end
248
+
249
+ # @private
250
+ def self.add_default_options(name, opts, block)
251
+ { :exchange => name, :nowait => (block.nil? && !name.empty?) }.merge(opts)
252
+ end
253
+
254
+ # @private
255
+ def self.add_default_options(name, opts)
256
+ # :nowait is always false for GorgonBunny
257
+ h = { :queue => name, :nowait => false }.merge(opts)
258
+
259
+ if name.empty?
260
+ {
261
+ :passive => false,
262
+ :durable => false,
263
+ :auto_delete => false,
264
+ :arguments => nil
265
+ }.merge(h)
266
+ else
267
+ h
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,56 @@
1
+ module GorgonBunny
2
+ # @private
3
+ module Framing
4
+ ENCODINGS_SUPPORTED = defined? Encoding
5
+ HEADER_SLICE = (0..6).freeze
6
+ DATA_SLICE = (7..-1).freeze
7
+ PAYLOAD_SLICE = (0..-2).freeze
8
+
9
+ # @private
10
+ module String
11
+ class Frame < GorgonAMQ::Protocol::Frame
12
+ def self.decode(string)
13
+ header = string[HEADER_SLICE]
14
+ type, channel, size = self.decode_header(header)
15
+ data = string[DATA_SLICE]
16
+ payload = data[PAYLOAD_SLICE]
17
+ frame_end = data[-1, 1]
18
+
19
+ frame_end.force_encoding(GorgonAMQ::Protocol::Frame::FINAL_OCTET.encoding) if ENCODINGS_SUPPORTED
20
+
21
+ # 1) the size is miscalculated
22
+ if payload.bytesize != size
23
+ raise BadLengthError.new(size, payload.bytesize)
24
+ end
25
+
26
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
27
+ raise NoFinalOctetError.new if frame_end != GorgonAMQ::Protocol::Frame::FINAL_OCTET
28
+
29
+ self.new(type, payload, channel)
30
+ end
31
+ end
32
+ end # String
33
+
34
+
35
+ # @private
36
+ module IO
37
+ class Frame < GorgonAMQ::Protocol::Frame
38
+ def self.decode(io)
39
+ header = io.read(7)
40
+ type, channel, size = self.decode_header(header)
41
+ data = io.read_fully(size + 1)
42
+ payload, frame_end = data[PAYLOAD_SLICE], data[-1, 1]
43
+
44
+ # 1) the size is miscalculated
45
+ if payload.bytesize != size
46
+ raise BadLengthError.new(size, payload.bytesize)
47
+ end
48
+
49
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
50
+ raise NoFinalOctetError.new if frame_end != GorgonAMQ::Protocol::Frame::FINAL_OCTET
51
+ self.new(type, payload, channel)
52
+ end # self.from
53
+ end # Frame
54
+ end # IO
55
+ end # Framing
56
+ end # GorgonBunny
@@ -0,0 +1,70 @@
1
+ require "thread"
2
+ require "gorgon_amq/protocol/client"
3
+ require "gorgon_amq/protocol/frame"
4
+
5
+ module GorgonBunny
6
+ # Periodically sends heartbeats, keeping track of the last publishing activity.
7
+ #
8
+ # @private
9
+ class HeartbeatSender
10
+
11
+ #
12
+ # API
13
+ #
14
+
15
+ def initialize(transport, logger)
16
+ @transport = transport
17
+ @logger = logger
18
+ @mutex = Monitor.new
19
+
20
+ @last_activity_time = Time.now
21
+ end
22
+
23
+ def start(period = 30)
24
+ @mutex.synchronize do
25
+ # calculate interval as half the given period plus
26
+ # some compensation for Ruby's implementation inaccuracy
27
+ # (we cannot get at the nanos level the Java client uses, and
28
+ # our approach is simplistic). MK.
29
+ @interval = [(period / 2) - 1, 0.4].max
30
+
31
+ @thread = Thread.new(&method(:run))
32
+ end
33
+ end
34
+
35
+ def stop
36
+ @mutex.synchronize { @thread.exit }
37
+ end
38
+
39
+ def signal_activity!
40
+ @last_activity_time = Time.now
41
+ end
42
+
43
+ protected
44
+
45
+ def run
46
+ begin
47
+ loop do
48
+ self.beat
49
+
50
+ sleep @interval
51
+ end
52
+ rescue IOError => ioe
53
+ @logger.error "I/O error in the hearbeat sender: #{ioe.message}"
54
+ stop
55
+ rescue Exception => e
56
+ @logger.error "Error in the hearbeat sender: #{e.message}"
57
+ stop
58
+ end
59
+ end
60
+
61
+ def beat
62
+ now = Time.now
63
+
64
+ if now > (@last_activity_time + @interval)
65
+ @logger.debug "Sending a heartbeat, last activity time: #{@last_activity_time}, interval (s): #{@interval}"
66
+ @transport.write_without_timeout(GorgonAMQ::Protocol::HeartbeatFrame.encode)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,119 @@
1
+ module GorgonBunny
2
+ # Wraps basic properties hash as returned by amq-protocol to
3
+ # provide access to the delivery properties as immutable hash as
4
+ # well as methods.
5
+ class MessageProperties
6
+
7
+ #
8
+ # Behaviors
9
+ #
10
+
11
+ include Enumerable
12
+
13
+ #
14
+ # API
15
+ #
16
+
17
+ # @private
18
+ def initialize(properties)
19
+ @properties = properties
20
+ end
21
+
22
+ # Iterates over the message properties
23
+ # @see Enumerable#each
24
+ def each(*args, &block)
25
+ @properties.each(*args, &block)
26
+ end
27
+
28
+ # Accesses message properties by key
29
+ # @see Hash#[]
30
+ def [](k)
31
+ @properties[k]
32
+ end
33
+
34
+ # @return [Hash] Hash representation of this delivery info
35
+ def to_hash
36
+ @properties
37
+ end
38
+
39
+ # @private
40
+ def to_s
41
+ to_hash.to_s
42
+ end
43
+
44
+ # @private
45
+ def inspect
46
+ to_hash.inspect
47
+ end
48
+
49
+ # @return [String] (Optional) content type of the message, as set by the publisher
50
+ def content_type
51
+ @properties[:content_type]
52
+ end
53
+
54
+ # @return [String] (Optional) content encoding of the message, as set by the publisher
55
+ def content_encoding
56
+ @properties[:content_encoding]
57
+ end
58
+
59
+ # @return [String] Message headers
60
+ def headers
61
+ @properties[:headers]
62
+ end
63
+
64
+ # @return [Integer] Delivery mode (persistent or transient)
65
+ def delivery_mode
66
+ @properties[:delivery_mode]
67
+ end
68
+
69
+ # @return [Integer] Message priority, as set by the publisher
70
+ def priority
71
+ @properties[:priority]
72
+ end
73
+
74
+ # @return [String] What message this message is a reply to (or corresponds to), as set by the publisher
75
+ def correlation_id
76
+ @properties[:correlation_id]
77
+ end
78
+
79
+ # @return [String] (Optional) How to reply to the publisher (usually a reply queue name)
80
+ def reply_to
81
+ @properties[:reply_to]
82
+ end
83
+
84
+ # @return [String] Message expiration, as set by the publisher
85
+ def expiration
86
+ @properties[:expiration]
87
+ end
88
+
89
+ # @return [String] Message ID, as set by the publisher
90
+ def message_id
91
+ @properties[:message_id]
92
+ end
93
+
94
+ # @return [Time] Message timestamp, as set by the publisher
95
+ def timestamp
96
+ @properties[:timestamp]
97
+ end
98
+
99
+ # @return [String] Message type, as set by the publisher
100
+ def type
101
+ @properties[:type]
102
+ end
103
+
104
+ # @return [String] Publishing user, as set by the publisher
105
+ def user_id
106
+ @properties[:user_id]
107
+ end
108
+
109
+ # @return [String] Publishing application, as set by the publisher
110
+ def app_id
111
+ @properties[:app_id]
112
+ end
113
+
114
+ # @return [String] Cluster ID, as set by the publisher
115
+ def cluster_id
116
+ @properties[:cluster_id]
117
+ end
118
+ end
119
+ end