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