activerecord-beagle-turso 0.1.7 → 0.1.8

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: 51cf88616ac49583385f9d993560cc4f5fb53169707dec16c68b3c37f6d60884
4
- data.tar.gz: 21732cc614964877aeca0da6e34d066cfcfb2061747ac68f9eb2803abff60d1a
3
+ metadata.gz: cd2ebde88afce8658ba27c6a8ae41ffb3228e6ec7bb6c65be6ff0a5a1c56d6ec
4
+ data.tar.gz: 2c6ac050108dd08d75808fb5aa28854164f91835c6529ff400ff97351d81c449
5
5
  SHA512:
6
- metadata.gz: fe0832ec4ae1acc466934cb0ed1a0c47f2de5e5d5f86b47091ffbe1c5e8f77c86ee50534808d2cf7dbef318aa526cd0026146ac367d1c3f56275723d8630ccc3
7
- data.tar.gz: bf86e7af654df142cd722228c9245bd5456b6cfd9b2de5a43ae9067e018fee5491e17005896eec7f693ef0fc9e8d9bd9ac1d5791033bfff579879562fd69d1b9
6
+ metadata.gz: 0cc05e0873fd0a0d143c2bae7758320768b06bdca6f6cd676de504cc09bc61360877aa81fa7d4b88ca7836758103135d3056d15cc5587721481b826fd9d0855c
7
+ data.tar.gz: d0be294e7c35be6e5275cbbbcb54e80546ddb1b4c30a6f6ea32b72ec88c8554eded57b6537e128581ce599c3d1c173e49c443f7ca4e88ba7366641d351b9916d
data/README.md CHANGED
@@ -63,6 +63,13 @@ some diagnostics, but these are custom `database.yml` keys, so that masking
63
63
  does not cover them automatically. Source them from encrypted credentials or
64
64
  `ENV`, as above, and avoid logging/inspecting the raw connection config.
65
65
 
66
+ `timeout:` (milliseconds, as in the stock SQLite adapter; Rails' generated
67
+ `database.yml` sets 5000) is how long a write waits for another connection's
68
+ transaction to finish before raising `Beagle::Turso::BusyError`. Transactions
69
+ begin with `BEGIN IMMEDIATE`, so a writer waits at `BEGIN`, never part-way
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.
72
+
66
73
  An optional `bootstrap_if_empty:` (default `true`) controls whether a brand
67
74
  new/empty local file is populated from the remote on first open — see
68
75
  `Beagle::Turso::Database.open`.
@@ -111,9 +111,10 @@ module ActiveRecord
111
111
  attr_reader :database, :connection
112
112
 
113
113
  def initialize(database)
114
- @database = database
115
- @connection = database.connect
116
- @closed = false
114
+ @database = database
115
+ @connection = database.connect
116
+ @closed = false
117
+ @busy_timeout = nil
117
118
  end
118
119
 
119
120
  # A bare read of exactly +defer_foreign_keys+ or +read_uncommitted+
@@ -181,11 +182,39 @@ module ActiveRecord
181
182
  def push = database.push
182
183
  def pull = database.pull
183
184
 
184
- # --- sqlite3-gem lifecycle shims the SQLite3Adapter drives ---
185
- # configure_connection only touches this when config[:timeout] is set.
186
- def busy_handler_timeout=(_)
187
- nil
185
+ # --- busy handling ---
186
+ # configure_connection passes database.yml's `timeout:` (ms) here, as it
187
+ # does to the sqlite3 gem's busy handler. Unset or 0 means no waiting:
188
+ # a BusyError surfaces at once, as with the sqlite3 gem.
189
+ def busy_handler_timeout=(milliseconds)
190
+ @busy_timeout = milliseconds.to_i.positive? ? milliseconds.to_i / 1000.0 : nil
188
191
  end
192
+
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
215
+ end
216
+
217
+ # --- sqlite3-gem lifecycle shims the SQLite3Adapter drives ---
189
218
  # connected? => !(@raw_connection.nil? || @raw_connection.closed?)
190
219
  def closed? = @closed
191
220
  # Release the beagle-turso session eagerly rather than waiting for GC.
@@ -356,26 +385,10 @@ module ActiveRecord
356
385
  if batch
357
386
  raw_connection.execute_batch(sql)
358
387
  ::ActiveRecord::Result.empty
359
- elsif write_statement?(sql)
360
- if (rewritten = sql_with_returning_rowid(sql))
361
- insert_returning_rowid(raw_connection, rewritten, params)
362
- else
363
- affected = raw_connection.execute(sql, params)
364
- @last_inserted_rowid = raw_connection.last_insert_rowid
365
- ::ActiveRecord::Result.empty(affected_rows: affected)
366
- end
388
+ elsif busy_retry?(sql)
389
+ raw_connection.retry_busy { run_statement(raw_connection, sql, params) }
367
390
  else
368
- columns, rows = raw_connection.query_result(sql, params)
369
- # Belt-and-suspenders: a RETURNING insert lands here (id read from
370
- # rows by last_inserted_id), and a data-modifying CTE that slips past
371
- # write_statement? would too -- keep the rowid so an inserted id is
372
- # never silently lost even on a heuristic miss.
373
- if INSERT_REGEX.match?(sql)
374
- # On a replica where last_insert_rowid is untrustworthy, never
375
- # offer it as a fallback id (a RETURNING insert's id is in rows).
376
- @last_inserted_rowid = insert_returning_rowid? ? nil : raw_connection.last_insert_rowid
377
- end
378
- ::ActiveRecord::Result.new(columns, rows)
391
+ run_statement(raw_connection, sql, params)
379
392
  end
380
393
 
381
394
  verified!
@@ -384,6 +397,43 @@ module ActiveRecord
384
397
  result
385
398
  end
386
399
 
400
+ # A busy statement is retried only when that is safe: the BEGIN that
401
+ # opens a transaction (Rails sends BEGIN IMMEDIATE, so a writer waits
402
+ # for the lock there rather than failing mid-way), or a statement
403
+ # outside any transaction. Inside one, a BusyError means the
404
+ # transaction is dead; it must surface and roll back. A multi-statement
405
+ # batch is never retried (it may have run in part).
406
+ BEGIN_REGEX = /\A\s*BEGIN\b/i
407
+ private_constant :BEGIN_REGEX
408
+
409
+ def busy_retry?(sql)
410
+ !transaction_open? || BEGIN_REGEX.match?(mask_non_code(sql))
411
+ end
412
+
413
+ def run_statement(raw_connection, sql, params)
414
+ if write_statement?(sql)
415
+ if (rewritten = sql_with_returning_rowid(sql))
416
+ insert_returning_rowid(raw_connection, rewritten, params)
417
+ else
418
+ affected = raw_connection.execute(sql, params)
419
+ @last_inserted_rowid = raw_connection.last_insert_rowid
420
+ ::ActiveRecord::Result.empty(affected_rows: affected)
421
+ end
422
+ else
423
+ columns, rows = raw_connection.query_result(sql, params)
424
+ # Belt-and-suspenders: a RETURNING insert lands here (id read from
425
+ # rows by last_inserted_id), and a data-modifying CTE that slips past
426
+ # write_statement? would too -- keep the rowid so an inserted id is
427
+ # never silently lost even on a heuristic miss.
428
+ if INSERT_REGEX.match?(sql)
429
+ # On a replica where last_insert_rowid is untrustworthy, never
430
+ # offer it as a fallback id (a RETURNING insert's id is in rows).
431
+ @last_inserted_rowid = insert_returning_rowid? ? nil : raw_connection.last_insert_rowid
432
+ end
433
+ ::ActiveRecord::Result.new(columns, rows)
434
+ end
435
+ end
436
+
387
437
  # Route to the WRITE branch (execute -> affected count + last_insert_rowid)
388
438
  # rather than the read branch. A statement is a write when SQLite3's own
389
439
  # read/write classifier says so, OR when it is a data-modifying CTE that
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module BeagleTurso
5
- VERSION = "0.1.7"
5
+ VERSION = "0.1.8"
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.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - BeagleSoftwareUK
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.2'
33
+ version: '0.3'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.2'
40
+ version: '0.3'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: sqlite3
43
43
  requirement: !ruby/object:Gem::Requirement