Class: Fly::RegionalDatabase

Inherits:
Object
  • Object
show all
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

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.cookies[Fly.configuration.replay_threshold_cookie])
      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.set_cookie(
      Fly.configuration.replay_threshold_cookie,
      Time.now.to_i + Fly.configuration.replay_threshold_in_seconds
    )
  end

  response.finish
end

#in_primary_region?Boolean

Returns:

  • (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_urlObject



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

Returns:

  • (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_bodyObject



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

Returns:

  • (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