resilient_reads 0.1.4 → 0.1.6

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: 56b29303c6173728af9204eaa7b2b4c2ed73a5b6d7e6e3100d8e23bbc2afc2ae
4
- data.tar.gz: 813d2268fd80a7835c2c4744eca2f4a6f879645049305ec02df71443b8329d47
3
+ metadata.gz: 1fe8df35a3f61180a19739623b0aaff7bbbe294243ad427c009582ed1c8a318c
4
+ data.tar.gz: fb4714db90a83396646d513a9d017f38882ed908d063becd504144376d094c5d
5
5
  SHA512:
6
- metadata.gz: 32e222767e097b4735a534a2cf4f1be7edc4e15214e3ccdcb1135c277170023d134228fea85efe0068882ed6a9549e961f9d8cc8270dc71ca371c7ae7a83dc5e
7
- data.tar.gz: 7e4da86bf77656e5b948446314f2841ce1aa64f4ef01cb980ad5b59eb678cf8291b335d6372d63cdf2a9f90dcb90f446a8caf91b6e7c692e1a2c9f0dcf4fd5b3
6
+ metadata.gz: b5e1659a0280bba649717303fc25d0f5188a7c8660dda88fc4e4857fc6a102eaba2a65259ce07f641338164f6876cb53ddd438d9cf318191acb471998cc166d8
7
+ data.tar.gz: 05542c3105b4eccd0092e1242524fb88dd37804d0f7757c8b9255d36aec2cdb95181a7a80a835e3ec777745def2118fd61416757c06ac13370c4a11f2be85142
@@ -11,7 +11,7 @@ module ResilientReads
11
11
  # Query names that should never be routed to a replica. These are
12
12
  # schema introspection or internal bookkeeping queries that run during
13
13
  # model loading and connection setup.
14
- SKIP_NAMES = Set.new(%w[SCHEMA EXPLAIN]).freeze
14
+ SKIP_NAMES = Set.new(%w[SCHEMA EXPLAIN TRANSACTION]).freeze
15
15
 
16
16
  # SQL clauses that acquire locks and must execute on the primary,
17
17
  # even though the statement starts with SELECT.
@@ -31,15 +31,21 @@ module ResilientReads
31
31
 
32
32
  execute_on_replica(sql, ctx, *args, **kwargs)
33
33
  else
34
- if ctx && ctx[:distributing]
35
- if write_query?(sql)
36
- # Sticky writes: after any write inside a distribute_reads
34
+ if ctx && ctx[:distributing] && !ctx[:on_replica]
35
+ adapter_write = write_query?(sql)
36
+
37
+ if adapter_write || ResilientReads.write_query?(sql)
38
+ # Sticky writes: after any DML write inside a distribute_reads
37
39
  # block, all subsequent reads stay on primary for the rest of
38
40
  # the block. This prevents stale-read → conflicting-write
39
41
  # chains that cause MySQL/InnoDB deadlocks, especially with
40
42
  # transactionless writes like update_column that don't bump
41
43
  # open_transactions.
42
- if ResilientReads.config.sticky_writes
44
+ #
45
+ # Only adapter-detected writes (DML/DDL) trigger sticky —
46
+ # transaction commands (BEGIN/COMMIT) are handled by the
47
+ # open_transactions guard instead.
48
+ if ResilientReads.config.sticky_writes && adapter_write
43
49
  ctx[:stick_to_primary] = true
44
50
  end
45
51
  ResilientReads.log_query("primary", sql, name, reason: "write query")
@@ -53,12 +59,17 @@ module ResilientReads
53
59
 
54
60
  private
55
61
 
56
- # Schema queries, internal queries, and queries without a name
57
- # (connection setup, etc.) should always hit the primary.
62
+ # Queries that must stay on the primary: writes, transaction
63
+ # commands, schema introspection, and locking reads.
64
+ #
65
+ # We check *both* the adapter's write_query? (which knows about
66
+ # adapter-specific DDL/DML) and ResilientReads' own WRITE_PATTERN
67
+ # (which catches transaction commands like BEGIN/COMMIT/SET that
68
+ # some adapters classify as reads).
58
69
  def skip_replica_routing?(sql, name)
59
- return true if name.nil? || name == ""
60
- return true if SKIP_NAMES.include?(name)
70
+ return true if name && SKIP_NAMES.include?(name)
61
71
  return true if write_query?(sql)
72
+ return true if ResilientReads.write_query?(sql)
62
73
  return true if locking_query?(sql)
63
74
  false
64
75
  end
@@ -87,8 +98,18 @@ module ResilientReads
87
98
  end
88
99
 
89
100
  # Optional lag check — uses cached value to avoid querying on every request.
101
+ # The routing guard must be set while the lag query runs so that the
102
+ # replica adapter's patched raw_execute doesn't re-enter
103
+ # execute_on_replica (the lag SQL is a SELECT executed on the replica
104
+ # connection, which would otherwise pass skip_replica_routing? and
105
+ # recurse infinitely).
90
106
  if ResilientReads.config.max_lag
91
- lag = replica.cached_lag
107
+ ctx[:routing] = true
108
+ begin
109
+ lag = replica.cached_lag
110
+ ensure
111
+ ctx[:routing] = false
112
+ end
92
113
  if lag && lag > ResilientReads.config.max_lag
93
114
  if ResilientReads.config.lag_failover
94
115
  ResilientReads.log_query("primary", sql, args.first, reason: "replica '#{replica.name}' lag #{lag.round(1)}s > max #{ResilientReads.config.max_lag}s")
@@ -85,8 +85,16 @@ module ResilientReads
85
85
  end
86
86
 
87
87
  # Verify the replica is reachable. Returns true/false and updates health.
88
+ # Uses the raw connection to avoid ActiveRecord SQL instrumentation/logging.
88
89
  def check_health!
89
- @connection_class.connection.execute("SELECT 1")
90
+ raw = @connection_class.connection.raw_connection
91
+ if raw.respond_to?(:exec) # PG::Connection
92
+ raw.exec("SELECT 1")
93
+ elsif raw.respond_to?(:ping) # Mysql2::Client / Trilogy
94
+ raw.ping
95
+ else
96
+ @connection_class.connection.execute("SELECT 1")
97
+ end
90
98
  mark_healthy!
91
99
  true
92
100
  rescue => e
@@ -1,3 +1,3 @@
1
1
  module ResilientReads
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.6"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resilient_reads
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamie Puckett
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-06-20 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Distribute database reads across multiple replicas in Rails with automatic
13
13
  load balancing, health checking, and graceful failover to primary.
@@ -59,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
61
  requirements: []
62
- rubygems_version: 3.6.9
62
+ rubygems_version: 3.6.2
63
63
  specification_version: 4
64
64
  summary: Distribute database reads across multiple replicas in Rails with automatic
65
65
  load balancing, health checking, and graceful failover to primary.