klomp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "http://rubygems.org"
2
+ gem "onstomp"
3
+ gem "json"
4
+
5
+ group :development do
6
+ gem "foreman"
7
+ gem "rake"
8
+ gem "ZenTest"
9
+ end
@@ -0,0 +1,20 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ ZenTest (4.8.0)
5
+ foreman (0.46.0)
6
+ thor (>= 0.13.6)
7
+ json (1.7.1)
8
+ onstomp (1.0.6)
9
+ rake (0.9.2.2)
10
+ thor (0.15.2)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ ZenTest
17
+ foreman
18
+ json
19
+ onstomp
20
+ rake
@@ -0,0 +1,2 @@
1
+ apollo_primary: /usr/local/var/apollo-primary/bin/apollo-broker run
2
+ apollo_secondary: /usr/local/var/apollo-secondary/bin/apollo-broker run
@@ -0,0 +1,87 @@
1
+ # Klomp
2
+
3
+ Klomp is a simple wrapper around the [OnStomp](https://github.com/meadvillerb/onstomp/)
4
+ library with some additional HA and usability features:
5
+
6
+ * When initialized with multiple broker URIs, Klomp will publish messages to
7
+ one broker at a time, but will consume from all brokers simultaneously. This is
8
+ a slight improvement over the regular [OnStomp::Failover::Client](http://mdvlrb.com/onstomp/OnStomp/Failover/Client.html)
9
+ which handles all publishing and subscribing through a single "active" broker.
10
+ This traditional one-broker-at-a-time technique can lead to a split-brain
11
+ scenario in which messages are only received by a subset of your STOMP clients.
12
+ By consuming from all brokers simultaneously, Klomp ensures that no message is
13
+ left behind.
14
+
15
+ * Where applicable, message bodies are automatically translated between native
16
+ Ruby and JSON objects.
17
+
18
+ * If a reply-to header is found in a message, a response is automatically
19
+ sent to the reply-to destination.
20
+
21
+ ## Installation
22
+
23
+ gem install klomp
24
+
25
+ ## Example usage
26
+
27
+ The goal is that you should be able to use most (if not all) of the standard
28
+ OnStomp API (see [OnStomp's UserNarrative](https://github.com/meadvillerb/onstomp/blob/master/extra_doc/UserNarrative.md))
29
+ via a `Klomp::Client`:
30
+
31
+ client = Klomp::Client.new([ ... ])
32
+
33
+ However, there will be some differences in the API due to how `Klomp::Client`
34
+ manages connections. For example, while the `connected?` method normally
35
+ returns a boolean value, Klomp's `connected?` will return an array of booleans
36
+ (i.e. one result for each broker).
37
+
38
+ ## Developers
39
+
40
+ Set up the environment using `bundle install`. Note that the tests currently
41
+ assume a specific Apollo configuration which can be created on OSX using the
42
+ following commands:
43
+
44
+ brew install apollo
45
+ apollo create /usr/local/var/apollo-primary
46
+ apollo create /usr/local/var/apollo-secondary
47
+ sed -i -e 's/616/626/' /usr/local/var/apollo-secondary/etc/apollo.xml
48
+
49
+ Once Apollo is configured, the brokers can be started via `foreman start`. Now
50
+ you can run the test suite via `rake test` or `autotest`.
51
+
52
+ In addition to the regular test suite, there is a rake task called
53
+ "test_failover" that will start an infinite publish/subscribe loop. Once this
54
+ task is running, you can randomly kill Apollo brokers to test STOMP client
55
+ failover.
56
+
57
+ ## Change Log
58
+
59
+ ### 0.0.1
60
+
61
+ * Initial release
62
+
63
+ ## License
64
+
65
+ Copyright (C) 2012 LivingSocial
66
+
67
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
68
+ this software and associated documentation files (the "Software"), to deal in
69
+ the Software without restriction, including without limitation the rights to
70
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
71
+ of the Software, and to permit persons to whom the Software is furnished to do
72
+ so, subject to the following conditions:
73
+
74
+ The above copyright notice and this permission notice shall be included in all
75
+ copies or substantial portions of the Software.
76
+
77
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
78
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
79
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
80
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
81
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
82
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
83
+ SOFTWARE.
84
+
85
+ ## Credits
86
+
87
+ * [Michael Paul Thomas Conigliaro](http://conigliaro.org): Original author
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Dir.glob('tasks/*.rake').each { |r| import r }
5
+
6
+ Rake::TestTask.new
7
+ task :default => :test
@@ -0,0 +1,20 @@
1
+ $:.push File.expand_path(File.join(File.dirname(__FILE__), "lib"))
2
+ require 'klomp'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["LivingSocial"]
6
+ gem.email = ["dev.happiness@livingsocial.com"]
7
+ gem.description = "A simple wrapper around the OnStomp library with additional features"
8
+ gem.summary = "A simple wrapper around the OnStomp library with additional features"
9
+ gem.homepage = "https://github.com/livingsocial/klomp"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "klomp"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Klomp::VERSION
17
+
18
+ gem.add_dependency("onstomp")
19
+ gem.add_dependency("json")
20
+ end
@@ -0,0 +1,9 @@
1
+ require 'onstomp'
2
+ require 'onstomp/failover'
3
+ require 'json'
4
+
5
+ require 'klomp/client'
6
+
7
+ module Klomp
8
+ VERSION = '0.0.1'
9
+ end
@@ -0,0 +1,88 @@
1
+ module Klomp
2
+
3
+ class Client
4
+ attr_reader :options, :read_conn, :write_conn
5
+
6
+ def initialize(uri, options={})
7
+ @options ||= {
8
+ :translate_json => true,
9
+ :auto_reply_to => true
10
+ }
11
+
12
+ ofc_options = options.inject({
13
+ :retry_attempts => -1,
14
+ :retry_delay => 1
15
+ }) { |memo,(k,v)| memo.merge({k => v}) if memo.has_key?(k) }
16
+
17
+ if uri.is_a?(Array)
18
+ @write_conn = OnStomp::Failover::Client.new(uri, ofc_options)
19
+ @read_conn = uri.inject([]) { |memo,obj| memo + [OnStomp::Failover::Client.new([obj], ofc_options)] }
20
+ else
21
+ @write_conn = OnStomp::Failover::Client.new([uri], ofc_options)
22
+ @read_conn = [@write_conn]
23
+ end
24
+ end
25
+
26
+ def send(*args, &block)
27
+ if @options[:translate_json] && [Array, Hash].any? { |type| args[1].kind_of?(type) }
28
+ args[1] = args[1].to_json
29
+ args[2] = {} if args[2].nil?
30
+ args[2][:'content-type'] = 'application/json'
31
+ else
32
+ args[1] = args[1].to_s
33
+ end
34
+ @write_conn.send(*args, &block)
35
+ end
36
+
37
+ def subscribe(*args, &block)
38
+ frames = []
39
+ @read_conn.each do |c|
40
+ frames << c.subscribe(*args) do |msg|
41
+ if @options[:translate_json]
42
+ msg.body = begin
43
+ JSON.parse(msg.body)
44
+ rescue JSON::ParserError
45
+ msg.body
46
+ end
47
+ end
48
+ reply_args = yield msg
49
+ if @options[:auto_reply_to] && !msg.headers[:'reply-to'].nil?
50
+ if reply_args.is_a?(Array)
51
+ send(msg.headers[:'reply-to'], *reply_args)
52
+ else
53
+ send(msg.headers[:'reply-to'], reply_args)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ frames
59
+ end
60
+
61
+ def method_missing(method, *args, &block)
62
+ write_only_methods = [
63
+ :abort,
64
+ :begin,
65
+ :commit,
66
+ ]
67
+ read_only_methods = [
68
+ :ack,
69
+ :nack,
70
+ :unsubscribe
71
+ ]
72
+ returns = {
73
+ :connect => self
74
+ }
75
+
76
+ result = if write_only_methods.include?(method)
77
+ @write_conn.send(method, *args, &block)
78
+ elsif read_only_methods.include?(method)
79
+ @read_conn.map { |c| c.__send__(method, *args, &block) }
80
+ else
81
+ ([@write_conn] + @read_conn).uniq.map { |c| c.__send__(method, *args) }
82
+ end
83
+ returns.include?(method) ? returns[method] : result
84
+ end
85
+
86
+ end
87
+
88
+ end
@@ -0,0 +1,35 @@
1
+ desc "Start an infinite publish/subscribe loop to test STOMP client failover"
2
+ task :test_failover do
3
+ require 'klomp'
4
+
5
+ # Set the delay between publish events. If this is too small, the consumer
6
+ # will never be able to catch up to the producer, giving the false impression
7
+ # of lost messages.
8
+ publish_interval = 0.01
9
+
10
+ client = Klomp::Client.new([
11
+ 'stomp://admin:password@localhost:61613',
12
+ 'stomp://admin:password@127.0.0.1:62613'
13
+ ]).connect
14
+
15
+ last_i = nil
16
+ client.subscribe("/queue/test") do |msg|
17
+ print "-"
18
+ last_i = msg.body.to_i
19
+ end
20
+
21
+ begin
22
+ i = 0
23
+ loop do
24
+ i += 1
25
+ client.send("/queue/test", i.to_s) do |r|
26
+ print "+"
27
+ end
28
+ sleep publish_interval
29
+ end
30
+ rescue SignalException
31
+ client.disconnect
32
+ puts
33
+ puts "Sent #{i}; Received #{last_i}; Lost #{i - last_i}"
34
+ end
35
+ end
@@ -0,0 +1,91 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/pride'
3
+
4
+ require 'klomp'
5
+
6
+ describe Klomp::Client do
7
+
8
+ before do
9
+ @uris = [
10
+ 'stomp://admin:password@localhost:61613',
11
+ 'stomp://admin:password@127.0.0.1:62613'
12
+ ]
13
+ @destination = '/queue/test_component.test_event'
14
+ end
15
+
16
+ it 'accepts a single uri and establishes separate failover connections for writes and reads' do
17
+ client = Klomp::Client.new(@uris.first).connect
18
+
19
+ assert_equal [client.write_conn], client.read_conn
20
+ assert client.write_conn.connected?
21
+
22
+ client.disconnect
23
+ end
24
+
25
+ it 'accepts an array of uris and establishes separate failover connections for writes and reads' do
26
+ client = Klomp::Client.new(@uris).connect
27
+
28
+ assert client.write_conn.connected?
29
+ refute_empty client.read_conn
30
+ client.read_conn.each do |obj|
31
+ assert obj.connected?
32
+ end
33
+
34
+ client.disconnect
35
+ end
36
+
37
+ it 'disconnnects' do
38
+ client = Klomp::Client.new(@uris.first).connect
39
+ assert client.write_conn.connected?
40
+ client.disconnect
41
+ refute client.write_conn.connected?
42
+ end
43
+
44
+ it 'sends heartbeat' do
45
+ client = Klomp::Client.new(@uris).connect.beat
46
+ end
47
+
48
+ it 'sends requests and gets responses' do
49
+ client = Klomp::Client.new(@uris).connect
50
+ body = { 'body' => rand(36**128).to_s(36) }
51
+
52
+ client.send(@destination, body, :ack=>'client')
53
+
54
+ got_message = false
55
+ client.subscribe(@destination) do |msg|
56
+ got_message = true if msg.body == body
57
+ client.ack(msg)
58
+ end
59
+ sleep 1
60
+ assert got_message
61
+
62
+ client.disconnect
63
+ end
64
+
65
+ it 'automatically publishes responses to the reply-to destination' do
66
+ client = Klomp::Client.new(@uris).connect
67
+ reply_to_body = { 'reply_to_body' => rand(36**128).to_s(36) }
68
+
69
+ client.send(@destination, nil, { 'reply-to' => @destination })
70
+
71
+ got_message = false
72
+ client.subscribe(@destination) do |msg|
73
+ got_message = true if msg.body == reply_to_body
74
+ reply_to_body
75
+ end
76
+ sleep 1
77
+ assert got_message
78
+
79
+ client.disconnect
80
+ end
81
+
82
+ it 'unsubscribes' do
83
+ client = Klomp::Client.new(@uris).connect
84
+
85
+ subscribe_frames = client.subscribe(@destination) { |msg| }
86
+ client.unsubscribe(subscribe_frames)
87
+
88
+ client.disconnect
89
+ end
90
+
91
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: klomp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - LivingSocial
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: onstomp
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A simple wrapper around the OnStomp library with additional features
47
+ email:
48
+ - dev.happiness@livingsocial.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - Gemfile.lock
56
+ - Procfile
57
+ - README.md
58
+ - Rakefile
59
+ - klomp.gemspec
60
+ - lib/klomp.rb
61
+ - lib/klomp/client.rb
62
+ - tasks/test_failover.rake
63
+ - test/test_client.rb
64
+ homepage: https://github.com/livingsocial/klomp
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 1.8.23
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: A simple wrapper around the OnStomp library with additional features
88
+ test_files:
89
+ - test/test_client.rb
90
+ has_rdoc: