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