aws-sessionstore-dynamodb 0.5.0

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