stapfen 2.2.0-java
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/.gitignore +19 -0
- data/CHANGES.md +16 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +106 -0
- data/Rakefile +10 -0
- data/examples/simple.rb +44 -0
- data/lib/stapfen.rb +25 -0
- data/lib/stapfen/client.rb +8 -0
- data/lib/stapfen/client/jms.rb +118 -0
- data/lib/stapfen/client/kafka.rb +76 -0
- data/lib/stapfen/client/stomp.rb +35 -0
- data/lib/stapfen/destination.rb +59 -0
- data/lib/stapfen/logger.rb +48 -0
- data/lib/stapfen/message.rb +58 -0
- data/lib/stapfen/version.rb +3 -0
- data/lib/stapfen/worker.rb +265 -0
- data/spec/client/jms_spec.rb +199 -0
- data/spec/client/kafka_spec.rb +54 -0
- data/spec/client/stomp_spec.rb +5 -0
- data/spec/client_spec.rb +5 -0
- data/spec/destination_spec.rb +71 -0
- data/spec/logger_spec.rb +41 -0
- data/spec/message_spec.rb +96 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/worker_spec.rb +275 -0
- data/stapfen.gemspec +24 -0
- metadata +93 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'stomp'
|
2
|
+
|
3
|
+
module Stapfen
|
4
|
+
module Client
|
5
|
+
class Stomp < ::Stomp::Client
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
# Perform a deep-copy of the configuration since +Stomp::Client+ will
|
9
|
+
# mutate/mess up the configuration +Hash+ passed in here, see:
|
10
|
+
# <https://github.com/stompgem/stomp/issues/80>
|
11
|
+
super(Marshal.load(Marshal.dump(config)))
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect(*args)
|
15
|
+
# No-op, since Stomp::Client will connect on instantiation
|
16
|
+
end
|
17
|
+
|
18
|
+
def can_unreceive?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def runloop
|
23
|
+
# Performing this join/runningloop to make sure that we don't
|
24
|
+
# experience potential deadlocks between signal handlers who might
|
25
|
+
# close the connection, and an infinite Client#join call
|
26
|
+
#
|
27
|
+
# Instead of using client#open? we use #running which will still be
|
28
|
+
# true even if the client is currently in an exponential reconnect loop
|
29
|
+
while self.running do
|
30
|
+
self.join(1)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Stapfen
|
2
|
+
class Destination
|
3
|
+
attr_accessor :name, :type
|
4
|
+
|
5
|
+
def queue?
|
6
|
+
@type == :queue
|
7
|
+
end
|
8
|
+
|
9
|
+
def topic?
|
10
|
+
@type == :topic
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_stomp
|
14
|
+
if queue?
|
15
|
+
return "/queue/#{@name}"
|
16
|
+
end
|
17
|
+
|
18
|
+
if topic?
|
19
|
+
return "/topic/#{@name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def as_jms
|
24
|
+
if queue?
|
25
|
+
return "queue://#{@name}"
|
26
|
+
end
|
27
|
+
|
28
|
+
if topic?
|
29
|
+
return "topic://#{@name}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def jms_opts
|
34
|
+
if queue?
|
35
|
+
return {:queue_name => name}
|
36
|
+
end
|
37
|
+
|
38
|
+
if topic?
|
39
|
+
return {:topic_name => name}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def as_kafka
|
44
|
+
return name
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a {Stapfen::Destination} from the given string
|
48
|
+
#
|
49
|
+
# @param [String] name
|
50
|
+
# @return [Stapfen::Destination]
|
51
|
+
def self.from_string(name)
|
52
|
+
destination = self.new
|
53
|
+
pieces = name.split('/')
|
54
|
+
destination.type = pieces[1].to_sym
|
55
|
+
destination.name = pieces[2 .. -1].join('/')
|
56
|
+
return destination
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
module Stapfen
|
3
|
+
# Logging module to ensure that {{Stapfen::Worker}} classes can perform
|
4
|
+
# logging if they've been configured to
|
5
|
+
module Logger
|
6
|
+
# Collection of methods to pass arguments through from the class and
|
7
|
+
# instance level to a configured logger
|
8
|
+
PROXY_METHODS = [:info, :debug, :warn, :error].freeze
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
PROXY_METHODS.each do |method|
|
12
|
+
define_method(method) do |*args|
|
13
|
+
proxy_log_method(method, args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def proxy_log_method(method, arguments)
|
20
|
+
if self.logger
|
21
|
+
self.logger.call.send(method, *arguments)
|
22
|
+
return true
|
23
|
+
end
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.included(klass)
|
29
|
+
klass.extend(ClassMethods)
|
30
|
+
end
|
31
|
+
|
32
|
+
PROXY_METHODS.each do |method|
|
33
|
+
define_method(method) do |*args|
|
34
|
+
proxy_log_method(method, args)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def proxy_log_method(method, arguments)
|
41
|
+
if self.class.logger
|
42
|
+
self.class.logger.call.send(method, *arguments)
|
43
|
+
return true
|
44
|
+
end
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Stapfen
|
2
|
+
class Message
|
3
|
+
attr_reader :message_id, :body, :original, :destination
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
super()
|
7
|
+
@body = opts[:body]
|
8
|
+
@destination = opts[:destination]
|
9
|
+
@message_id = opts[:message_id]
|
10
|
+
@original = opts[:original]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create an instance of {Stapfen::Message} from the passed in
|
14
|
+
# {Stomp::Message}
|
15
|
+
#
|
16
|
+
# @param [Stomp::Message] message A message created by the Stomp gem
|
17
|
+
# @return [Stapfen::Message] A Stapfen wrapper object
|
18
|
+
def self.from_stomp(message)
|
19
|
+
unless message.kind_of? Stomp::Message
|
20
|
+
raise Stapfen::InvalidMessageError, message.inspect
|
21
|
+
end
|
22
|
+
|
23
|
+
return self.new(:body => message.body,
|
24
|
+
:destination => message.headers['destination'],
|
25
|
+
:message_id => message.headers['message-id'],
|
26
|
+
:original => message)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create an instance of {Stapfen::Message} from the passed in
|
30
|
+
# +ActiveMQBytesMessage+ which a JMS consumer should receive
|
31
|
+
#
|
32
|
+
# @param [ActiveMQBytesMessage] message
|
33
|
+
# @return [Stapfen::Message] A Stapfen wrapper object
|
34
|
+
def self.from_jms(message)
|
35
|
+
unless message.kind_of? Java::JavaxJms::Message
|
36
|
+
raise Stapfen::InvalidMessageError, message.inspect
|
37
|
+
end
|
38
|
+
|
39
|
+
return self.new(:body => message.data,
|
40
|
+
:destination => message.jms_destination.getQualifiedName,
|
41
|
+
:message_id => message.jms_message_id,
|
42
|
+
:original => message)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create an instance of {Stapfen::Message} from the passed in
|
46
|
+
# +String+ which a Kafka consumer should receive
|
47
|
+
#
|
48
|
+
# @param [String] message
|
49
|
+
# @return [Stapfen::Message] A Stapfen wrapper object
|
50
|
+
def self.from_kafka(message)
|
51
|
+
unless message.kind_of? String
|
52
|
+
raise Stapfen::InvalidMessageError, message.inspect
|
53
|
+
end
|
54
|
+
|
55
|
+
return self.new(:body => message)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'stomp'
|
2
|
+
require 'stapfen/logger'
|
3
|
+
require 'stapfen/destination'
|
4
|
+
require 'stapfen/message'
|
5
|
+
|
6
|
+
module Stapfen
|
7
|
+
class Worker
|
8
|
+
include Stapfen::Logger
|
9
|
+
|
10
|
+
# Class variables!
|
11
|
+
@@signals_handled = false
|
12
|
+
@@workers = []
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_accessor :configuration, :consumers, :logger, :destructor
|
16
|
+
end
|
17
|
+
|
18
|
+
# Instantiate a new +Worker+ instance and run it
|
19
|
+
def self.run!
|
20
|
+
worker = self.new
|
21
|
+
|
22
|
+
@@workers << worker
|
23
|
+
|
24
|
+
handle_signals
|
25
|
+
|
26
|
+
worker.run
|
27
|
+
end
|
28
|
+
|
29
|
+
# Expects a block to be passed which will yield the appropriate
|
30
|
+
# configuration for the Stomp gem. Whatever the block yields will be passed
|
31
|
+
# directly into the {{Stomp::Client#new}} method
|
32
|
+
def self.configure(&block)
|
33
|
+
unless block_given?
|
34
|
+
raise Stapfen::ConfigurationError
|
35
|
+
end
|
36
|
+
@configuration = block
|
37
|
+
end
|
38
|
+
|
39
|
+
# Force the worker to use STOMP as the messaging protocol (default)
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
def self.use_stomp!
|
43
|
+
begin
|
44
|
+
require 'stomp'
|
45
|
+
rescue LoadError
|
46
|
+
puts "You need the `stomp` gem to be installed to use stomp!"
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
|
50
|
+
@protocol = 'stomp'
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.stomp?
|
55
|
+
@protocol.nil? || @protocol == 'stomp'
|
56
|
+
end
|
57
|
+
|
58
|
+
# Force the worker to use JMS as the messaging protocol.
|
59
|
+
#
|
60
|
+
# *Note:* Only works under JRuby
|
61
|
+
#
|
62
|
+
# @return [Boolean]
|
63
|
+
def self.use_jms!
|
64
|
+
unless RUBY_PLATFORM == 'java'
|
65
|
+
raise Stapfen::ConfigurationError, "You cannot use JMS unless you're running under JRuby!"
|
66
|
+
end
|
67
|
+
|
68
|
+
begin
|
69
|
+
require 'java'
|
70
|
+
require 'jms'
|
71
|
+
rescue LoadError
|
72
|
+
puts "You need the `jms` gem to be installed to use JMS!"
|
73
|
+
raise
|
74
|
+
end
|
75
|
+
|
76
|
+
@protocol = 'jms'
|
77
|
+
return true
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.jms?
|
81
|
+
@protocol == 'jms'
|
82
|
+
end
|
83
|
+
|
84
|
+
# Force the worker to use Kafka as the messaging protocol.
|
85
|
+
#
|
86
|
+
# *Note:* Only works under JRuby
|
87
|
+
#
|
88
|
+
# @return [Boolean]
|
89
|
+
def self.use_kafka!
|
90
|
+
unless RUBY_PLATFORM == 'java'
|
91
|
+
raise Stapfen::ConfigurationError, "You cannot use Kafka unless you're running under JRuby!"
|
92
|
+
end
|
93
|
+
|
94
|
+
begin
|
95
|
+
require 'java'
|
96
|
+
require 'hermann'
|
97
|
+
rescue LoadError
|
98
|
+
puts "You need the `hermann` gem to be installed to use Kafka!"
|
99
|
+
raise
|
100
|
+
end
|
101
|
+
|
102
|
+
@protocol = 'kafka'
|
103
|
+
return true
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.kafka?
|
107
|
+
@protocol == 'kafka'
|
108
|
+
end
|
109
|
+
|
110
|
+
# Optional method, should be passed a block which will yield a {{Logger}}
|
111
|
+
# instance for the Stapfen worker to use
|
112
|
+
def self.log(&block)
|
113
|
+
@logger = block
|
114
|
+
end
|
115
|
+
|
116
|
+
# Main message consumption block
|
117
|
+
def self.consume(queue_name, headers={}, &block)
|
118
|
+
unless block_given?
|
119
|
+
raise Stapfen::ConsumeError, "Cannot consume #{queue_name} without a block!"
|
120
|
+
end
|
121
|
+
@consumers ||= []
|
122
|
+
@consumers << [queue_name, headers, block]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Optional method, specifes a block to execute when the worker is shutting
|
126
|
+
# down.
|
127
|
+
def self.shutdown(&block)
|
128
|
+
@destructor = block
|
129
|
+
end
|
130
|
+
|
131
|
+
# Return all the currently running Stapfen::Worker instances in this
|
132
|
+
# process
|
133
|
+
def self.workers
|
134
|
+
@@workers
|
135
|
+
end
|
136
|
+
|
137
|
+
# Invoke +exit_cleanly+ on each of the registered Worker instances that
|
138
|
+
# this class is keeping track of
|
139
|
+
#
|
140
|
+
# @return [Boolean] Whether or not we've exited/terminated cleanly
|
141
|
+
def self.exit_cleanly
|
142
|
+
return false if workers.empty?
|
143
|
+
|
144
|
+
cleanly = true
|
145
|
+
workers.each do |w|
|
146
|
+
begin
|
147
|
+
w.exit_cleanly
|
148
|
+
rescue StandardError => ex
|
149
|
+
$stderr.write("Failure while exiting cleanly #{ex.inspect}\n#{ex.backtrace}")
|
150
|
+
cleanly = false
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
if RUBY_PLATFORM == 'java'
|
155
|
+
info "Telling the JVM to exit cleanly"
|
156
|
+
Java::JavaLang::System.exit(0)
|
157
|
+
end
|
158
|
+
|
159
|
+
return cleanly
|
160
|
+
end
|
161
|
+
|
162
|
+
# Utility method to set up the proper worker signal handlers
|
163
|
+
def self.handle_signals
|
164
|
+
return if @@signals_handled
|
165
|
+
|
166
|
+
Signal.trap(:INT) do
|
167
|
+
self.exit_cleanly
|
168
|
+
exit!
|
169
|
+
end
|
170
|
+
|
171
|
+
Signal.trap(:TERM) do
|
172
|
+
self.exit_cleanly
|
173
|
+
end
|
174
|
+
|
175
|
+
@@signals_handled = true
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
|
180
|
+
############################################################################
|
181
|
+
# Instance Methods
|
182
|
+
############################################################################
|
183
|
+
|
184
|
+
attr_accessor :client
|
185
|
+
|
186
|
+
def run
|
187
|
+
if self.class.stomp?
|
188
|
+
require 'stapfen/client/stomp'
|
189
|
+
@client = Stapfen::Client::Stomp.new(self.class.configuration.call)
|
190
|
+
elsif self.class.jms?
|
191
|
+
require 'stapfen/client/jms'
|
192
|
+
@client = Stapfen::Client::JMS.new(self.class.configuration.call)
|
193
|
+
elsif self.class.kafka?
|
194
|
+
require 'stapfen/client/kafka'
|
195
|
+
@client = Stapfen::Client::Kafka.new(self.class.configuration.call)
|
196
|
+
end
|
197
|
+
|
198
|
+
debug("Running with #{@client} inside of Thread:#{Thread.current.inspect}")
|
199
|
+
|
200
|
+
@client.connect
|
201
|
+
|
202
|
+
self.class.consumers.each do |name, headers, block|
|
203
|
+
unreceive_headers = {}
|
204
|
+
[:max_redeliveries, :dead_letter_queue].each do |sym|
|
205
|
+
unreceive_headers[sym] = headers[sym] if headers.has_key? sym
|
206
|
+
end
|
207
|
+
|
208
|
+
# We're taking each block and turning it into a method so that we can
|
209
|
+
# use the instance scope instead of the blocks originally bound scope
|
210
|
+
# which would be at a class level
|
211
|
+
method_name = name.gsub(/[.|\-]/, '_').to_sym
|
212
|
+
self.class.send(:define_method, method_name, &block)
|
213
|
+
|
214
|
+
client.subscribe(name, headers) do |m|
|
215
|
+
message = nil
|
216
|
+
if self.class.stomp?
|
217
|
+
message = Stapfen::Message.from_stomp(m)
|
218
|
+
end
|
219
|
+
|
220
|
+
if self.class.jms?
|
221
|
+
message = Stapfen::Message.from_jms(m)
|
222
|
+
end
|
223
|
+
|
224
|
+
if self.class.kafka?
|
225
|
+
message = Stapfen::Message.from_kafka(m)
|
226
|
+
end
|
227
|
+
|
228
|
+
success = self.send(method_name, message)
|
229
|
+
|
230
|
+
unless success
|
231
|
+
if client.can_unreceive? && !unreceive_headers.empty?
|
232
|
+
client.unreceive(m, unreceive_headers)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
begin
|
239
|
+
client.runloop
|
240
|
+
warn("Exiting the runloop for #{self}")
|
241
|
+
rescue Interrupt
|
242
|
+
exit_cleanly
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Invokes the shutdown block if it has been created, and closes the
|
247
|
+
# {{Stomp::Client}} connection unless it has already been shut down
|
248
|
+
def exit_cleanly
|
249
|
+
info("#{self} exiting ")
|
250
|
+
self.class.destructor.call if self.class.destructor
|
251
|
+
|
252
|
+
info "Killing client"
|
253
|
+
begin
|
254
|
+
# Only close the client if we have one sitting around
|
255
|
+
if client
|
256
|
+
unless client.closed?
|
257
|
+
client.close
|
258
|
+
end
|
259
|
+
end
|
260
|
+
rescue StandardError => exc
|
261
|
+
error "Exception received while trying to close client! #{exc.inspect}"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|