alephant-sequencer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 39cf57ec693d0b9940bc8697bed009cfdac7f681
4
+ data.tar.gz: a7ac6da5464b431b80d822310c86b681575ec85d
5
+ SHA512:
6
+ metadata.gz: 1c6720473cd4f33b5b4f72ef21ffcb8ae34c73cf9d520c5bf686be6e37e9d38d50a42992dde1289bf9975de2114ae422bec27348415d856b7a76ecb133f671ce
7
+ data.tar.gz: e5b0ad2c052721f9ab3386840adf53f426f5ef56be0f7dfce1c79206fb22df721950807b6df9ea79ae1ae17da79ad11b9ac37043a9ee256aebbeac68e4e01c0a
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /config/*.yaml
2
+ /config/*.yml
3
+ Gemfile.lock
4
+ .rspec
5
+ *.gem
6
+
7
+ /pkg
8
+ /tmp
9
+ /components
10
+
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - "jruby"
4
+ notifications:
5
+ email:
6
+ recipients:
7
+ - kenoir@gmail.com
8
+ on_failure: change
9
+ on_success: never
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in alephant-sequencer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Robert Kenny
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Alephant::Sequencer
2
+
3
+ Using DynamoDB consistent read to enforce message order from SQS.
4
+
5
+ [![Build
6
+ Status](https://travis-ci.org/BBC-News/alephant-sequencer.png)](https://travis-ci.org/BBC-News/alephant-sequencer)
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'alephant-sequencer'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install alephant-sequencer
21
+
22
+ ## Usage
23
+
24
+ ```rb
25
+
26
+ require 'alephant-sequencer'
27
+
28
+ #Optional JSONPath specifying location of sequence_id
29
+ sequence_id = '$.sequence_number'
30
+
31
+ sequencer = Sequencer.create(table_name, sqs_queue_url, sequence_id)
32
+
33
+ # Data from SQS message
34
+ data = Struct.new(:body).new({:sequence_number => 3})
35
+
36
+ # Sets last seen id
37
+ sequencer.set_last_seen(data)
38
+
39
+ # Gets last seen id
40
+ sequencer.get_last_seen
41
+ # => 3
42
+
43
+ # Is the message sequential?
44
+ sequencer.sequential?(data)
45
+
46
+ # Reset sequence
47
+ sequencer.delete!
48
+
49
+ ## Contributing
50
+ 1. Fork it ( http://github.com/<my-github-username>/alephant-sequencer/fork )
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
+
3
+ require 'rspec/core/rake_task'
4
+ require 'bundler/gem_tasks'
5
+ require 'alephant/sequencer'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => :spec
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'alephant/sequencer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "alephant-sequencer"
8
+ spec.version = Alephant::Sequencer::VERSION
9
+ spec.authors = ["Robert Kenny"]
10
+ spec.email = ["kenoir@gmail.com"]
11
+ spec.summary = %q{Adds sequencing functionality to Alephant.}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.5"
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "rspec"
23
+ spec.add_development_dependency "rspec-nc"
24
+ spec.add_development_dependency "guard"
25
+ spec.add_development_dependency "guard-rspec"
26
+ spec.add_development_dependency "pry"
27
+ spec.add_development_dependency "pry-remote"
28
+ spec.add_development_dependency "pry-nav"
29
+
30
+ spec.add_runtime_dependency 'aws-sdk', '~> 1.0'
31
+ end
@@ -0,0 +1,15 @@
1
+ require "alephant/sequencer/version"
2
+ require "alephant/sequencer/sequencer"
3
+ require "alephant/sequencer/sequence_table"
4
+
5
+ module Alephant
6
+ module Sequencer
7
+ @@sequence_tables = {}
8
+
9
+ def self.create(table_name, ident, jsonpath = nil)
10
+ @@sequence_tables[table_name] ||= SequenceTable.new(table_name)
11
+ Sequencer.new(@@sequence_tables[table_name], ident, jsonpath)
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,99 @@
1
+ require 'aws-sdk'
2
+ require 'thread'
3
+ require 'timeout'
4
+
5
+ module Alephant
6
+ module Sequencer
7
+ class SequenceTable
8
+ attr_reader :table_name
9
+
10
+ TIMEOUT = 120
11
+ DEFAULT_CONFIG = {
12
+ :write_units => 5,
13
+ :read_units => 10,
14
+ }
15
+ SCHEMA = {
16
+ :hash_key => {
17
+ :key => :string,
18
+ :value => :string
19
+ }
20
+ }
21
+
22
+ def initialize(table_name, config = DEFAULT_CONFIG)
23
+ @mutex = Mutex.new
24
+ @dynamo_db = AWS::DynamoDB.new
25
+ @table_name = table_name
26
+ @config = config
27
+ end
28
+
29
+ def create
30
+ @mutex.synchronize do
31
+ ensure_table_exists
32
+ ensure_table_active
33
+ end
34
+ end
35
+
36
+ def table
37
+ @table ||= @dynamo_db.tables[@table_name]
38
+ end
39
+
40
+ def sequence_for(ident)
41
+ rows = batch_get_value_for(ident)
42
+ rows.count >= 1 ? rows.first['value'].to_i : 0
43
+ end
44
+
45
+ def set_sequence_for(ident,value)
46
+ @mutex.synchronize do
47
+ AWS::DynamoDB::BatchWrite.new.tap { |batch|
48
+ batch.put(
49
+ table_name,
50
+ [:key => ident,:value => value]
51
+ )
52
+ }.process!
53
+ end
54
+ end
55
+
56
+ def delete_item!(ident)
57
+ table.items[ident].delete
58
+ end
59
+
60
+ private
61
+ def batch_get_value_for(ident)
62
+ table.batch_get(['value'],[ident],batch_get_opts)
63
+ end
64
+
65
+ def batch_get_opts
66
+ { :consistent_read => true }
67
+ end
68
+
69
+ def ensure_table_exists
70
+ create_dynamodb_table unless table.exists?
71
+ end
72
+
73
+ def ensure_table_active
74
+ sleep_until_table_active unless table_active?
75
+ end
76
+
77
+ def create_dynamodb_table
78
+ @table = @dynamo_db.tables.create(
79
+ @table_name,
80
+ @config[:read_units],
81
+ @config[:write_units],
82
+ SCHEMA
83
+ )
84
+ end
85
+
86
+ def table_active?
87
+ table.status == :active
88
+ end
89
+
90
+ def sleep_until_table_active
91
+ begin
92
+ Timeout::timeout(TIMEOUT) do
93
+ sleep 1 until table_active?
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,52 @@
1
+ require 'jsonpath'
2
+
3
+ module Alephant
4
+ module Sequencer
5
+ class Sequencer
6
+ attr_reader :ident, :jsonpath
7
+
8
+ def initialize(sequence_table, id, sequence_path = nil)
9
+ @mutex = Mutex.new
10
+ @sequence_table = sequence_table
11
+ @jsonpath = sequence_path
12
+ @ident = id
13
+
14
+ @sequence_table.create
15
+ end
16
+
17
+ def sequential?(data)
18
+ get_last_seen < sequence_id_from(data)
19
+ end
20
+
21
+ def delete!
22
+ @sequence_table.delete_item!(ident)
23
+ end
24
+
25
+ def set_last_seen(data)
26
+ last_seen_id = sequence_id_from(data)
27
+
28
+ @sequence_table.set_sequence_for(ident, last_seen_id)
29
+ end
30
+
31
+ def get_last_seen
32
+ @sequence_table.sequence_for(ident)
33
+ end
34
+
35
+ private
36
+ def sequence_id_from(data)
37
+ jsonpath.nil? ?
38
+ default_sequence_id_for(data) :
39
+ sequence_from_jsonpath_for(data)
40
+ end
41
+
42
+ def sequence_from_jsonpath_for(data)
43
+ JsonPath.on(data.body, jsonpath).first
44
+ end
45
+
46
+ def default_sequence_id_for(data)
47
+ data.body['sequence_id'].to_i
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ module Alephant
2
+ module Sequencer
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+
3
+ describe Alephant::Sequencer do
4
+ let(:ident) { :ident }
5
+ let(:jsonpath) { :jsonpath }
6
+
7
+ describe ".create(table_name, ident, jsonpath)" do
8
+ it "should return a Sequencer" do
9
+ Alephant::Sequencer::SequenceTable.any_instance.stub(:create)
10
+ expect(subject.create(:table_name, ident, jsonpath)).to be_a Alephant::Sequencer::Sequencer
11
+ end
12
+ end
13
+
14
+ describe Alephant::Sequencer::Sequencer do
15
+ let(:data) { double() }
16
+ let(:last_seen) { 42 }
17
+ let(:sequence_table) { double().tap { |o| o.stub(:create) } }
18
+ subject { Alephant::Sequencer::Sequencer.new(sequence_table, ident, jsonpath) }
19
+
20
+ describe "#initialize(opts, id)" do
21
+ it "sets @jsonpath, @ident" do
22
+ expect(subject.jsonpath).to eq(jsonpath)
23
+ expect(subject.ident).to eq(ident)
24
+ end
25
+
26
+ it "calls create on sequence_table" do
27
+ table = double()
28
+ table.should_receive(:create)
29
+
30
+ Alephant::Sequencer::Sequencer.new(table, ident, jsonpath)
31
+ end
32
+ end
33
+
34
+ describe "#get_last_seen" do
35
+ it "returns sequence_table.sequence_for(ident)" do
36
+ table = double()
37
+ table.stub(:create)
38
+ table.should_receive(:sequence_for).with(ident).and_return(:expected_value)
39
+
40
+ expect(
41
+ Alephant::Sequencer::Sequencer.new(table, ident).get_last_seen
42
+ ).to eq(:expected_value)
43
+ end
44
+ end
45
+
46
+ describe "#set_last_seen(data)" do
47
+ before(:each) do
48
+ Alephant::Sequencer::Sequencer.any_instance.stub(:sequence_id_from).and_return(last_seen)
49
+ end
50
+
51
+ it "calls set_sequence_for(ident, last_seen)" do
52
+ table = double()
53
+ table.stub(:create)
54
+ table.should_receive(:set_sequence_for).with(ident, last_seen)
55
+
56
+ Alephant::Sequencer::Sequencer.new(table, ident).set_last_seen(data)
57
+ end
58
+ end
59
+
60
+ describe "#sequential?(data, jsonpath)" do
61
+
62
+ before(:each) do
63
+ Alephant::Sequencer::Sequencer.any_instance.stub(:get_last_seen).and_return(1)
64
+ data.stub(:body).and_return('sequence_id' => id_value)
65
+ end
66
+
67
+ context "jsonpath = '$.sequence_id'" do
68
+ let(:jsonpath) { '$.sequence_id' }
69
+ subject { Alephant::Sequencer::Sequencer.new(sequence_table, :ident, jsonpath) }
70
+ context "sequential" do
71
+ let(:id_value) { 2 }
72
+ it "is true" do
73
+ expect(subject.sequential?(data)).to be_true
74
+ end
75
+ end
76
+
77
+ context "nonsequential" do
78
+ let(:id_value) { 0 }
79
+ it "is false" do
80
+ expect(subject.sequential?(data)).to be_false
81
+ end
82
+ end
83
+ end
84
+
85
+ context "jsonpath = nil" do
86
+ let(:jsonpath) { nil }
87
+ subject { Alephant::Sequencer::Sequencer.new(sequence_table, :ident, jsonpath) }
88
+
89
+ context "sequential" do
90
+ let(:id_value) { 2 }
91
+ it "is true" do
92
+ expect(subject.sequential?(data)).to be_true
93
+ end
94
+ end
95
+
96
+ context "nonsequential" do
97
+ let(:id_value) { 0 }
98
+ it "is false" do
99
+ expect(subject.sequential?(data)).to be_false
100
+ end
101
+ end
102
+ end
103
+
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,5 @@
1
+ $: << File.join(File.dirname(__FILE__),"..", "lib")
2
+
3
+ require 'pry'
4
+ require 'alephant/sequencer'
5
+
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alephant-sequencer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Robert Kenny
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: '1.5'
25
+ prerelease: false
26
+ type: :development
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ prerelease: false
40
+ type: :development
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ prerelease: false
54
+ type: :development
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-nc
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ prerelease: false
68
+ type: :development
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ prerelease: false
82
+ type: :development
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ prerelease: false
96
+ type: :development
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ prerelease: false
110
+ type: :development
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-remote
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - '>='
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ prerelease: false
124
+ type: :development
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-nav
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ prerelease: false
138
+ type: :development
139
+ - !ruby/object:Gem::Dependency
140
+ name: aws-sdk
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: '1.0'
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ~>
149
+ - !ruby/object:Gem::Version
150
+ version: '1.0'
151
+ prerelease: false
152
+ type: :runtime
153
+ description:
154
+ email:
155
+ - kenoir@gmail.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - .gitignore
161
+ - .travis.yml
162
+ - Gemfile
163
+ - LICENSE.txt
164
+ - README.md
165
+ - Rakefile
166
+ - alephant-sequencer.gemspec
167
+ - lib/alephant/sequencer.rb
168
+ - lib/alephant/sequencer/sequence_table.rb
169
+ - lib/alephant/sequencer/sequencer.rb
170
+ - lib/alephant/sequencer/version.rb
171
+ - spec/sequencer_spec.rb
172
+ - spec/spec_helper.rb
173
+ homepage: ''
174
+ licenses:
175
+ - MIT
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - '>='
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - '>='
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 2.1.9
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: Adds sequencing functionality to Alephant.
197
+ test_files:
198
+ - spec/sequencer_spec.rb
199
+ - spec/spec_helper.rb