alephant-sequencer 0.0.1

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 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