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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd2ebde88afce8658ba27c6a8ae41ffb3228e6ec7bb6c65be6ff0a5a1c56d6ec
|
|
4
|
+
data.tar.gz: 2c6ac050108dd08d75808fb5aa28854164f91835c6529ff400ff97351d81c449
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
115
|
-
@connection
|
|
116
|
-
@closed
|
|
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
|
-
# ---
|
|
185
|
-
# configure_connection
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
360
|
-
|
|
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
|
-
|
|
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
|
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.
|
|
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.
|
|
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.
|
|
40
|
+
version: '0.3'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: sqlite3
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|