fly-ruby 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|