Class: Fly::RegionalDatabase
- Inherits:
-
Object
- Object
- Fly::RegionalDatabase
- Defined in:
- lib/fly-ruby/regional_database.rb
Overview
Note that using instance variables in Rack middleware is considered a poor practice in multithreaded environments. Instead of using dirty tricks like using Object#dup, values are passed to methods.
Instance Method Summary collapse
- #call(env) ⇒ Object
- #in_primary_region? ⇒ Boolean
-
#initialize(app) ⇒ RegionalDatabase
constructor
A new instance of RegionalDatabase.
-
#prefer_regional_database! ⇒ Object
Overwrite the primary database URL with that of the regional replica.
- #regional_database_url ⇒ Object
-
#replay_in_primary_region!(state:) ⇒ Object
Stop the current request and ask for it to be replayed in the primary region.
- #replay_request_state(header_value) ⇒ Object
- #replayable_http_method?(http_method) ⇒ Boolean
- #response_body ⇒ Object
- #within_replay_threshold?(threshold) ⇒ Boolean
Constructor Details
#initialize(app) ⇒ RegionalDatabase
Returns a new instance of RegionalDatabase.
7 8 9 10 |
# File 'lib/fly-ruby/regional_database.rb', line 7 def initialize(app) @app = app prefer_regional_database! unless in_primary_region? end |
Instance Method Details
#call(env) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/fly-ruby/regional_database.rb', line 67 def call(env) request = Rack::Request.new(env) # Check whether this request satisfies any of the following conditions for replaying in the primary region: # # 1. Its HTTP method matches those configured for automatic replay (post/patch/put/delete by default). # This approach should avoid potentially slow code execution - before_actions or other controller code - # happening before a request reaches a database write. # 2. It arrived before the threshold defined by the last write request. This threshold # helps avoid the same client from missing its own write due to replication lag, # like when a user adds to a todo list via XHR if !in_primary_region? if replayable_http_method?(request.request_method) return replay_in_primary_region!(state: "http_method") elsif within_replay_threshold?(request.[Fly.configuration.]) return replay_in_primary_region!(state: "threshold") end end begin status, headers, body = @app.call(env) rescue ActiveRecord::StatementInvalid => e if e.cause.is_a?(PG::ReadOnlySqlTransaction) return replay_in_primary_region!(state: "captured_write") else raise e end end response = Rack::Response.new(body, status, headers) replay_state = replay_request_state(request.get_header("HTTP_FLY_REPLAY_SRC")) # Request was replayed, but not by a threshold if replay_state && replay_state != "threshold" response.( Fly.configuration., Time.now.to_i + Fly.configuration.replay_threshold_in_seconds ) end response.finish end |
#in_primary_region? ⇒ Boolean
27 28 29 |
# File 'lib/fly-ruby/regional_database.rb', line 27 def in_primary_region? Fly.configuration.primary_region == Fly.configuration.current_region end |
#prefer_regional_database! ⇒ Object
Overwrite the primary database URL with that of the regional replica
13 14 15 16 17 18 19 20 21 22 23 24 25 |
# File 'lib/fly-ruby/regional_database.rb', line 13 def prefer_regional_database! uri = URI.parse(Fly.configuration.database_url) hostname = "#{Fly.configuration.current_region}.#{uri.hostname}" port = 5433 uri.hostname = hostname uri.port = port uri.to_s ENV[Fly.configuration.database_url_env_var] = uri.to_s ENV[Fly.configuration.database_host_env_var] = hostname ENV[Fly.configuration.database_port_env_var] = port.to_s end |
#regional_database_url ⇒ Object
31 32 |
# File 'lib/fly-ruby/regional_database.rb', line 31 def regional_database_url end |
#replay_in_primary_region!(state:) ⇒ Object
Stop the current request and ask for it to be replayed in the primary region. Pass one of three states to the target region, to determine how to handle the request:
Possible states: captured_write, http_method, threshold captured_write: A write was rejected by the database http_method: A non-idempotent HTTP method was replayed before hitting the application threshold: A recent write set a threshold during which all requests are replayed
46 47 48 49 50 51 52 53 |
# File 'lib/fly-ruby/regional_database.rb', line 46 def replay_in_primary_region!(state:) res = Rack::Response.new( response_body, 409, {"fly-replay" => "region=#{Fly.configuration.primary_region};state=#{state}"} ) res.finish end |
#replay_request_state(header_value) ⇒ Object
63 64 65 |
# File 'lib/fly-ruby/regional_database.rb', line 63 def replay_request_state(header_value) header_value&.scan(/(.*?)=(.*?)($|;)/)&.detect { |v| v[0] == "state" }&.at(1) end |
#replayable_http_method?(http_method) ⇒ Boolean
59 60 61 |
# File 'lib/fly-ruby/regional_database.rb', line 59 def replayable_http_method?(http_method) Fly.configuration.replay_http_methods.include?(http_method) end |
#response_body ⇒ Object
34 35 36 |
# File 'lib/fly-ruby/regional_database.rb', line 34 def response_body "<html>Replaying request in #{Fly.configuration.primary_region}</html>" end |
#within_replay_threshold?(threshold) ⇒ Boolean
55 56 57 |
# File 'lib/fly-ruby/regional_database.rb', line 55 def within_replay_threshold?(threshold) threshold && (threshold.to_i - Time.now.to_i) > 0 end |