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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79850f3de9e2a7e78ff9d46ba7728c605fae5651786d3a425dec0d12056d75b6
4
- data.tar.gz: 5349dfbb16a8a257a909c68410f2041bf5202bbeed5ae88e496a9efa2e1c9478
3
+ metadata.gz: dacb7a202a7a167f2ef451373f29893ca4efe7c0a46e60aeb4d958883a6cd3ec
4
+ data.tar.gz: d650c2d9459908dc81f5bdf8f72f6744c097b5f40de20fc7c339b64b96797361
5
5
  SHA512:
6
- metadata.gz: e43ad67e8d81f5d817ad274bb0f2e2a97d276fc1a9f0bb78549ea6cc06b5c2e5a90fd2ddd03f7529f592cf037bed68ed9f172945425829b7952acbdd4c20df58
7
- data.tar.gz: d91c4fd6e1a5d3619165c9a44076840ac331c8533f0cb94f34001022d987bca1f7b654de34668cc6029305341f39bc7caa22b31ad76968802261fd884816b878
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
@@ -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
- return unless defined?(ApplicationController)
19
-
20
- ApplicationController.send(:after_action) do
21
- response.headers['Fly-Region'] = ENV['FLY_REGION']
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
- # Run the middleware high in the stack, but after static file delivery
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
- class RegionalDatabase
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
- response_body,
19
+ "",
52
20
  409,
53
- {"fly-replay" => "region=#{Fly.configuration.primary_region};state=#{state}"}
21
+ {"Fly-Replay" => "region=#{Fly.configuration.primary_region};state=#{state}"}
54
22
  )
55
23
  res.finish
56
24
  end
57
25
 
58
- def within_replay_threshold?(threshold)
59
- threshold && (threshold.to_i - Time.now.to_i) > 0
60
- end
26
+ class DbExceptionHandlerMiddleware
27
+ def initialize(app)
28
+ @app = app
29
+ end
61
30
 
62
- def replayable_http_method?(http_method)
63
- Fly.configuration.replay_http_methods.include?(http_method)
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
- def replay_request_state(header_value)
67
- header_value&.scan(/(.*?)=(.*?)($|;)/)&.detect { |v| v[0] == "state" }&.at(1)
68
- end
42
+ class ReplayableRequestMiddleware
43
+ def initialize(app)
44
+ @app = app
45
+ end
69
46
 
70
- def call(env)
71
- request = Rack::Request.new(env)
47
+ def within_replay_threshold?(threshold)
48
+ threshold && (threshold.to_i - Time.now.to_i) > 0
49
+ end
72
50
 
73
- # Check whether this request satisfies any of the following conditions for replaying in the primary region:
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
- begin
91
- status, headers, body = @app.call(env)
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
- response = Rack::Response.new(body, status, headers)
101
- replay_state = replay_request_state(request.get_header("HTTP_FLY_REPLAY_SRC"))
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
- # Request was replayed, but not by a threshold
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
- response.finish
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
@@ -1,3 +1,3 @@
1
1
  module Fly
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
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.1
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-15 00:00:00.000000000 Z
11
+ date: 2021-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack