resilient_reads 0.1.5 → 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: d9a7b3f39e250957ac840a69af135911e9e466e1c1a656eaf965701b63cb5e9b
4
- data.tar.gz: 14c6ca6f9bd822cafc53e3cfa39562284ca4fffa3e2f8169ae0684fbac7ea1bf
3
+ metadata.gz: 1fe8df35a3f61180a19739623b0aaff7bbbe294243ad427c009582ed1c8a318c
4
+ data.tar.gz: fb4714db90a83396646d513a9d017f38882ed908d063becd504144376d094c5d
5
5
  SHA512:
6
- metadata.gz: 959af6f366c6bd99e6e37570c450b30746c197a87daa2448b83549cf53b166782e3e73582a740f346768e10d98e84f0bd5424cac39477fbeab630a90f1be0fe3
7
- data.tar.gz: 7b001e842fb6828491541f565a71b8a952a0435a4ad5de9faafe38076519196a858aa17f1113c9309479711fda07eaf0b2a8f8aa0d26da35a5495fedb0f8b21d
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")
@@ -1,3 +1,3 @@
1
1
  module ResilientReads
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
@@ -111,7 +111,7 @@ module ResilientReads
111
111
  logger = config.logger
112
112
  return unless logger
113
113
 
114
- # logger.public_send(level, "[ResilientReads] #{message}")
114
+ logger.public_send(level, "[ResilientReads] #{message}")
115
115
  rescue
116
116
  # Never let logging break query flow.
117
117
  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.5
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: 2026-06-19 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.