right_sharding 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Right Sharding
2
+
3
+ Right Sharding is a Rack Middleware that makes requests aware of RightScale shards.
4
+ See: https://wookiee.rightscale.com/display/rightscale/Sharding
5
+
6
+ It currently supports RightSite and Right API requests.
7
+
8
+ Maintained by the RightScale Cornsilk team.
9
+
10
+ ## Setup
11
+
12
+ Right Sharding uses rconf to install bundler.
13
+
14
+ Install latest rconf:
15
+ $ gem install rconf
16
+
17
+ Run rconf:
18
+ $ rconf
19
+
20
+ ## Testing
21
+
22
+ ### Unit Testing
23
+
24
+ Run rspec:
25
+ $ bundle exec spec spec/
@@ -0,0 +1,53 @@
1
+ basedir = File.dirname(__FILE__)
2
+
3
+ #Require the core suite of RightSharding classes and modules
4
+ require File.join(basedir, "right_sharding", "routing")
5
+ require File.join(basedir, "right_sharding", "base_proxy")
6
+ require File.join(basedir, "right_sharding", "current_account")
7
+ require File.join(basedir, "right_sharding", "instance_api_proxy")
8
+ require File.join(basedir, "right_sharding", "shard_handler")
9
+ require File.join(basedir, "right_sharding", "user_api_proxy")
10
+ require File.join(basedir, "right_sharding", 'extensions', 'core')
11
+
12
+
13
+ module RightSharding
14
+ def self.activate(config, options = {})
15
+ # Ensure that this super-important module attribute is set before any init code attempts to use it!
16
+ RightSharding::Routing.skip_shard_routing_callback = options.delete(:skip_shard_routing_callback)
17
+
18
+ # The library uses only the CurrentAccount middleware.
19
+ current_account_only = options.delete(:current_account_only)
20
+
21
+ # Add middleware to update the global_session account based on received headers/params.
22
+ # Must come after GlobalSession.
23
+ config.middleware.insert_after ::Rack::MethodOverride, ::Rack::CurrentAccount, options
24
+
25
+ unless current_account_only
26
+ # Add the middleware to intercept the requests and reroute users to the appropriate shards
27
+ # It must come after GlobalSession and CurrentAccount; GlobalSession will be used for the
28
+ # legacy path.
29
+ config.middleware.insert_after ::Rack::CurrentAccount, ::Rack::ShardRouting, options
30
+
31
+ # Add the middleware to proxy the instance facing api to the appropriate shards
32
+ config.middleware.insert_before ::Rack::ShardRouting, ::Rack::ShardInstanceApiProxy, options
33
+
34
+ # This will proxy the specific accounts passed in the options variable.
35
+ config.middleware.insert_after(::Rack::ShardInstanceApiProxy, ::Rack::ShardUserApiProxy, options)
36
+ end
37
+ rescue NoMethodError => e
38
+ raise NoMethodError, "GlobalSession must be used first."
39
+ end
40
+ end
41
+
42
+
43
+ # HACK: monkey patch to ensure no extra headers get added
44
+ # do not allow Rest-Client default_headers to add 'application/xml'
45
+ # See: https://github.com/rest-client/rest-client/blob/v1.6.1/lib/restclient/request.rb#L279
46
+ require 'rest_client'
47
+ module RestClient
48
+ class Request
49
+ def default_headers
50
+ {}
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,204 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
15
+
16
+ module RightSharding
17
+ module Rack
18
+ module Middleware
19
+
20
+ class BaseProxy
21
+ REST_CLIENT_DEFAULT_TIMEOUT = 1000 # max time to read a response in seconds
22
+ REST_CLIENT_DEFAULT_OPEN_TIMEOUT = 1000 # max time to open a connection in seconds
23
+ class BaseProxyYield < StandardError; end
24
+
25
+ class BaseProxyException < StandardError
26
+ attr_reader :code, :headers, :body
27
+
28
+ def initialize(code, headers, body)
29
+ @env = nil # subclass needs to set this
30
+ @code = code
31
+ @headers = headers
32
+ @body = body
33
+
34
+ super(body)
35
+ end
36
+ end
37
+
38
+ #-----------------------------------------------------------------------
39
+ # Rack Middleware
40
+ #-----------------------------------------------------------------------
41
+
42
+ def initialize(app, params = {})
43
+ @app = app
44
+
45
+ @config_object = Module.const_get(params[:config_object])
46
+ @shard_object = Module.const_get(params[:shard_object])
47
+ @sharding_object = Module.const_get(params[:sharding_object])
48
+ @logger = Module.const_get(params[:logger])
49
+ end
50
+
51
+ # Subclass needs to implement
52
+ # def call(env)
53
+
54
+ #-----------------------------------------------------------------------
55
+ # Getters
56
+ #-----------------------------------------------------------------------
57
+
58
+ def get_this_shard_id
59
+ RightSharding::ShardHandler.fetch_this_shard_id(@config_object)
60
+ end
61
+
62
+ def get_shard_from_shard_id(shard_id)
63
+ RightSharding::ShardHandler.fetch_shard(@shard_object, shard_id)
64
+ end
65
+
66
+ #-----------------------------------------------------------------------
67
+ # General Helper
68
+ #-----------------------------------------------------------------------
69
+
70
+ def proxy_request_to_the_destinated_shard(hostname, options={})
71
+ request = get_request
72
+
73
+ rest_options = {}
74
+ rest_options[:url] = "#{request.protocol}#{hostname}:#{request.port}#{request.request_uri}"
75
+ rest_options[:method] = request.method
76
+ rest_options[:headers] = request.get_headers_for_proxy
77
+ rest_options[:payload] = request.get_payload_for_proxy if request.get_payload_for_proxy
78
+ rest_options[:cookies] = request.get_cookies_for_proxy if request.get_cookies_for_proxy
79
+ rest_options[:timeout] = REST_CLIENT_DEFAULT_TIMEOUT
80
+ rest_options[:open_timeout] = REST_CLIENT_DEFAULT_OPEN_TIMEOUT
81
+
82
+ shard_proxy_logger(:info, "#{self.class}: Proxying the request to '#{hostname}' with url: #{rest_options[:url]}")
83
+
84
+ response = RestClient::Request.execute(rest_options)
85
+ if options[:proxy_global_session]
86
+ set_global_session_from_response(response)
87
+ end
88
+
89
+ [response.code, get_rack_format_response_headers(response), response.to_s]
90
+ rescue URI::InvalidURIError, SocketError, Errno::ECONNREFUSED, GlobalSession::ClientError => e
91
+ shard_proxy_logger(:error, "#{self.class}: failed: #{e.class} proxy request to url: #{rest_options[:url]}\n" + e.message + "\n " + e.backtrace.join("\n "))
92
+ [503, {}, "Unable to proxy the request to the destination '#{hostname}'. #{e.message}"]
93
+ rescue RestClient::Exception => e
94
+ shard_proxy_logger(:error, "#{self.class}: failed: #{e.class} proxy request to url: #{rest_options[:url]}\n" + e.message + "\n " + e.backtrace.join("\n "))
95
+ [e.http_code, get_rack_format_response_headers(e.response), e.response.to_s]
96
+ end
97
+
98
+ #-----------------------------------------------------------------------
99
+ # Request Helper
100
+ #-----------------------------------------------------------------------
101
+
102
+ def get_request
103
+ request = ActionController::Request.new(@env)
104
+
105
+ override_request_ssl(request)
106
+
107
+ define_get_headers_for_proxy(request)
108
+ define_get_payload_for_proxy(request)
109
+ define_get_cookies_for_proxy(request)
110
+
111
+ request
112
+ end
113
+
114
+ def override_request_ssl(request)
115
+ if (@http_proto && @http_proto == "http") || (@env['HTTP_X_FORWARDED_PROTO'] && @env['HTTP_X_FORWARDED_PROTO'].split(',')[0] == "http")
116
+ def request.ssl?; false; end
117
+ else
118
+ def request.ssl?; true; end
119
+ end
120
+ end
121
+
122
+ def define_get_headers_for_proxy(request)
123
+ def request.get_headers_for_proxy
124
+ pheaders = {}
125
+
126
+ # preserve headers
127
+ headers.each {|key, value| pheaders[$1] = value if key =~ /^HTTP_(.*)/}
128
+
129
+ # Send the 'X_FORWARDED_FOR' that we received on this server so that IP Whitelisting can check against that
130
+ # Prevent spoofing by calcuating the HMAC using a shared secret (in this case, the library_auth_key because
131
+ # we didn't want to add a new shared secret for this temporary proxy stuff)
132
+ forwarded_for_hmac = OpenSSL::HMAC.hexdigest('sha1', ::LIBRARY_AUTH_KEY, pheaders["X_FORWARDED_FOR"] || "")
133
+ pheaders["X_RS_PROXY_FORWARDED_FOR"] = "#{pheaders["X_FORWARDED_FOR"]};#{forwarded_for_hmac}"
134
+
135
+ # chain x_forwarded_for
136
+ pheaders["X_FORWARDED_FOR"] = [pheaders["X_FORWARDED_FOR"], self.ip].join(",") unless self.ip.blank?
137
+
138
+ pheaders
139
+ end
140
+ end
141
+
142
+ def define_get_payload_for_proxy(request)
143
+ def request.get_payload_for_proxy
144
+ raw_post.blank? ? nil : raw_post
145
+ end
146
+ end
147
+
148
+ def define_get_cookies_for_proxy(request)
149
+ def request.get_cookies_for_proxy
150
+ if headers["rack.request.cookie_hash"].blank?
151
+ nil
152
+ else
153
+ headers["rack.request.cookie_hash"].reject {|k, v| k !~ /rs_gbl|session_id/}
154
+ end
155
+ end
156
+ end
157
+
158
+ #-----------------------------------------------------------------------
159
+ # Response Helper
160
+ #-----------------------------------------------------------------------
161
+
162
+ def get_rack_format_response_headers(response)
163
+ response.raw_headers.inject({}) do |out, (key, value)|
164
+ out[key] = %w{ set-cookie }.include?(key.downcase) ? value : value.first
165
+ out
166
+ end
167
+ end
168
+
169
+ def set_global_session_from_response(response)
170
+ # Get the response's global session cookie if it exists
171
+ cookie_name = @env['global_session'].directory.configuration['cookie']['name']
172
+ cookie = CGI.unescape(response.cookies[cookie_name]) if response.cookies[cookie_name] # unescape expects a string
173
+ @env['global_session'] = @env['global_session'].directory.create_session(cookie)
174
+ end
175
+
176
+ #-----------------------------------------------------------------------
177
+ # URI Parsing Helper
178
+ #-----------------------------------------------------------------------
179
+
180
+ def get_api_version
181
+ request = ActionController::Request.new(@env)
182
+
183
+ if !request.parameters.blank? && !request.parameters[:api_version].blank?
184
+ api_version = request.parameters[:api_version]
185
+ elsif !@env["HTTP_X_API_VERSION"].blank?
186
+ api_version = @env["HTTP_X_API_VERSION"]
187
+ end
188
+
189
+ return (!api_version.blank? ? api_version.to_f : nil)
190
+ end
191
+
192
+ #-----------------------------------------------------------------------
193
+ # Logging Helper
194
+ #-----------------------------------------------------------------------
195
+
196
+ def shard_proxy_logger(level, message)
197
+ @logger.send(level, "#{message}")
198
+ end
199
+
200
+ end
201
+
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,68 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
2
+
3
+ module RightSharding
4
+ module Rack
5
+ module Middleware
6
+
7
+ class CurrentAccount
8
+ attr_reader :app
9
+ attr_reader :config_object, :logger
10
+ #-----------------------------------------------------------------------
11
+ # Rack Middleware
12
+ #-----------------------------------------------------------------------
13
+
14
+ def initialize(app, params = {})
15
+ @app = app
16
+
17
+ @config_object = Module.const_get(params[:config_object])
18
+ @logger = Module.const_get(params[:logger])
19
+ end
20
+
21
+ # TODO Current this replaces the account id set in env["global_session"]["account"]
22
+ # ultimately, we want this middleware to simply determine the correct account and set it
23
+ # in an env variable such as request.env['rightscale.current_account']. Once we refactor all
24
+ # the apps to use that instead, we can remove the use of global_session['account']
25
+ def call(env)
26
+ reset_global_session_account(env)
27
+ return @app.call(env)
28
+ end
29
+
30
+ # Passing a X_ACCOUNT header or account query parameter can override
31
+ # the account provided in the global_session cookie. The X_ACCOUNT header
32
+ # or account query parameter must be the id of the account, and not the href.
33
+ #
34
+ # Order of precedence:
35
+ # "X_ACCOUNT" header > "account" query parameter > global_session
36
+ #
37
+ def reset_global_session_account(env)
38
+ if env['HTTP_X_ACCOUNT']
39
+ account_id = env['HTTP_X_ACCOUNT']
40
+ from = 'header'
41
+ elsif env['QUERY_STRING']
42
+ query_params = ::Rack::Utils.parse_query(env['QUERY_STRING'])
43
+ if query_params['account']
44
+ account_id = query_params['account']
45
+ from = 'query string'
46
+ end
47
+ end
48
+
49
+ # Only reset the account_id if it exists, is numeric, and is different from what's
50
+ # currently in the global session
51
+ if account_id && account_id =~ /\A\d+\Z/ && account_id.to_i != env["global_session"]["account"]
52
+ logger.info("RightSharding::CurrentAccount: Resetting global_session account: " +
53
+ "#{env["global_session"]["account"]} -> #{account_id} (#{from})")
54
+ env["global_session"]["account"] = account_id.to_i
55
+ end
56
+ true
57
+ end
58
+
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ module Rack
65
+ unless defined?(::Rack::CurrentAccount)
66
+ CurrentAccount = ::RightSharding::Rack::Middleware::CurrentAccount
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+
2
+ # Custom core patches for Ruby 1.8.x
3
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("1.9")
4
+
5
+ # Ruby 1.8.x doesn't provide an Array#sample method, so alias it to Array#choice
6
+ class Array
7
+ alias_method :sample, :choice
8
+ end
9
+
10
+ end
@@ -0,0 +1,121 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
15
+
16
+ module RightSharding
17
+ module Rack
18
+ module Middleware
19
+
20
+ class InstanceApiProxy < BaseProxy
21
+
22
+ class InstanceApiProxyYield < BaseProxyYield; end
23
+
24
+ class InstanceApiProxyException < BaseProxyException; end
25
+
26
+ #-----------------------------------------------------------------------
27
+ # Rack Middleware
28
+ #-----------------------------------------------------------------------
29
+
30
+ def initialize(app, params = {})
31
+ super
32
+ @http_proto = params[:http_proto]
33
+ end
34
+
35
+ def call(env)
36
+ @env = env
37
+
38
+ if is_instance_api?(1.0)
39
+ unless token = get_instance_api_token_from_uri
40
+ raise InstanceApiProxyYield, "Unable to extract the token out: '#{@env["PATH_INFO"]}'"
41
+ end
42
+
43
+ unless shard_info = get_shard_info_from_database(token)
44
+ shard_info = get_shard_info_from_right_net(token)
45
+ end
46
+
47
+ unless shard_info.blank?
48
+ if shard_info[:shard_id] != get_this_shard_id
49
+ # the account is migrated
50
+ return proxy_request_to_the_destinated_shard(shard_info[:shard_hostname])
51
+ end
52
+ else
53
+ # we are here when the account/shard cannot be found or the account is disabled in every shard, we'll pass on and let the app to deal with it
54
+ raise InstanceApiProxyYield, "Unable to find the account/shard or the account is disabled: '#{@env["PATH_INFO"]}'"
55
+ end
56
+ end
57
+
58
+ # everything is good
59
+ return @app.call(env)
60
+ rescue InstanceApiProxyYield => e
61
+ shard_proxy_logger(:error, e.message)
62
+ return @app.call(env)
63
+ rescue InstanceApiProxyException => e
64
+ shard_proxy_logger(:error, "failed: #{e.class} " + e.message + "\n " + e.backtrace.join("\n "))
65
+ return [e.code, e.headers, e.body]
66
+ end
67
+
68
+ #-----------------------------------------------------------------------
69
+ # Getters
70
+ #-----------------------------------------------------------------------
71
+
72
+ def get_shard_info_from_database(token)
73
+ RightSharding::ShardHandler.fetch_instance_api_token_shard_id_and_hostname(token)
74
+ end
75
+
76
+ def get_shard_id_from_right_net(token)
77
+ RightNetData.get_instance(:auth_token => token)
78
+ rescue RightSupport::Net::NoResult => e
79
+ raise InstanceApiProxyException.new(503, {}, "Unable to retrieve the instance from the given token")
80
+ rescue RestClient::Exception => e
81
+ raise InstanceApiProxyException.new(e.response.code, get_rack_format_response_headers(e.response), e.response.to_s)
82
+ end
83
+
84
+ def get_shard_info_from_right_net(token)
85
+ if instance_attributes = get_shard_id_from_right_net(token)
86
+ if !instance_attributes[:shard_id].blank? && instance_attributes[:shard_id].to_i != 0
87
+ if shard = get_shard_from_shard_id(instance_attributes[:shard_id])
88
+ {:shard_id => shard.id, :shard_hostname => shard.hostname}
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ #-----------------------------------------------------------------------
95
+ # URI Parsing Helper
96
+ #-----------------------------------------------------------------------
97
+
98
+ def is_instance_api?(version)
99
+ @env["PATH_INFO"] =~ /(?:^|\/)api\/inst\/ec2_instances\// && get_api_version == version
100
+ end
101
+
102
+ def get_instance_api_token_from_uri
103
+ @env["PATH_INFO"][/(?:^|\/)api\/inst\/ec2_instances\/([^\/]+)(?:\/|$)/, 1]
104
+ end
105
+
106
+ # Overrides BaseProxy, default is to return 1.0 as the version if an API
107
+ # version is not detected. (required for v3 instance api requests)
108
+ def get_api_version
109
+ super || 1.0
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+ end
116
+ end
117
+
118
+ module Rack
119
+ ShardInstanceApiProxy = ::RightSharding::Rack::Middleware::InstanceApiProxy unless defined?(::Rack::ShardInstanceApiProxy)
120
+ end
121
+
@@ -0,0 +1,298 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
2
+
3
+ module RightSharding
4
+ # Rails routing functionality embedded into RightSharding. The module contains public utility
5
+ # methods; most of the actual routing and redirection logic is tucked away inside the routing
6
+ # middleware that lives under this namespace.
7
+ #
8
+ # @see RightSharding::Rack::Middleware::Routing
9
+ module Routing
10
+
11
+ # mattr_accessor is Rails feature so it is not gonna work when we run "rake spec".
12
+ # # mattr_accessor :skip_shard_routing_callback
13
+ #
14
+ def self.skip_shard_routing_callback
15
+ @@skip_shard_routing_callback ||= nil
16
+ end
17
+ def self.skip_shard_routing_callback=(skip_shard_routing_callback)
18
+ @@skip_shard_routing_callback = skip_shard_routing_callback
19
+ end
20
+
21
+ module_function
22
+
23
+ # Determine whether shard routing and account-prefixing should be skipped for a given path and request method.
24
+ # This delegates to a callback that should be provided by whoever initializes the Routing middleware.
25
+ #
26
+ # @see RightSharding::Rack::Middleware::Routing#initialize
27
+ #
28
+ # @return [Boolean] true if shard routing and account-prefixing should be skipped; false otherwise
29
+ def skip_shard_routing?(path, request_method)
30
+ skip_shard_routing_callback.present? && skip_shard_routing_callback.call(path, request_method)
31
+ end
32
+
33
+ # Extract the current-account ID from a dashboard (right_site or embedded library) path.
34
+ #
35
+ # @return [String] a numeric string, or nil if the path does not include account info
36
+ def fetch_current_account_id_from_path(path)
37
+ path[/(?:^|\/)acct\/(\d+)(?:\/|$)/, 1]
38
+ end
39
+
40
+ # Prepend "/acct/X" to a URL path. Does not check if the path already contains this prefix;
41
+ # be careful!
42
+ #
43
+ # @param [String] original_path the path-info of the original URI, e.g. "/dashboard;index"
44
+ # @param [Integer] account_id the account ID to prepend to the path
45
+ # @return [String] the original_path with an account-id prefix
46
+ def add_account_to_path(original_path, account_id)
47
+ case original_path
48
+ when /(?:^|\/)api(?:\/|$)/
49
+ original_path.sub(/(^|\/)api(\/|$)/, '\1' + "api/acct/#{account_id}" + '\2')
50
+ else
51
+ "/acct/#{account_id}" + (original_path =~ /^\// ? "" : "/") + original_path
52
+ end
53
+ end
54
+ end
55
+
56
+ module Rack
57
+ module Middleware
58
+
59
+ class Routing
60
+
61
+ attr_reader :app
62
+ attr_reader :config_object, :sharding_object, :logger
63
+
64
+ #-----------------------------------------------------------------------
65
+ # Rack Middleware
66
+ #-----------------------------------------------------------------------
67
+
68
+ def initialize(app, params = {})
69
+ @app = app
70
+
71
+ @config_object = Module.const_get(params[:config_object])
72
+ @sharding_object = Module.const_get(params[:sharding_object])
73
+ @logger = Module.const_get(params[:logger])
74
+
75
+ @http_proto = params[:http_proto]
76
+ @skip_shard_routing_callback = params[:skip_shard_routing_callback]
77
+ end
78
+
79
+ def call(env)
80
+ # On every request, we want to initialize/clear the local cache to keep track the Account and Shard map,
81
+ # so it can reduce the hit to any cache system. The cache exists throughout the request and only that request.
82
+ ShardHandler::CurrentRequestCache.init
83
+
84
+ return app.call(env) if skip_shard_routing?(env["PATH_INFO"], env["REQUEST_METHOD"])
85
+
86
+ shard_id = RightSharding::ShardHandler.fetch_this_shard_id(config_object)
87
+
88
+ # HTTP_X_FORWARDED_HOST header may contain multiple values, pick the first one
89
+ forward_host = (env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"]).split(',')[0]
90
+
91
+ # NOTE: we use HTTP_HOST instead of SERVER_NAME because:
92
+ # * for the configuration like WEBrick server, it returns both hostname and port. (development env)
93
+ # * for others, it only returns the hostname
94
+ uri_handler = new_uri_handler(get_http_proto(env),
95
+ forward_host,
96
+ env['PATH_INFO'],
97
+ env['QUERY_STRING'])
98
+
99
+ if (account_id = fetch_current_account_id_from_path(env["PATH_INFO"]))
100
+ requested_shard = ShardHandler.fetch_shard_id_and_hostname(sharding_object, account_id.to_i)
101
+
102
+ if requested_shard.blank? || requested_shard[:shard_id].blank?
103
+ # no shard info, pass the request through
104
+ shard_routing_logger(:error, "Could not locate shard info", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
105
+ return app.call(env)
106
+ elsif requested_shard[:shard_id] != shard_id
107
+ # the requested account is in a different shard => redirect
108
+
109
+ # reset the hostname to a new hostname and everything else remains the same
110
+ reset_request_uri(uri_handler, requested_shard[:shard_hostname], env["REQUEST_METHOD"], env['PATH_INFO'], env['QUERY_STRING'])
111
+
112
+ return reroute_to_shard(uri_handler)
113
+ else
114
+ # everything good => continue to the next in the chain
115
+ return app.call(env)
116
+ end
117
+ else
118
+ ## legacy or api route
119
+ # always default to the account_id from global_session if none given in the url
120
+ if (account_id = env["global_session"]["account"])
121
+ requested_shard = ShardHandler.fetch_shard_id_and_hostname(sharding_object, account_id.to_i)
122
+
123
+ path_info = env['PATH_INFO']
124
+ # add account to the legacy route
125
+ if path_info && !path_info.empty?
126
+ # we don't rewrite the route if it is api-1.5 request
127
+ if path_info !~ /(?:^|\/)api\//
128
+ shard_routing_logger(:info, "Missing account_id in URL", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
129
+ path_info = add_account_to_path(path_info, account_id)
130
+ else
131
+ api_version = get_api_version(env, uri_handler)
132
+
133
+ if api_version
134
+ # we don't rewrite the route if it is api-1.0 instance facing request
135
+ if api_version < 1.5 && path_info !~ /(?:^|\/)api\/inst\//
136
+ shard_routing_logger(:error, "Invalid API (<1.5) Request", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
137
+ path_info = add_account_to_path(path_info, account_id)
138
+ end
139
+ else
140
+ shard_routing_logger(:error, "Missing API version", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
141
+ end
142
+ end
143
+ end
144
+
145
+ # requested the account is in a different shard => redirect
146
+ if requested_shard.blank? || requested_shard[:shard_id].blank?
147
+ # no shard info, pass the request through
148
+ shard_routing_logger(:error, "Could not locate shard info", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
149
+ return app.call(env)
150
+ elsif requested_shard[:shard_id] != shard_id
151
+ # reset the hostname and the path (added /acct/:id) and everything else remains the same
152
+ reset_request_uri(uri_handler, requested_shard[:shard_hostname], env["REQUEST_METHOD"], path_info, env['QUERY_STRING'])
153
+
154
+ return reroute_to_shard(uri_handler)
155
+ else
156
+ if uri_handler.path != path_info
157
+ # rewrite request uri by adding account into the route
158
+ reset_request_uri(uri_handler,
159
+ env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"],
160
+ env["REQUEST_METHOD"], path_info,
161
+ env['QUERY_STRING'])
162
+
163
+ return reroute_to_shard(uri_handler)
164
+ else
165
+ return app.call(env)
166
+ end
167
+ end
168
+ else
169
+ # Invalid
170
+ shard_routing_logger(:error, "Invalid Request", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
171
+ return app.call(env)
172
+ end
173
+ end
174
+ ensure
175
+ ShardHandler::CurrentRequestCache.destroy
176
+ end
177
+
178
+ private ######################################################################
179
+ def get_http_proto(env)
180
+ if @http_proto
181
+ @http_proto
182
+ elsif env['HTTPS'] == 'on'
183
+ 'https'
184
+ elsif env['HTTP_X_FORWARDED_SSL'] == 'on'
185
+ 'https'
186
+ elsif env['HTTP_X_FORWARDED_PROTO']
187
+ env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
188
+ else
189
+ 'https'
190
+ end
191
+ end
192
+
193
+
194
+ #-----------------------------------------------------------------------
195
+ # URL Parser
196
+ #-----------------------------------------------------------------------
197
+
198
+ # Call through to the equivalent module method in RightSharding::Routing.
199
+ def fetch_current_account_id_from_path(path)
200
+ RightSharding::Routing.fetch_current_account_id_from_path(path)
201
+ end
202
+
203
+ # Call through to the equivalent module method in RightSharding::Routing.
204
+ def skip_shard_routing?(path, request_method)
205
+ RightSharding::Routing.skip_shard_routing?(path, request_method)
206
+ end
207
+
208
+ def get_api_version(env, uri_handler)
209
+ api_version = nil
210
+
211
+ if env["HTTP_X_API_VERSION"]
212
+ api_version = env["HTTP_X_API_VERSION"]
213
+ elsif uri_handler.query
214
+ query_hash = CGI.parse(uri_handler.query)
215
+
216
+ api_version_key = query_hash.keys.find {|query_key| query_key =~ /api_version/i}
217
+ api_version = api_version_key ? query_hash[api_version_key][0] : nil
218
+ end
219
+
220
+ return (!api_version.blank? ? api_version.to_f : nil)
221
+ end
222
+
223
+ #-----------------------------------------------------------------------
224
+ # URI Constructor
225
+ #-----------------------------------------------------------------------
226
+
227
+ def new_uri_handler(schema, hostname_with_port, path_info, query_string)
228
+ hostname, port = hostname_with_port.split(":")
229
+
230
+ # Ensure port is a float for URI::HTTP.build to not fail
231
+ port = port.to_i unless port.nil?
232
+
233
+ uri_handler = if schema == "http"
234
+ URI::HTTP.build(:host => hostname, :port => port)
235
+ else
236
+ URI::HTTPS.build(:host => hostname, :port => port)
237
+ end
238
+
239
+ uri_handler.path = path_info || ""
240
+ # set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
241
+ uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
242
+ uri_handler
243
+ end
244
+
245
+ def reset_request_uri(uri_handler, hostname_with_optional_port, request_method, path_info, query_string)
246
+ hostname, port = hostname_with_optional_port.split(":")
247
+
248
+ uri_handler.host = hostname
249
+ uri_handler.port = port unless port.nil?
250
+
251
+ if request_method.downcase == "get"
252
+ uri_handler.path = path_info || ""
253
+ # set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
254
+ uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
255
+ else
256
+ uri_handler.path = ""
257
+ uri_handler.query = nil
258
+ end
259
+
260
+ uri_handler
261
+ end
262
+
263
+ # Call through to the equivalent module method in RightSharding::Routing.
264
+ def add_account_to_path(original_path, account_id)
265
+ RightSharding::Routing.add_account_to_path(original_path, account_id)
266
+ end
267
+
268
+ #-----------------------------------------------------------------------
269
+ # Redirect Helpers
270
+ #-----------------------------------------------------------------------
271
+
272
+ def reroute_to_shard(uri_handler)
273
+ location = uri_handler.to_s
274
+
275
+ [ 301, { "Location" => location }, [ redirect_message(location) ] ]
276
+ end
277
+
278
+ def redirect_message(location)
279
+ %Q(Redirecting to <a href="#{location}">#{location}</a>)
280
+ end
281
+
282
+ #-----------------------------------------------------------------------
283
+ # Logging Helpers
284
+ #-----------------------------------------------------------------------
285
+
286
+ def shard_routing_logger(type, header, uri, query_string, request_method)
287
+ logger.send(type, "RightSharding::Routing: #{header} - #{uri} - '#{query_string}' - '#{request_method}' request")
288
+ end
289
+
290
+ end
291
+
292
+ end
293
+ end
294
+ end
295
+
296
+ module Rack
297
+ ShardRouting = ::RightSharding::Rack::Middleware::Routing unless defined?(::Rack::ShardRouting)
298
+ end
@@ -0,0 +1,137 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ module RightSharding
15
+ module ShardHandler
16
+
17
+ SHARDING_CACHE_EXPIRATION_RANGE = (30..60).to_a
18
+
19
+ #---------------------------------------------------------------------------
20
+ # THIS_SHARD_ID
21
+ #---------------------------------------------------------------------------
22
+
23
+ def self.fetch_this_shard_id(config_object)
24
+ # defer the call to the object after we are sure that it is not in cache
25
+ this_shard_id_proc = Proc.new { config_object.find_this_shard_id }
26
+
27
+ if CurrentRequestCache.is_activated?
28
+ CurrentRequestCache.find_or_save_this_shard_id(this_shard_id_proc)
29
+ else
30
+ # In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
31
+ this_shard_id_proc.call
32
+ end
33
+ end
34
+
35
+ #---------------------------------------------------------------------------
36
+ # THIS_SHARD
37
+ #---------------------------------------------------------------------------
38
+
39
+ def self.shard_object_cache_key(shard_id)
40
+ "shard/#{shard_id}"
41
+ end
42
+
43
+ def self.fetch_shard(shard_object, shard_id)
44
+ # defer the call to the object after we are sure that it is not in cache
45
+ this_shard_proc = Proc.new {
46
+ shard_object.get_shard_object(shard_id, shard_object_cache_key(shard_id), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
47
+ }
48
+
49
+ if CurrentRequestCache.is_activated?
50
+ CurrentRequestCache.find_or_save_this_shard(this_shard_proc)
51
+ else
52
+ # In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
53
+ this_shard_proc.call
54
+ end
55
+ end
56
+
57
+ def self.fetch_this_shard(config_object, shard_object)
58
+ fetch_shard(shard_object, fetch_this_shard_id(config_object))
59
+ end
60
+
61
+ #---------------------------------------------------------------------------
62
+ # SHARD information for each Account
63
+ #---------------------------------------------------------------------------
64
+
65
+ def self.shard_cache_key(account_id)
66
+ "account/#{account_id}/shard_id_and_hostname"
67
+ end
68
+
69
+ def self.fetch_shard_id_and_hostname(sharding_object, account_id)
70
+ # defer the call to the object after we are sure that it is not in cache
71
+ get_shard_id_and_hostname_proc = Proc.new {
72
+ sharding_object.get_shard_id_and_hostname(account_id, shard_cache_key(account_id), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
73
+ }
74
+
75
+ if CurrentRequestCache.is_activated?
76
+ CurrentRequestCache.find_or_save_shard_map(account_id, get_shard_id_and_hostname_proc)
77
+ else
78
+ # In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
79
+ get_shard_id_and_hostname_proc.call
80
+ end
81
+ end
82
+
83
+ def self.fetch_shard_hostname(sharding_object, account_id)
84
+ requested_shard = fetch_shard_id_and_hostname(sharding_object, account_id)
85
+ requested_shard[:shard_hostname]
86
+ end
87
+
88
+ #---------------------------------------------------------------------------
89
+ # SHARD information for each InstanceApiToken
90
+ #---------------------------------------------------------------------------
91
+
92
+ def self.instance_api_token_shard_cache_key(token)
93
+ "instance_api_token/#{token}/shard_id_and_hostname"
94
+ end
95
+
96
+ def self.fetch_instance_api_token_shard_id_and_hostname(token)
97
+ # FIXME: cyclical dependency - InstanceApiToken is defined in right_site
98
+ InstanceApiToken.get_shard_id_and_hostname(token, instance_api_token_shard_cache_key(token), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
99
+ end
100
+
101
+ #---------------------------------------------------------------------------
102
+ # CurrentRequestCache - caching data only for this request
103
+ #---------------------------------------------------------------------------
104
+
105
+ class CurrentRequestCache
106
+ CACHE_ID = :right_sharding_current_request_cache
107
+
108
+ def self.init
109
+ Thread.current[CACHE_ID] = {}
110
+
111
+ Thread.current[CACHE_ID]["shard_map"] = {}
112
+ Thread.current[CACHE_ID]["this_shard_id"] = nil
113
+ end
114
+
115
+ def self.destroy
116
+ Thread.current[CACHE_ID] = nil
117
+ end
118
+
119
+ def self.is_activated?
120
+ !Thread.current[CACHE_ID].nil?
121
+ end
122
+
123
+ def self.find_or_save_shard_map(account_id, value_proc)
124
+ Thread.current[CACHE_ID]["shard_map"][account_id] ||= value_proc.call
125
+ end
126
+
127
+ def self.find_or_save_this_shard_id(value_proc)
128
+ Thread.current[CACHE_ID]["this_shard_id"] ||= value_proc.call
129
+ end
130
+
131
+ def self.find_or_save_this_shard(value_proc)
132
+ Thread.current[CACHE_ID]["this_shard"] ||= value_proc.call
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,147 @@
1
+ #--
2
+ # Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
3
+ #
4
+ # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
5
+ # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
6
+ # reproduction, modification, or disclosure of this program is
7
+ # strictly prohibited. Any use of this program by an authorized
8
+ # licensee is strictly subject to the terms and conditions,
9
+ # including confidentiality obligations, set forth in the applicable
10
+ # License Agreement between RightScale, Inc. and
11
+ # the licensee.
12
+ #++
13
+
14
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
15
+
16
+ module RightSharding
17
+ module Rack
18
+ module Middleware
19
+
20
+ class UserApiProxy < BaseProxy
21
+
22
+ ACCOUNTS_TO_PROXY = []
23
+
24
+ class UserApiProxyYield < BaseProxyYield; end
25
+ class UserApiProxyException < BaseProxyException; end
26
+
27
+ #-----------------------------------------------------------------------
28
+ # Rack Middleware
29
+ #-----------------------------------------------------------------------
30
+
31
+ def initialize(app, params = {})
32
+ super
33
+ @http_proto = params[:http_proto]
34
+ @accounts_to_proxy = params[:accounts_to_proxy] || []
35
+ end
36
+
37
+ def call(env)
38
+ @env = env
39
+
40
+ if is_an_api_call?
41
+ account_id = get_account_id()
42
+ if is_proxiable?(account_id)
43
+ this_shard_id = RightSharding::ShardHandler.fetch_this_shard_id(@config_object)
44
+ requested_shard = RightSharding::ShardHandler.fetch_shard_id_and_hostname(@sharding_object, account_id.to_i)
45
+ requested_shard_id = requested_shard.blank? ? nil : requested_shard[:shard_id]
46
+
47
+ if !requested_shard_id.blank? && (this_shard_id != requested_shard_id)
48
+ @env['global_session.req.update'] = false
49
+ hostname = get_shard_hostname(account_id)
50
+ return proxy_request_to_the_destinated_shard(hostname, :proxy_global_session => true)
51
+ end
52
+ end
53
+ end
54
+
55
+ # everything is good
56
+ return @app.call(env)
57
+ rescue UserApiProxyYield => e
58
+ shard_proxy_logger(:error, e.message)
59
+ return @app.call(env)
60
+ rescue UserApiProxyException => e
61
+ shard_proxy_logger(:error, "failed: #{e.class} " + e.message + "\n " + e.backtrace.join("\n "))
62
+ return [e.code, e.headers, e.body]
63
+ end
64
+
65
+ # -----------------------------------------------------------------------
66
+ # General Helpers
67
+ # -----------------------------------------------------------------------
68
+
69
+ def is_proxiable?(account_id)
70
+ return true if @accounts_to_proxy.any? {|p| p == account_id}
71
+ false
72
+ end
73
+
74
+ def get_shard_hostname(account_id)
75
+ RightSharding::ShardHandler.fetch_shard_hostname(@sharding_object, account_id)
76
+ end
77
+
78
+ def get_account_id()
79
+ account_id = nil
80
+ api_version = get_api_version()
81
+ if api_version == 1.0 # It can be in url or global session, prefer url
82
+ if @env["PATH_INFO"] =~ /(?:^|\/)api\/acct\/([^\/]+)(?:\/|$)/
83
+ account_id = $1
84
+ else
85
+ if @env['global_session'] && @env['global_session']['account']
86
+ account_id = @env['global_session']['account']
87
+ end
88
+ end
89
+ elsif api_version == 1.5 # version 1.5 assumes a global account session
90
+ if @env['global_session'] && @env['global_session']['account']
91
+ account_id = @env['global_session']['account']
92
+ elsif @env['PATH_INFO'] =~ /(?:^|\/)api\/session.*$/
93
+ # We get here if it's a session request
94
+ req = ::Rack::Request.new(@env)
95
+ params = req.params
96
+ if params['account_href'] =~ /\/api\/accounts\/(\d+)/
97
+ account_id = $1
98
+ end
99
+ end
100
+ end
101
+ account_id.nil? ? nil : account_id.to_i
102
+ end
103
+
104
+ def is_an_api_call?
105
+ return true if @env['PATH_INFO'] =~ /(?:^|\/)api\/.*$/
106
+ false
107
+ end
108
+
109
+ def new_uri_handler(schema, hostname_with_port, path_info, query_string)
110
+ hostname, port = hostname_with_port.split(":")
111
+
112
+ uri_handler = if schema == "http"
113
+ URI::HTTP.build(:host => hostname, :port => port)
114
+ else
115
+ URI::HTTPS.build(:host => hostname, :port => port)
116
+ end
117
+
118
+ uri_handler.path = path_info || ""
119
+ # set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
120
+ uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
121
+ uri_handler
122
+ end
123
+
124
+ def get_http_proto(env)
125
+ if @http_proto
126
+ @http_proto
127
+ elsif env['HTTPS'] == 'on'
128
+ 'https'
129
+ elsif env['HTTP_X_FORWARDED_SSL'] == 'on'
130
+ 'https'
131
+ elsif env['HTTP_X_FORWARDED_PROTO']
132
+ env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
133
+ else
134
+ 'https'
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+ end
142
+ end
143
+
144
+ module Rack
145
+ ShardUserApiProxy = ::RightSharding::Rack::Middleware::UserApiProxy unless defined?(::Rack::ShardUserApiProxy)
146
+ end
147
+
metadata ADDED
@@ -0,0 +1,239 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: right_sharding
3
+ version: !ruby/object:Gem::Version
4
+ hash: 17
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 3
10
+ version: 1.0.3
11
+ platform: ruby
12
+ authors:
13
+ - RightScale, Inc.
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2014-07-25 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ type: :runtime
22
+ name: global_session
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - <
27
+ - !ruby/object:Gem::Version
28
+ hash: 27
29
+ segments:
30
+ - 4
31
+ - 0
32
+ version: "4.0"
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ hash: 9
36
+ segments:
37
+ - 1
38
+ - 0
39
+ - 15
40
+ version: 1.0.15
41
+ prerelease: false
42
+ requirement: *id001
43
+ - !ruby/object:Gem::Dependency
44
+ type: :runtime
45
+ name: mime-types
46
+ version_requirements: &id002 !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ~>
50
+ - !ruby/object:Gem::Version
51
+ hash: 47
52
+ segments:
53
+ - 1
54
+ - 16
55
+ version: "1.16"
56
+ prerelease: false
57
+ requirement: *id002
58
+ - !ruby/object:Gem::Dependency
59
+ type: :runtime
60
+ name: rest-client
61
+ version_requirements: &id003 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ hash: 97
67
+ segments:
68
+ - 1
69
+ - 7
70
+ - 0
71
+ - 3
72
+ version: 1.7.0.3
73
+ prerelease: false
74
+ requirement: *id003
75
+ - !ruby/object:Gem::Dependency
76
+ type: :runtime
77
+ name: actionpack
78
+ version_requirements: &id004 !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - <
82
+ - !ruby/object:Gem::Version
83
+ hash: 27
84
+ segments:
85
+ - 4
86
+ - 0
87
+ version: "4.0"
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 9
91
+ segments:
92
+ - 2
93
+ - 3
94
+ - 5
95
+ version: 2.3.5
96
+ prerelease: false
97
+ requirement: *id004
98
+ - !ruby/object:Gem::Dependency
99
+ type: :runtime
100
+ name: right_support
101
+ version_requirements: &id005 !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ~>
105
+ - !ruby/object:Gem::Version
106
+ hash: 9
107
+ segments:
108
+ - 2
109
+ - 5
110
+ version: "2.5"
111
+ prerelease: false
112
+ requirement: *id005
113
+ - !ruby/object:Gem::Dependency
114
+ type: :runtime
115
+ name: simple_uuid
116
+ version_requirements: &id006 !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ~>
120
+ - !ruby/object:Gem::Version
121
+ hash: 15
122
+ segments:
123
+ - 0
124
+ - 2
125
+ version: "0.2"
126
+ prerelease: false
127
+ requirement: *id006
128
+ - !ruby/object:Gem::Dependency
129
+ type: :development
130
+ name: nokogiri
131
+ version_requirements: &id007 !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ~>
135
+ - !ruby/object:Gem::Version
136
+ hash: 3
137
+ segments:
138
+ - 1
139
+ - 5
140
+ - 0
141
+ version: 1.5.0
142
+ prerelease: false
143
+ requirement: *id007
144
+ - !ruby/object:Gem::Dependency
145
+ type: :development
146
+ name: jeweler
147
+ version_requirements: &id008 !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ hash: 3
153
+ segments:
154
+ - 0
155
+ version: "0"
156
+ prerelease: false
157
+ requirement: *id008
158
+ - !ruby/object:Gem::Dependency
159
+ type: :development
160
+ name: yard
161
+ version_requirements: &id009 !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ hash: 3
167
+ segments:
168
+ - 0
169
+ version: "0"
170
+ prerelease: false
171
+ requirement: *id009
172
+ - !ruby/object:Gem::Dependency
173
+ type: :development
174
+ name: redcarpet
175
+ version_requirements: &id010 !ruby/object:Gem::Requirement
176
+ none: false
177
+ requirements:
178
+ - - <
179
+ - !ruby/object:Gem::Version
180
+ hash: 7
181
+ segments:
182
+ - 3
183
+ - 0
184
+ version: "3.0"
185
+ prerelease: false
186
+ requirement: *id010
187
+ description:
188
+ email:
189
+ executables: []
190
+
191
+ extensions: []
192
+
193
+ extra_rdoc_files:
194
+ - README.md
195
+ files:
196
+ - lib/right_sharding.rb
197
+ - lib/right_sharding/base_proxy.rb
198
+ - lib/right_sharding/current_account.rb
199
+ - lib/right_sharding/extensions/core.rb
200
+ - lib/right_sharding/instance_api_proxy.rb
201
+ - lib/right_sharding/routing.rb
202
+ - lib/right_sharding/shard_handler.rb
203
+ - lib/right_sharding/user_api_proxy.rb
204
+ - README.md
205
+ homepage:
206
+ licenses: []
207
+
208
+ post_install_message:
209
+ rdoc_options: []
210
+
211
+ require_paths:
212
+ - lib
213
+ required_ruby_version: !ruby/object:Gem::Requirement
214
+ none: false
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ hash: 3
219
+ segments:
220
+ - 0
221
+ version: "0"
222
+ required_rubygems_version: !ruby/object:Gem::Requirement
223
+ none: false
224
+ requirements:
225
+ - - ">="
226
+ - !ruby/object:Gem::Version
227
+ hash: 3
228
+ segments:
229
+ - 0
230
+ version: "0"
231
+ requirements: []
232
+
233
+ rubyforge_project:
234
+ rubygems_version: 1.8.15
235
+ signing_key:
236
+ specification_version: 3
237
+ summary: ""
238
+ test_files: []
239
+