aws-sessionstore-dynamodb 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.yardopts +3 -0
  4. data/Gemfile +28 -0
  5. data/LICENSE.txt +12 -0
  6. data/README.md +171 -0
  7. data/Rakefile +15 -0
  8. data/aws-sessionstore-dynamodb.gemspec +18 -0
  9. data/lib/aws-sessionstore-dynamodb.rb +34 -0
  10. data/lib/aws/session_store/dynamo_db/configuration.rb +298 -0
  11. data/lib/aws/session_store/dynamo_db/errors/base_handler.rb +45 -0
  12. data/lib/aws/session_store/dynamo_db/errors/default_handler.rb +57 -0
  13. data/lib/aws/session_store/dynamo_db/garbage_collection.rb +128 -0
  14. data/lib/aws/session_store/dynamo_db/invalid_id_error.rb +21 -0
  15. data/lib/aws/session_store/dynamo_db/lock_wait_timeout_error.rb +21 -0
  16. data/lib/aws/session_store/dynamo_db/locking/base.rb +162 -0
  17. data/lib/aws/session_store/dynamo_db/locking/null.rb +40 -0
  18. data/lib/aws/session_store/dynamo_db/locking/pessimistic.rb +160 -0
  19. data/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb +21 -0
  20. data/lib/aws/session_store/dynamo_db/rack_middleware.rb +130 -0
  21. data/lib/aws/session_store/dynamo_db/railtie.rb +28 -0
  22. data/lib/aws/session_store/dynamo_db/table.rb +98 -0
  23. data/lib/aws/session_store/dynamo_db/tasks/session_table.rake +21 -0
  24. data/lib/aws/session_store/dynamo_db/version.rb +21 -0
  25. data/lib/rails/generators/sessionstore/dynamodb/dynamodb_generator.rb +55 -0
  26. data/lib/rails/generators/sessionstore/dynamodb/templates/sessionstore/USAGE +13 -0
  27. data/lib/rails/generators/sessionstore/dynamodb/templates/sessionstore/dynamodb.yml +71 -0
  28. data/lib/rails/generators/sessionstore/dynamodb/templates/sessionstore_migration.rb +10 -0
  29. data/spec/aws/session_store/dynamo_db/app_config.yml +19 -0
  30. data/spec/aws/session_store/dynamo_db/config/dynamo_db_session.yml +24 -0
  31. data/spec/aws/session_store/dynamo_db/configuration_spec.rb +101 -0
  32. data/spec/aws/session_store/dynamo_db/error/default_error_handler_spec.rb +62 -0
  33. data/spec/aws/session_store/dynamo_db/garbage_collection_spec.rb +156 -0
  34. data/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb +95 -0
  35. data/spec/aws/session_store/dynamo_db/rack_middleware_database_spec.rb +129 -0
  36. data/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb +149 -0
  37. data/spec/aws/session_store/dynamo_db/rails_app_config.yml +24 -0
  38. data/spec/aws/session_store/dynamo_db/table_spec.rb +46 -0
  39. data/spec/spec_helper.rb +61 -0
  40. data/tasks/test.rake +29 -0
  41. metadata +123 -0
@@ -0,0 +1,21 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+
15
+ module AWS
16
+ module SessionStore
17
+ module DynamoDB
18
+ VERSION = "0.5.0"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'rails/generators/named_base'
15
+
16
+ # This class generates
17
+ # a migration file for deleting and creating
18
+ # a DynamoDB sessions table.
19
+ module Sessionstore
20
+ module Generators
21
+ class DynamodbGenerator < Rails::Generators::NamedBase
22
+ include Rails::Generators::Migration
23
+
24
+ source_root File.expand_path('templates', File.dirname(__FILE__))
25
+
26
+ # Desired name of migration class
27
+ argument :name, :type => :string, :default => "sessionstore_migration"
28
+
29
+ # @return [Rails Migration File] migration file for creation and deletion of
30
+ # session table.
31
+ def generate_migration_file
32
+ migration_template "sessionstore_migration.rb",
33
+ "#{Rails.root}/db/migrate/#{file_name}"
34
+ end
35
+
36
+
37
+ def copy_sample_config_file
38
+ file = File.join("sessionstore", "dynamodb.yml")
39
+ template file, File.join(Rails.root, "config", file)
40
+ end
41
+
42
+ private
43
+
44
+ # @return [String] filename
45
+ def file_name
46
+ name.underscore
47
+ end
48
+
49
+ # @return [String] migration version using time stamp YYYYMMDDHHSS
50
+ def self.next_migration_number(dir = nil)
51
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ Description:
2
+ Generates a migration file for deleting and a creating a DynamoDB
3
+ sessions table, and a configuration file for the session store.
4
+
5
+ Example:
6
+ rails generate sessionstore:dynamodb <MigrationName>
7
+
8
+ This will create:
9
+ db/migrate/VERSION_migration_name.rb
10
+ config/sessionstore/dynamodb.yml
11
+
12
+ The migration will be run when the command rake db:migrate is run
13
+ in the command line.
@@ -0,0 +1,71 @@
1
+ # Uncomment and manipulate the key value pairs below
2
+ # in order to configure the DynamoDB Session Store Application.
3
+
4
+ # [String] The secret key for HMAC encryption.
5
+ # Must define secret_key here or ENV or at runtime!
6
+ # By default, a random key was generated for you!
7
+ #
8
+ secret_key: <%= require 'securerandom'; SecureRandom.hex(64) %>
9
+
10
+ # [String] Session table name.
11
+ #
12
+ # table_name: Sessions
13
+
14
+ # [String] Session table hash key name.
15
+ #
16
+ # table_key: session_id
17
+
18
+ # [Boolean] Define as true or false depending on if you want a strongly
19
+ # consistent read.
20
+ # See AWS DynamoDB documentation for table consistent_read for more
21
+ # information on this setting.
22
+ #
23
+ # consistent_read: true
24
+
25
+ # [Integer] Maximum number of reads consumed per second before
26
+ # DynamoDB returns a ThrottlingException. See AWS DynamoDB documentation
27
+ # or table read_capacity for more information on this setting.
28
+ #
29
+ # read_capacity: 10
30
+
31
+ # [Integer] Maximum number of writes consumed per second before
32
+ # DynamoDB returns a ThrottlingException. See AWS DynamoDB documentation
33
+ # or table write_capacity for more information on this setting.
34
+ #
35
+ # write_capacity: 5
36
+
37
+ # [Boolean] Define as true or false depending on whether you want all errors to be
38
+ # raised up the stack.
39
+ #
40
+ # raise_errors: false
41
+
42
+ # [Integer] Maximum number of seconds earlier
43
+ # from the current time that a session was created.
44
+ # By default this is 7 days.
45
+ #
46
+ # max_age: 604800
47
+
48
+ # [Integer] Maximum number of seconds
49
+ # before the current time that the session was last accessed.
50
+ # By default this is 5 hours.
51
+ #
52
+ # max_stale: 18000
53
+
54
+ # [Boolean] Define as true or false for whether you want to enable locking
55
+ # for all accesses to session data.
56
+ #
57
+ # enable_locking: false
58
+
59
+ # [Integer] Time in milleseconds after which lock will expire.
60
+ #
61
+ # lock_expiry_time: 500
62
+
63
+ # [Integer] Time in milleseconds to wait before retrying to obtain
64
+ # lock once an attempt to obtain lock has been made and has failed.
65
+ #
66
+ # lock_retry_delay: 500
67
+
68
+ # [Integer] Maximum time in seconds to wait to acquire lock
69
+ # before giving up.
70
+ #
71
+ # lock_max_wait_time: 1
@@ -0,0 +1,10 @@
1
+ class <%= name.camelize %> < ActiveRecord::Migration
2
+ def up
3
+ AWS::SessionStore::DynamoDB::Table.create_table
4
+ end
5
+
6
+ def down
7
+ AWS::SessionStore::DynamoDB::Table.delete_table
8
+ end
9
+ end
10
+
@@ -0,0 +1,19 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ table_name: NewTable
15
+ table_key: Somekey
16
+ consistent_read: true
17
+ AWS_ACCESS_KEY_ID: FakeKey
18
+ AWS_SECRET_ACCESS_KEY: Secret
19
+ AWS_REGION: New York
@@ -0,0 +1,24 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ development:
15
+ key1: development value 1
16
+ test:
17
+ table_name: NewTable
18
+ table_key: Somekey
19
+ consistent_read: true
20
+ AWS_ACCESS_KEY_ID: FakeKey
21
+ AWS_SECRET_ACCESS_KEY: Secret
22
+ AWS_REGION: New York
23
+ production:
24
+ key1: production value 1
@@ -0,0 +1,101 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require "spec_helper"
15
+
16
+ describe AWS::SessionStore::DynamoDB::Configuration do
17
+
18
+ let(:defaults) do
19
+ {
20
+ :table_name => "sessions",
21
+ :table_key => "session_id",
22
+ :consistent_read => true,
23
+ :read_capacity => 10,
24
+ :write_capacity => 5,
25
+ :raise_errors => false
26
+ }
27
+ end
28
+
29
+ let(:expected_file_opts) do
30
+ {
31
+ :consistent_read => true,
32
+ :AWS_ACCESS_KEY_ID => 'FakeKey',
33
+ :AWS_REGION => 'New York',
34
+ :table_name => 'NewTable',
35
+ :table_key => 'Somekey',
36
+ :AWS_SECRET_ACCESS_KEY => 'Secret'
37
+ }
38
+ end
39
+
40
+ let(:runtime_options) do
41
+ {
42
+ :table_name => "SessionTable",
43
+ :table_key => "session_id_stuff"
44
+ }
45
+ end
46
+
47
+ def expected_options(opts)
48
+ cfg = AWS::SessionStore::DynamoDB::Configuration.new(opts)
49
+ expected_opts = defaults.merge(expected_file_opts).merge(opts)
50
+ cfg.to_hash.should include(expected_opts)
51
+ end
52
+
53
+ context "Configuration Tests" do
54
+ it "configures option with out runtime,YAML or ENV options" do
55
+ cfg = AWS::SessionStore::DynamoDB::Configuration.new
56
+ cfg.to_hash.should include(defaults)
57
+ end
58
+
59
+ it "configures accurate option hash with runtime options, no YAML or ENV" do
60
+ cfg = AWS::SessionStore::DynamoDB::Configuration.new(runtime_options)
61
+ expected_opts = defaults.merge(runtime_options)
62
+ cfg.to_hash.should include(expected_opts)
63
+ end
64
+
65
+ it "merge YAML and runtime options giving runtime precendence" do
66
+ config_path = File.dirname(__FILE__) + '/app_config.yml'
67
+ runtime_opts = {:config_file => config_path}.merge(runtime_options)
68
+ expected_options(runtime_opts)
69
+ end
70
+
71
+ it "loads options from YAML file based on Rails environment" do
72
+ rails = double('Rails', {:env => 'test', :root => ''})
73
+ stub_const("Rails", rails)
74
+ config_path = File.dirname(__FILE__) + '/rails_app_config.yml'
75
+ runtime_opts = {:config_file => config_path}.merge(runtime_options)
76
+ expected_options(runtime_opts)
77
+ end
78
+
79
+ it "has rails defiend but no file specified, no error thrown" do
80
+ rails = double('Rails', {:env => 'test', :root => ''})
81
+ stub_const("Rails", rails)
82
+ cfg = AWS::SessionStore::DynamoDB::Configuration.new(runtime_options)
83
+ expected_opts = defaults.merge(runtime_options)
84
+ cfg.to_hash.should include(expected_opts)
85
+ end
86
+
87
+ it "has rails defiend but and default rails YAML file loads" do
88
+ rails = double('Rails', {:env => 'test', :root => File.dirname(__FILE__)})
89
+ stub_const("Rails", rails)
90
+ cfg = AWS::SessionStore::DynamoDB::Configuration.new(runtime_options)
91
+ expected_opts = defaults.merge(runtime_options)
92
+ cfg.to_hash.should include(expected_opts)
93
+ end
94
+
95
+ it "throws an exception when wrong path for file" do
96
+ config_path = 'Wrong path!'
97
+ runtime_opts = {:config_file => config_path}.merge(runtime_options)
98
+ expect{cfg = AWS::SessionStore::DynamoDB::Configuration.new(runtime_opts)}.to raise_error(Errno::ENOENT)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,62 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+
15
+ require 'spec_helper'
16
+
17
+ describe AWS::SessionStore::DynamoDB do
18
+ include Rack::Test::Methods
19
+
20
+ instance_exec(&ConstantHelpers)
21
+
22
+ before do
23
+ @options = { :dynamo_db_client => client, :secret_key => 'meltingbutter' }
24
+ end
25
+
26
+ let(:base_app) { MultiplierApplication.new }
27
+ let(:app) { AWS::SessionStore::DynamoDB::RackMiddleware.new(base_app, @options) }
28
+ let(:client) { double('AWS::DynamoDB::Client') }
29
+
30
+ context "Error handling for Rack Middleware with default error handler" do
31
+ it "raises error for missing secret key" do
32
+ client.stub(:update_item).and_raise(missing_key_error)
33
+ lambda { get "/" }.should raise_error(missing_key_error)
34
+ end
35
+
36
+ it "catches exception for inaccurate table name and raises error " do
37
+ client.stub(:update_item).and_raise(resource_error)
38
+ lambda { get "/" }.should raise_error(resource_error)
39
+ end
40
+
41
+ it "catches exception for inaccurate table key" do
42
+ client.stub(:update_item).and_raise(key_error)
43
+ client.stub(:get_item).and_raise(key_error)
44
+ get "/"
45
+ last_request.env["rack.errors"].string.should include(key_error_msg)
46
+ end
47
+ end
48
+
49
+ context "Test ExceptionHandler with true as return value for handle_error" do
50
+ it "raises all errors" do
51
+ @options[:raise_errors] = true
52
+ client.stub(:update_item).and_raise(client_error)
53
+ lambda { get "/" }.should raise_error(client_error)
54
+ end
55
+
56
+ it "catches exception for inaccurate table key and raises error" do
57
+ @options[:raise_errors] = true
58
+ client.stub(:update_item).and_raise(key_error)
59
+ lambda { get "/" }.should raise_error(key_error)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,156 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'spec_helper'
15
+
16
+ describe AWS::SessionStore::DynamoDB::GarbageCollection do
17
+ def member(min,max)
18
+ member = []
19
+ for i in min..max
20
+ member << {"session_id"=>{:s=>"#{i}"}}
21
+ end
22
+ member
23
+ end
24
+
25
+ def format_scan_result
26
+ member = []
27
+ for i in 31..49
28
+ member << {"session_id"=>{:s=>"#{i}"}}
29
+ end
30
+
31
+ member.inject([]) do |rqst_array, item|
32
+ rqst_array << {:delete_request => {:key => item}}
33
+ rqst_array
34
+ end
35
+ end
36
+
37
+ def collect_garbage
38
+ options = { :dynamo_db_client => dynamo_db_client, :max_age => 100, :max_stale => 100 }
39
+ AWS::SessionStore::DynamoDB::GarbageCollection.collect_garbage(options)
40
+ end
41
+
42
+ let(:scan_resp1){
43
+ resp = {
44
+ :member => member(0, 49),
45
+ :count => 50,
46
+ :scanned_count => 1000,
47
+ :last_evaluated_key => {}
48
+ }
49
+ }
50
+
51
+ let(:scan_resp2){
52
+ {
53
+ :member => member(0, 31),
54
+ :last_evaluated_key => {"session_id"=>{:s=>"31"}}
55
+ }
56
+ }
57
+
58
+ let(:scan_resp3){
59
+ {
60
+ :member => member(31,49),
61
+ :last_evaluated_key => {}
62
+ }
63
+ }
64
+
65
+ let(:write_resp1){
66
+ {
67
+ :unprocessed_items => {}
68
+ }
69
+ }
70
+
71
+ let(:write_resp2){
72
+ {
73
+ :unprocessed_items => {
74
+ "sessions" => [
75
+ {
76
+ :delete_request => {
77
+ :key => {
78
+ "session_id" =>
79
+ {
80
+ :s => "1"
81
+ }
82
+ }
83
+ }
84
+ },
85
+ {
86
+ :delete_request => {
87
+ :key => {
88
+ "session_id" =>
89
+ {
90
+ :s => "17"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ }
99
+
100
+ let(:dynamo_db_client) {AWS::DynamoDB::Client.new}
101
+
102
+ context "Mock DynamoDB client with garbage collection" do
103
+
104
+ it "processes scan result greater than 25 and deletes in batches of 25" do
105
+ dynamo_db_client.should_receive(:scan).
106
+ exactly(1).times.and_return(scan_resp1)
107
+ dynamo_db_client.should_receive(:batch_write_item).
108
+ exactly(2).times.and_return(write_resp1)
109
+ collect_garbage
110
+ end
111
+
112
+ it "gets scan results then returns last evaluated key and resumes scanning" do
113
+ dynamo_db_client.should_receive(:scan).
114
+ exactly(2).times.and_return(scan_resp2, scan_resp3)
115
+ dynamo_db_client.should_receive(:batch_write_item).
116
+ exactly(3).times.and_return(write_resp1)
117
+ collect_garbage
118
+ end
119
+
120
+ it "it formats unprocessed_items and then batch deletes them" do
121
+ dynamo_db_client.should_receive(:scan).
122
+ exactly(1).times.and_return(scan_resp3)
123
+ dynamo_db_client.should_receive(:batch_write_item).ordered.
124
+ with(:request_items => {"sessions" => format_scan_result}).
125
+ and_return(write_resp2)
126
+ dynamo_db_client.should_receive(:batch_write_item).ordered.with(
127
+ :request_items =>
128
+ {
129
+ "sessions" => [
130
+ {
131
+ :delete_request => {
132
+ :key => {
133
+ "session_id" =>
134
+ {
135
+ :s => "1"
136
+ }
137
+ }
138
+ }
139
+ },
140
+ {
141
+ :delete_request => {
142
+ :key => {
143
+ "session_id" =>
144
+ {
145
+ :s => "17"
146
+ }
147
+ }
148
+ }
149
+ }
150
+ ]
151
+ }
152
+ ).and_return(write_resp1)
153
+ collect_garbage
154
+ end
155
+ end
156
+ end