fly-ruby 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/fly-ruby/railtie.rb +10 -9
- data/lib/fly-ruby/regional_database.rb +61 -80
- data/lib/fly-ruby/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dacb7a202a7a167f2ef451373f29893ca4efe7c0a46e60aeb4d958883a6cd3ec
|
4
|
+
data.tar.gz: d650c2d9459908dc81f5bdf8f72f6744c097b5f40de20fc7c339b64b96797361
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d5cad909dcf688f11958ebb2012c4388d685ca9d691c4db7bc9b2fd43e3522dd6c52b4c98ac7a7f5ae9937a285069bf68608dbec27477d86ac4fad52cdbe624e
|
7
|
+
data.tar.gz: 93879cc71887b23c2f8082b44ee3630844f2b5fc493b9daebff964b119a090ba8e1b28b62f844c239c27b011b852f5a978b12ba27f85aa5fc3607c4f3dea0397
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
### Bug fixes
|
4
4
|
|
5
|
+
- Run the database exception handler at the bottom of the stack to ensure it will take priority over other exception handlers
|
6
|
+
|
7
|
+
## 0.2.1
|
8
|
+
|
9
|
+
### Bug fixes
|
10
|
+
|
5
11
|
- Only hijack the database connection for requests in secondary regions
|
6
12
|
|
7
13
|
## 0.2.0
|
data/lib/fly-ruby/railtie.rb
CHANGED
@@ -15,20 +15,21 @@ class Fly::Railtie < Rails::Railtie
|
|
15
15
|
|
16
16
|
# Set useful headers for debugging
|
17
17
|
def set_debug_response_headers
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
response.headers['Fly-Database-Host'] = Fly.configuration.regional_database_config["host"]
|
18
|
+
ActiveSupport::Reloader.to_prepare do
|
19
|
+
ApplicationController.send(:after_action) do
|
20
|
+
response.headers['Fly-Region'] = ENV['FLY_REGION']
|
21
|
+
end
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
26
25
|
initializer("fly.regional_database") do |app|
|
27
|
-
set_debug_response_headers
|
28
|
-
if Fly.configuration.eligible_for_activation?
|
29
|
-
app.config.middleware.insert_after ActionDispatch::Executor, Fly::RegionalDatabase
|
26
|
+
set_debug_response_headers if Fly.configuration.web?
|
30
27
|
|
31
|
-
|
28
|
+
if Fly.configuration.eligible_for_activation?
|
29
|
+
# Insert the request interceptor high in the stack, but after static file delivery
|
30
|
+
app.config.middleware.insert_after ActionDispatch::Executor, Fly::RegionalDatabase::ReplayableRequestMiddleware
|
31
|
+
# Insert the database exception handler at the bottom of the stack to take priority over other exception handlers
|
32
|
+
app.config.middleware.use Fly::RegionalDatabase::DbExceptionHandlerMiddleware
|
32
33
|
hijack_database_connection if Fly.configuration.in_secondary_region?
|
33
34
|
elsif Fly.configuration.web?
|
34
35
|
puts "Warning: DATABASE_URL, PRIMARY_REGION and FLY_REGION must be set to activate the fly-ruby middleware. Middleware not loaded."
|
@@ -5,39 +5,7 @@ module Fly
|
|
5
5
|
# multithreaded environments. Instead of using dirty tricks like using Object#dup,
|
6
6
|
# values are passed to methods.
|
7
7
|
|
8
|
-
|
9
|
-
def initialize(app)
|
10
|
-
@app = app
|
11
|
-
end
|
12
|
-
|
13
|
-
# Overwrite the database connection string environment variable
|
14
|
-
# to prefer connections to the regional replica.
|
15
|
-
#
|
16
|
-
# For Rails apps, this process will be repeated at middleware insertion time,
|
17
|
-
# to support situations where the database is already accessed by other
|
18
|
-
# initialization code. See Fly::Railtie.
|
19
|
-
|
20
|
-
def prefer_regional_database!
|
21
|
-
return if Fly.configuration.web?
|
22
|
-
|
23
|
-
uri = Fly.configuration.database_uri
|
24
|
-
|
25
|
-
ENV[Fly.configuration.database_url_env_var] = uri.to_s
|
26
|
-
ENV[Fly.configuration.database_host_env_var] = uri.hostname
|
27
|
-
ENV[Fly.configuration.database_port_env_var] = uri.port.to_s
|
28
|
-
end
|
29
|
-
|
30
|
-
def in_primary_region?
|
31
|
-
Fly.configuration.primary_region == Fly.configuration.current_region
|
32
|
-
end
|
33
|
-
|
34
|
-
def regional_database_url
|
35
|
-
end
|
36
|
-
|
37
|
-
def response_body
|
38
|
-
"<html>Replaying request in #{Fly.configuration.primary_region}</html>"
|
39
|
-
end
|
40
|
-
|
8
|
+
module RegionalDatabase
|
41
9
|
# Stop the current request and ask for it to be replayed in the primary region.
|
42
10
|
# Pass one of three states to the target region, to determine how to handle the request:
|
43
11
|
#
|
@@ -45,70 +13,83 @@ module Fly
|
|
45
13
|
# captured_write: A write was rejected by the database
|
46
14
|
# http_method: A non-idempotent HTTP method was replayed before hitting the application
|
47
15
|
# threshold: A recent write set a threshold during which all requests are replayed
|
48
|
-
|
49
|
-
def replay_in_primary_region!(state:)
|
16
|
+
|
17
|
+
def self.replay_in_primary_region!(state:)
|
50
18
|
res = Rack::Response.new(
|
51
|
-
|
19
|
+
"",
|
52
20
|
409,
|
53
|
-
{"
|
21
|
+
{"Fly-Replay" => "region=#{Fly.configuration.primary_region};state=#{state}"}
|
54
22
|
)
|
55
23
|
res.finish
|
56
24
|
end
|
57
25
|
|
58
|
-
|
59
|
-
|
60
|
-
|
26
|
+
class DbExceptionHandlerMiddleware
|
27
|
+
def initialize(app)
|
28
|
+
@app = app
|
29
|
+
end
|
61
30
|
|
62
|
-
|
63
|
-
|
31
|
+
def call(env)
|
32
|
+
@app.call(env)
|
33
|
+
rescue PG::ReadOnlySqlTransaction, ActiveRecord::StatementInvalid => e
|
34
|
+
if e.is_a?(PG::ReadOnlySqlTransaction) || e&.cause&.is_a?(PG::ReadOnlySqlTransaction)
|
35
|
+
RegionalDatabase.replay_in_primary_region!(state: "captured_write")
|
36
|
+
else
|
37
|
+
raise e
|
38
|
+
end
|
39
|
+
end
|
64
40
|
end
|
65
41
|
|
66
|
-
|
67
|
-
|
68
|
-
|
42
|
+
class ReplayableRequestMiddleware
|
43
|
+
def initialize(app)
|
44
|
+
@app = app
|
45
|
+
end
|
69
46
|
|
70
|
-
|
71
|
-
|
47
|
+
def within_replay_threshold?(threshold)
|
48
|
+
threshold && (threshold.to_i - Time.now.to_i) > 0
|
49
|
+
end
|
72
50
|
|
73
|
-
|
74
|
-
|
75
|
-
# 1. Its HTTP method matches those configured for automatic replay (post/patch/put/delete by default).
|
76
|
-
# This approach should avoid potentially slow code execution - before_actions or other controller code -
|
77
|
-
# happening before a request reaches a database write.
|
78
|
-
# 2. It arrived before the threshold defined by the last write request. This threshold
|
79
|
-
# helps avoid the same client from missing its own write due to replication lag,
|
80
|
-
# like when a user adds to a todo list via XHR
|
81
|
-
|
82
|
-
if !in_primary_region?
|
83
|
-
if replayable_http_method?(request.request_method)
|
84
|
-
return replay_in_primary_region!(state: "http_method")
|
85
|
-
elsif within_replay_threshold?(request.cookies[Fly.configuration.replay_threshold_cookie])
|
86
|
-
return replay_in_primary_region!(state: "threshold")
|
87
|
-
end
|
51
|
+
def replayable_http_method?(http_method)
|
52
|
+
Fly.configuration.replay_http_methods.include?(http_method)
|
88
53
|
end
|
89
54
|
|
90
|
-
|
91
|
-
|
92
|
-
rescue ActiveRecord::StatementInvalid => e
|
93
|
-
if e.cause.is_a?(PG::ReadOnlySqlTransaction)
|
94
|
-
return replay_in_primary_region!(state: "captured_write")
|
95
|
-
else
|
96
|
-
raise e
|
97
|
-
end
|
55
|
+
def replay_request_state(header_value)
|
56
|
+
header_value&.scan(/(.*?)=(.*?)($|;)/)&.detect { |v| v[0] == "state" }&.at(1)
|
98
57
|
end
|
99
58
|
|
100
|
-
|
101
|
-
|
59
|
+
def call(env)
|
60
|
+
request = Rack::Request.new(env)
|
61
|
+
|
62
|
+
# Does this request satisfiy a condition for replaying in the primary region?
|
63
|
+
#
|
64
|
+
# 1. Its HTTP method matches those configured for automatic replay
|
65
|
+
# 2. It arrived before the threshold defined by the last write request.
|
66
|
+
# This threshold helps avoid the same client from missing its own
|
67
|
+
# write due to replication lag.
|
68
|
+
|
69
|
+
if Fly.configuration.in_secondary_region?
|
70
|
+
if replayable_http_method?(request.request_method)
|
71
|
+
return RegionalDatabase.replay_in_primary_region!(state: "http_method")
|
72
|
+
elsif within_replay_threshold?(request.cookies[Fly.configuration.replay_threshold_cookie])
|
73
|
+
return RegionalDatabase.replay_in_primary_region!(state: "threshold")
|
74
|
+
end
|
75
|
+
end
|
102
76
|
|
103
|
-
|
104
|
-
if replay_state && replay_state != "threshold"
|
105
|
-
response.set_cookie(
|
106
|
-
Fly.configuration.replay_threshold_cookie,
|
107
|
-
Time.now.to_i + Fly.configuration.replay_threshold_in_seconds
|
108
|
-
)
|
109
|
-
end
|
77
|
+
status, headers, body = @app.call(env)
|
110
78
|
|
111
|
-
|
79
|
+
response = Rack::Response.new(body, status, headers)
|
80
|
+
replay_state = replay_request_state(request.get_header("HTTP_FLY_REPLAY_SRC"))
|
81
|
+
|
82
|
+
# Request was replayed, but not by a threshold, so set a threshold within which
|
83
|
+
# all requests should be replayed to the primary region
|
84
|
+
if replay_state && replay_state != "threshold"
|
85
|
+
response.set_cookie(
|
86
|
+
Fly.configuration.replay_threshold_cookie,
|
87
|
+
Time.now.to_i + Fly.configuration.replay_threshold_in_seconds
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
response.finish
|
92
|
+
end
|
112
93
|
end
|
113
94
|
end
|
114
95
|
end
|
data/lib/fly-ruby/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fly-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Sierles
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-07-
|
11
|
+
date: 2021-07-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|