activerecord-beagle-turso 0.1.8 → 0.1.9

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: cd2ebde88afce8658ba27c6a8ae41ffb3228e6ec7bb6c65be6ff0a5a1c56d6ec
4
- data.tar.gz: 2c6ac050108dd08d75808fb5aa28854164f91835c6529ff400ff97351d81c449
3
+ metadata.gz: f368e18d7818a9897c2a59309e2850bb44ad977d97513ae210996d2762f138f7
4
+ data.tar.gz: ffe739174f2998da3bb0bba9de535687dc6143914c123db6df718e9fe2c361df
5
5
  SHA512:
6
- metadata.gz: 0cc05e0873fd0a0d143c2bae7758320768b06bdca6f6cd676de504cc09bc61360877aa81fa7d4b88ca7836758103135d3056d15cc5587721481b826fd9d0855c
7
- data.tar.gz: d0be294e7c35be6e5275cbbbcb54e80546ddb1b4c30a6f6ea32b72ec88c8554eded57b6537e128581ce599c3d1c173e49c443f7ca4e88ba7366641d351b9916d
6
+ metadata.gz: '07097b798e8ef32bfad3d84318c9b1101284631a7e8171006c1a1be788e12d89e4dc292a7ef2155fdcc3468a1730046f4629c2c7fb16cb5bbc69b7d27965ed50'
7
+ data.tar.gz: 61880c51d328741a0cbf7641bb2e9eb12b946db91f4472265870b5e03b22cd4bc2ae07023381185bea42afc5329777ab83d31f0e4bab553267ee783df070d188
data/README.md CHANGED
@@ -68,7 +68,10 @@ does not cover them automatically. Source them from encrypted credentials or
68
68
  transaction to finish before raising `Beagle::Turso::BusyError`. Transactions
69
69
  begin with `BEGIN IMMEDIATE`, so a writer waits at `BEGIN`, never part-way
70
70
  through. The wait sleeps in Ruby, so other threads and fibers keep running.
71
- Without `timeout:` (or with 0), a busy write raises at once.
71
+ Opening a new connection waits the same way when a synced replica is busy
72
+ with a sync (the engine reports "database tape error: database is busy" as an
73
+ `OpenError` or `ConnectError`), so a pool growing during a `SyncJob` no
74
+ longer fails. Without `timeout:` (or with 0), both raise at once.
72
75
 
73
76
  An optional `bootstrap_if_empty:` (default `true`) controls whether a brand
74
77
  new/empty local file is populated from the remote on first open — see
@@ -74,12 +74,87 @@ module ActiveRecord
74
74
  INSERT_STATEMENT_REGEX = /\A\s*(?:(?:INSERT|REPLACE)\b|WITH\b[\s\S]*\)\s*(?:INSERT|REPLACE)\b)/i
75
75
  private_constant :INSERT_STATEMENT_REGEX
76
76
 
77
+ # A statement that lost the single-writer race. A stale snapshot is
78
+ # never retried: the transaction that read it has to start again.
79
+ STATEMENT_BUSY = lambda do |error|
80
+ error.is_a?(::Beagle::Turso::BusyError) && !error.message.include?("snapshot is stale")
81
+ end
82
+ private_constant :STATEMENT_BUSY
83
+
84
+ # Opening or connecting to a synced replica while another connection
85
+ # syncs it. The sync engine reports that as "database tape error:
86
+ # database is busy" inside an OpenError or ConnectError (the driver only
87
+ # classifies the engine's own "Database is busy" as BusyError).
88
+ OPEN_BUSY = lambda do |error|
89
+ STATEMENT_BUSY.call(error) ||
90
+ ((error.is_a?(::Beagle::Turso::OpenError) || error.is_a?(::Beagle::Turso::ConnectError)) &&
91
+ error.message.match?(/database is busy/i))
92
+ end
93
+ private_constant :OPEN_BUSY
94
+
77
95
  class << self
78
96
  # Open a beagle-turso Database (synced remote when +remote_url+ is
79
97
  # present, otherwise local/in-memory) and wrap it so the SQLite3Adapter
80
- # can drive it as a raw connection.
98
+ # can drive it as a raw connection. A busy open is retried for
99
+ # database.yml's `timeout:`, like a busy statement.
81
100
  def new_client(config)
82
- database =
101
+ busy = ->(error) { OPEN_BUSY.call(error) || torn_meta_read?(error, config) }
102
+ retry_busy(busy_timeout(config[:timeout]), busy: busy) do
103
+ database = open_database(config)
104
+ begin
105
+ Client.new(database)
106
+ rescue Exception
107
+ database.close
108
+ raise
109
+ end
110
+ end
111
+ end
112
+
113
+ # `timeout:` (ms, as in database.yml) as seconds; nil means no waiting.
114
+ def busy_timeout(milliseconds)
115
+ milliseconds.to_i.positive? ? milliseconds.to_i / 1000.0 : nil
116
+ end
117
+
118
+ # Retries the block while it raises an error +busy+ accepts, with
119
+ # capped exponential backoff, for up to +timeout+ seconds. The engine
120
+ # never waits for a lock itself (from beagle-turso 0.4.0 it raises at
121
+ # once), so this is where the wait happens. sleep releases the GVL and
122
+ # yields to a fiber scheduler, so whoever holds the lock can finish.
123
+ def retry_busy(timeout, busy:)
124
+ deadline = nil
125
+ delay = 0.002
126
+ begin
127
+ yield
128
+ rescue ::Beagle::Turso::Error => e
129
+ raise if timeout.nil? || !busy.call(e)
130
+
131
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
132
+ deadline ||= now + timeout
133
+ raise if now + delay > deadline
134
+
135
+ sleep delay
136
+ delay = [delay * 2, 0.1].min
137
+ retry
138
+ end
139
+ end
140
+
141
+ def synced_config?(config)
142
+ !config[:remote_url].to_s.empty? && !config[:auth_token].to_s.empty?
143
+ end
144
+
145
+ private
146
+ # The sync engine (turso 0.8.3) rewrites a replica's metadata file
147
+ # (`<database>-info`) in place, truncating it first, so an open that
148
+ # reads it mid-sync can find it empty and fail with "meta must be
149
+ # initialized before open". Retried only when the file is there: a
150
+ # replica that was never initialised fails at once.
151
+ def torn_meta_read?(error, config)
152
+ error.is_a?(::Beagle::Turso::OpenError) &&
153
+ error.message.include?("meta must be initialized before open") &&
154
+ File.exist?("#{config[:database]}-info")
155
+ end
156
+
157
+ def open_database(config)
83
158
  if config[:remote_url]
84
159
  Beagle::Turso::Database.open(
85
160
  local_path: config[:database].to_s,
@@ -90,12 +165,7 @@ module ActiveRecord
90
165
  else
91
166
  Beagle::Turso::Database.open_local(config[:database].to_s)
92
167
  end
93
- Client.new(database)
94
- end
95
-
96
- def synced_config?(config)
97
- !config[:remote_url].to_s.empty? && !config[:auth_token].to_s.empty?
98
- end
168
+ end
99
169
  end
100
170
 
101
171
  # Adapts a Beagle::Turso::Database + its connected Connection to the subset
@@ -187,31 +257,14 @@ module ActiveRecord
187
257
  # does to the sqlite3 gem's busy handler. Unset or 0 means no waiting:
188
258
  # a BusyError surfaces at once, as with the sqlite3 gem.
189
259
  def busy_handler_timeout=(milliseconds)
190
- @busy_timeout = milliseconds.to_i.positive? ? milliseconds.to_i / 1000.0 : nil
260
+ @busy_timeout = BeagleTursoAdapter.busy_timeout(milliseconds)
191
261
  end
192
262
 
193
- # Retries a statement that lost the single-writer race, with capped
194
- # exponential backoff, for up to the busy timeout. The engine never
195
- # waits for a lock itself (from beagle-turso 0.4.0 it raises at once),
196
- # so this is where the wait happens. sleep releases the GVL and yields
197
- # to a fiber scheduler, so the writer holding the lock can finish. The
198
- # caller decides what is safe to retry (see BeagleTursoAdapter#perform_query).
199
- def retry_busy
200
- deadline = nil
201
- delay = 0.002
202
- begin
203
- yield
204
- rescue ::Beagle::Turso::BusyError => e
205
- raise if @busy_timeout.nil? || e.message.include?("snapshot is stale")
206
-
207
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
208
- deadline ||= now + @busy_timeout
209
- raise if now + delay > deadline
210
-
211
- sleep delay
212
- delay = [delay * 2, 0.1].min
213
- retry
214
- end
263
+ # Retries a statement that lost the single-writer race for up to the
264
+ # busy timeout (see BeagleTursoAdapter.retry_busy). The caller decides
265
+ # what is safe to retry (see BeagleTursoAdapter#perform_query).
266
+ def retry_busy(&)
267
+ BeagleTursoAdapter.retry_busy(@busy_timeout, busy: STATEMENT_BUSY, &)
215
268
  end
216
269
 
217
270
  # --- sqlite3-gem lifecycle shims the SQLite3Adapter drives ---
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module BeagleTurso
5
- VERSION = "0.1.8"
5
+ VERSION = "0.1.9"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-beagle-turso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - BeagleSoftwareUK