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,45 @@
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::SessionStore::DynamoDB::Errors
16
+ # BaseErrorHandler provides an interface for error handlers
17
+ # that can be passed in to {AWS::SessionStore::DynamoDB::RackMiddleware}.
18
+ # Each error handler must implement a handle_error method.
19
+ #
20
+ # @example Sample ErrorHandler class
21
+ # class MyErrorHandler < BaseErrorHandler
22
+ # # Handles error passed in
23
+ # def handle_error(e, env = {})
24
+ # File.open(path_to_file, 'w') {|f| f.write(e.message) }
25
+ # false
26
+ # end
27
+ # end
28
+ class BaseHandler
29
+ # An error and an environment (optionally) will be passed in to
30
+ # this method and it will determine how to deal
31
+ # with the error.
32
+ # Must return false if you have handled the error but are not reraising the
33
+ # error up the stack.
34
+ # You may reraise the error passed.
35
+ #
36
+ # @param [AWS::DynamoDB::Errors::Base] error error passed in from
37
+ # AWS::SessionStore::DynamoDB::RackMiddleware.
38
+ # @param [Rack::Request::Environment,nil] env Rack environment
39
+ # @return [false] If exception was handled and will not reraise exception.
40
+ # @raise [AWS::DynamoDB::Errors] If error has be reraised.
41
+ def handle_error(error, env = {})
42
+ raise NotImplementedError
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
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::SessionStore::DynamoDB::Errors
16
+ # This class handles errors raised from DynamoDB.
17
+ class DefaultHandler < AWS::SessionStore::DynamoDB::Errors::BaseHandler
18
+ # Array of errors that will always be passed up the Rack stack.
19
+ HARD_ERRORS = [
20
+ AWS::DynamoDB::Errors::ResourceNotFoundException,
21
+ AWS::DynamoDB::Errors::ConditionalCheckFailedException,
22
+ AWS::SessionStore::DynamoDB::MissingSecretKeyError,
23
+ AWS::SessionStore::DynamoDB::LockWaitTimeoutError
24
+ ]
25
+
26
+ # Determines behavior of DefaultErrorHandler
27
+ # @param [true] raise_errors Pass all errors up the Rack stack.
28
+ def initialize(raise_errors)
29
+ @raise_errors = raise_errors
30
+ end
31
+
32
+ # Raises {HARD_ERRORS} up the Rack stack.
33
+ # Places all other errors in Racks error stream.
34
+ def handle_error(error, env = {})
35
+ if HARD_ERRORS.include?(error.class) || @raise_errors
36
+ raise error
37
+ else
38
+ store_error(error, env)
39
+ false
40
+ end
41
+ end
42
+
43
+ # Sends error to error stream
44
+ def store_error(error, env = {})
45
+ env["rack.errors"].puts(errors_string(error)) if env
46
+ end
47
+
48
+ # Returns string to be placed in error stream
49
+ def errors_string(error)
50
+ str = []
51
+ str << "Exception occurred: #{error.message}"
52
+ str << "Stack trace:"
53
+ str += error.backtrace.map {|l| " " + l }
54
+ str.join("\n")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,128 @@
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 'aws-sdk'
15
+
16
+ module AWS::SessionStore::DynamoDB
17
+ # Collects and deletes unwanted sessions based on
18
+ # their creation and update dates.
19
+ module GarbageCollection
20
+ module_function
21
+
22
+ # Scans DynamoDB session table to find
23
+ # sessions that match the max age and max stale period
24
+ # requirements. it then deletes all of the found sessions.
25
+ def collect_garbage(options = {})
26
+ config = load_config(options)
27
+ last_key = eliminate_unwanted_sessions(config)
28
+ while !last_key.empty?
29
+ last_key = eliminate_unwanted_sessions(config, last_key)
30
+ end
31
+ end
32
+
33
+ # Loads configuration options.
34
+ # @option (see Configuration#initialize)
35
+ # @api private
36
+ def load_config(options = {})
37
+ AWS::SessionStore::DynamoDB::Configuration.new(options)
38
+ end
39
+
40
+ # Sets scan filter attributes based on attributes specified.
41
+ # @api private
42
+ def scan_filter(config)
43
+ hash = {}
44
+ hash['created_at'] = oldest_date(config.max_age) if config.max_age
45
+ hash['updated_at'] = oldest_date(config.max_stale) if config.max_stale
46
+ { :scan_filter => hash }
47
+ end
48
+
49
+ # Scans and deletes batch.
50
+ # @api private
51
+ def eliminate_unwanted_sessions(config, last_key = nil)
52
+ scan_result = scan(config, last_key)
53
+ batch_delete(config, scan_result[:member])
54
+ scan_result[:last_evaluated_key] || {}
55
+ end
56
+
57
+ # Scans the table for sessions matching the max age and
58
+ # max stale time specified.
59
+ # @api private
60
+ def scan(config, last_item = nil)
61
+ options = scan_opts(config)
62
+ options.merge(start_key(last_item)) if last_item
63
+ config.dynamo_db_client.scan(options)
64
+ end
65
+
66
+ # Deletes the batch gotten from the scan result.
67
+ # @api private
68
+ def batch_delete(config, items)
69
+ begin
70
+ subset = items.shift(25)
71
+ sub_batch = write(subset)
72
+ process!(config, sub_batch)
73
+ end until subset.empty?
74
+ end
75
+
76
+ # Turns array into correct format to be passed in to
77
+ # a delete request.
78
+ # @api private
79
+ def write(sub_batch)
80
+ sub_batch.inject([]) do |rqst_array, item|
81
+ rqst_array << {:delete_request => {:key => item}}
82
+ rqst_array
83
+ end
84
+ end
85
+
86
+ # Proccesses pending request items.
87
+ # @api private
88
+ def process!(config, sub_batch)
89
+ return if sub_batch.empty?
90
+ opts = {}
91
+ opts[:request_items] = {config.table_name => sub_batch}
92
+ begin
93
+ response = config.dynamo_db_client.batch_write_item(opts)
94
+ opts[:request_items] = response[:unprocessed_items]
95
+ end until opts[:request_items].empty?
96
+ end
97
+
98
+ # Provides scan options.
99
+ # @api private
100
+ def scan_opts(config)
101
+ table_opts(config).merge(scan_filter(config))
102
+ end
103
+
104
+ # Provides table options
105
+ # @api private
106
+ def table_opts(config)
107
+ {
108
+ :table_name => config.table_name,
109
+ :attributes_to_get => [config.table_key]
110
+ }
111
+ end
112
+
113
+ # @return [Hash] Hash with specified date attributes.
114
+ # @api private
115
+ def oldest_date(sec)
116
+ hash = {}
117
+ hash[:attribute_value_list] = [:n => "#{((Time.now - sec).to_f)}"]
118
+ hash[:comparison_operator] = 'LT'
119
+ hash
120
+ end
121
+
122
+ # Provides start key.
123
+ # @api private
124
+ def start_key(last_item)
125
+ { :exclusive_start_key => last_item }
126
+ end
127
+ end
128
+ end
@@ -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::SessionStore::DynamoDB
16
+ class InvalidIDError < RuntimeError
17
+ def initialize(msg = "Corrupt Session ID!")
18
+ super
19
+ end
20
+ end
21
+ end
@@ -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::SessionStore::DynamoDB
16
+ class LockWaitTimeoutError < RuntimeError
17
+ def initialize(msg = 'Maximum time spent to acquire lock has been exceeded!')
18
+ super
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,162 @@
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::SessionStore::DynamoDB::Locking
16
+ # This class provides a framework for implementing
17
+ # locking strategies.
18
+ class Base
19
+
20
+ # Creates configuration object.
21
+ def initialize(cfg)
22
+ @config = cfg
23
+ end
24
+
25
+ # Updates session in database
26
+ def set_session_data(env, sid, session, options = {})
27
+ return false if session.empty?
28
+ packed_session = pack_data(session)
29
+ handle_error(env) do
30
+ save_opts = update_opts(env, sid, packed_session, options)
31
+ result = @config.dynamo_db_client.update_item(save_opts)
32
+ sid
33
+ end
34
+ end
35
+
36
+ # Packs session data.
37
+ def pack_data(data)
38
+ [Marshal.dump(data)].pack("m*")
39
+ end
40
+
41
+ # Gets session data.
42
+ def get_session_data(env, sid)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ # Deletes session based on id
47
+ def delete_session(env, sid)
48
+ handle_error(env) do
49
+ @config.dynamo_db_client.delete_item(delete_opts(sid))
50
+ end
51
+ end
52
+
53
+ # Each database operation is placed in this rescue wrapper.
54
+ # This wrapper will call the method, rescue any exceptions and then pass
55
+ # exceptions to the configured error handler.
56
+ def handle_error(env = nil, &block)
57
+ begin
58
+ yield
59
+ rescue AWS::DynamoDB::Errors::Base => e
60
+ @config.error_handler.handle_error(e, env)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # @return [Hash] Options for deleting session.
67
+ def delete_opts(sid)
68
+ merge_all(table_opts(sid), expected_attributes(sid))
69
+ end
70
+
71
+ # @return [Hash] Options for updating item in Session table.
72
+ def update_opts(env, sid, session, options = {})
73
+ if env['dynamo_db.new_session']
74
+ updt_options = save_new_opts(env, sid, session)
75
+ else
76
+ updt_options = save_exists_opts(env, sid, session, options)
77
+ end
78
+ updt_options
79
+ end
80
+
81
+ # @return [Hash] Options for saving a new session in database.
82
+ def save_new_opts(env, sid, session)
83
+ attribute_opts = attr_updts(env, session, created_attr)
84
+ merge_all(table_opts(sid), attribute_opts)
85
+ end
86
+
87
+ # @return [Hash] Options for saving an existing sesison in the database.
88
+ def save_exists_opts(env, sid, session, options = {})
89
+ add_attr = options[:add_attrs] || {}
90
+ expected = options[:expect_attr] || {}
91
+ attribute_opts = merge_all(attr_updts(env, session, add_attr), expected)
92
+ merge_all(table_opts(sid), attribute_opts)
93
+ end
94
+
95
+ # Unmarshal the data.
96
+ def unpack_data(packed_data)
97
+ Marshal.load(packed_data.unpack("m*").first)
98
+ end
99
+
100
+ # Table options for client.
101
+ def table_opts(sid)
102
+ {
103
+ :table_name => @config.table_name,
104
+ :key => {@config.table_key => {:s => sid}}
105
+ }
106
+ end
107
+
108
+ # Attributes to update via client.
109
+ def attr_updts(env, session, add_attrs = {})
110
+ data = data_unchanged?(env, session) ? {} : data_attr(session)
111
+ {
112
+ :attribute_updates => merge_all(updated_attr, data, add_attrs),
113
+ :return_values => "UPDATED_NEW"
114
+ }
115
+ end
116
+
117
+ # Update client with current time attribute.
118
+ def updated_at
119
+ { :value => {:n => "#{(Time.now).to_f}"}, :action => "PUT" }
120
+ end
121
+
122
+ # Attribute for creation of session.
123
+ def created_attr
124
+ { "created_at" => updated_at }
125
+ end
126
+
127
+ # Attribute for updating session.
128
+ def updated_attr
129
+ {
130
+ "updated_at" => updated_at
131
+ }
132
+ end
133
+
134
+ def data_attr(session)
135
+ { "data" => {:value => {:s => session}, :action => "PUT"} }
136
+ end
137
+
138
+ # Determine if data has been manipulated
139
+ def data_unchanged?(env, session)
140
+ return false unless env['rack.initial_data']
141
+ env['rack.initial_data'] == session
142
+ end
143
+
144
+ # Expected attributes
145
+ def expected_attributes(sid)
146
+ { :expected => {@config.table_key => {:value => {:s => sid}, :exists => true}} }
147
+ end
148
+
149
+ # Attributes to be retrieved via client
150
+ def attr_opts
151
+ {:attributes_to_get => ["data"],
152
+ :consistent_read => @config.consistent_read}
153
+ end
154
+
155
+ # @return [Hash] merged hash of all hashes passed in.
156
+ def merge_all(*hashes)
157
+ new_hash = {}
158
+ hashes.each{|hash| new_hash.merge!(hash)}
159
+ new_hash
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,40 @@
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::SessionStore::DynamoDB::Locking
16
+ # This class gets and sets sessions
17
+ # without a locking strategy.
18
+ class Null < AWS::SessionStore::DynamoDB::Locking::Base
19
+ # Retrieve session if it exists from the database by id.
20
+ # Unpack the data once retrieved from the database.
21
+ def get_session_data(env, sid)
22
+ handle_error(env) do
23
+ result = @config.dynamo_db_client.get_item(get_session_opts(sid))
24
+ extract_data(env, result)
25
+ end
26
+ end
27
+
28
+ # @return [Hash] Options for getting session.
29
+ def get_session_opts(sid)
30
+ merge_all(table_opts(sid), attr_opts)
31
+ end
32
+
33
+ # @return [String] Session data.
34
+ def extract_data(env, result = nil)
35
+ env['rack.initial_data'] = result[:item]["data"][:s] if result[:item]
36
+ unpack_data(result[:item]["data"][:s]) if result[:item]
37
+ end
38
+
39
+ end
40
+ end