activerecord-beagle-turso 0.1.8 → 0.1.10
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 +4 -4
- data/README.md +15 -1
- data/lib/active_record/connection_adapters/beagle_turso/migration_push.rb +2 -0
- data/lib/active_record/connection_adapters/beagle_turso/test_process_release.rb +77 -0
- data/lib/active_record/connection_adapters/beagle_turso_adapter.rb +84 -31
- data/lib/activerecord/beagle_turso/version.rb +1 -1
- data/lib/activerecord-beagle-turso.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85d72dcc7cf48c86d50b2e81ec3d481b0ab33c1aa3a9f1425574a82d3662c94a
|
|
4
|
+
data.tar.gz: 05c6675f967450f5f0aa66c22e0bf0244da1ed4874645d93265f13e1a0a40a77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0937eb27407a0acf3ff4ce564181d949dc0578559faba7f76a49b41c0a597b47add6d34881df708cbae16e8629b40dcf6113fbbaed00d7703756da7c2cfca585'
|
|
7
|
+
data.tar.gz: 719662f8a03bc87f2a8b29f4f9f5d0e7ca48c66323f3b3258a67abd0f833e53a08c4df1a4db5e899a45ad696a261ef2ac4d2bbe3fa6dc5fd73207e4e7f69aa51
|
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
|
-
|
|
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
|
|
@@ -247,6 +250,17 @@ this gem.
|
|
|
247
250
|
each sync — pick a sync cadence accordingly, or run Solid Queue in a separate
|
|
248
251
|
process.
|
|
249
252
|
|
|
253
|
+
4. **One process per database file.** A local Turso database file is locked to
|
|
254
|
+
the process that opened it (SQLite lets processes share a file; Turso does
|
|
255
|
+
not), so a second process opening it fails with `OpenError` ("... File is
|
|
256
|
+
locked by another process") while the first holds a connection. The common
|
|
257
|
+
case is handled: `bin/rails db:test:prepare test` (or `... spec`) runs the
|
|
258
|
+
tests in a child process, so when a `db:*` rake task finishes the adapter
|
|
259
|
+
releases its own file-backed connections in the rake process
|
|
260
|
+
(`BeagleTurso::TestProcessRelease`). In-memory databases, other adapters'
|
|
261
|
+
connections and `Beagle::Turso::Database`s opened outside ActiveRecord are
|
|
262
|
+
left alone.
|
|
263
|
+
|
|
250
264
|
## Known engine issue: self-referencing foreign keys
|
|
251
265
|
|
|
252
266
|
Turso's engine has open bugs with foreign keys that reference their own table
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_record/connection_adapters/beagle_turso/errors"
|
|
4
4
|
require "active_record/connection_adapters/beagle_turso/sync_manager"
|
|
5
|
+
require "active_record/connection_adapters/beagle_turso/test_process_release"
|
|
5
6
|
|
|
6
7
|
module ActiveRecord
|
|
7
8
|
module ConnectionAdapters
|
|
@@ -199,6 +200,7 @@ module ActiveRecord
|
|
|
199
200
|
rake_tasks do
|
|
200
201
|
require "rake"
|
|
201
202
|
MigrationPush.install!
|
|
203
|
+
TestProcessRelease.install!
|
|
202
204
|
end
|
|
203
205
|
end
|
|
204
206
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module ConnectionAdapters
|
|
5
|
+
module BeagleTurso
|
|
6
|
+
# Lets `bin/rails db:test:prepare test` -- the Rails CI default -- run as
|
|
7
|
+
# one command (and `db:test:prepare spec`, or any task after it that
|
|
8
|
+
# starts a process of its own).
|
|
9
|
+
#
|
|
10
|
+
# The rake test tasks do not run the tests in the rake process:
|
|
11
|
+
# Rails::TestUnit::Runner.run_from_rake starts a separate `rails test`
|
|
12
|
+
# process and waits for it (rspec-rails' `spec` task does the same). A
|
|
13
|
+
# local Turso database file is locked to the process that opened it --
|
|
14
|
+
# SQLite lets processes share a file, Turso does not -- and
|
|
15
|
+
# db:test:prepare left its connection open, so the test process could
|
|
16
|
+
# not open the database:
|
|
17
|
+
#
|
|
18
|
+
# open failed: Locking error: Failed locking file '.../storage/test.sqlite3'.
|
|
19
|
+
# File is locked by another process
|
|
20
|
+
#
|
|
21
|
+
# So when an outermost db:* task finishes, this adapter's file-backed
|
|
22
|
+
# connections in the rake process are released. A pool reconnects on
|
|
23
|
+
# its next use. In-memory databases are left alone (closing one would
|
|
24
|
+
# destroy it, and no other process can open it anyway), as are other
|
|
25
|
+
# adapters' connections.
|
|
26
|
+
module TestProcessRelease
|
|
27
|
+
# Prepended onto Rake::Task so it works whatever order the Railtie and
|
|
28
|
+
# ActiveRecord's own rake tasks load in (same approach as MigrationPush).
|
|
29
|
+
module TaskHook
|
|
30
|
+
def execute(args = nil)
|
|
31
|
+
if name.start_with?("db:")
|
|
32
|
+
TestProcessRelease.around { super }
|
|
33
|
+
else
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
def install!
|
|
41
|
+
return if ::Rake::Task.ancestors.include?(TaskHook)
|
|
42
|
+
::Rake::Task.prepend(TaskHook)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Releases once, when the outermost db:* task finishes: a db task
|
|
46
|
+
# that invokes others (db:test:prepare -> db:test:load_schema) keeps
|
|
47
|
+
# its connection until it is done.
|
|
48
|
+
def around
|
|
49
|
+
self.depth = depth + 1
|
|
50
|
+
yield
|
|
51
|
+
ensure
|
|
52
|
+
self.depth = depth - 1
|
|
53
|
+
release_connections! if depth.zero?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def release_connections!
|
|
57
|
+
# Not loaded means nothing connected; don't load it to find out.
|
|
58
|
+
return unless defined?(::ActiveRecord::Base) && !::ActiveRecord.autoload?(:Base)
|
|
59
|
+
|
|
60
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list(:all).each do |pool|
|
|
61
|
+
pool.disconnect! if releasable?(pool.db_config)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def releasable?(db_config)
|
|
66
|
+
db_config.adapter.to_s == "beagle_turso" && db_config.database.to_s != ":memory:"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def depth = Thread.current[:beagle_turso_test_process_release_depth].to_i
|
|
70
|
+
def depth=(value)
|
|
71
|
+
Thread.current[:beagle_turso_test_process_release_depth] = value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
260
|
+
@busy_timeout = BeagleTursoAdapter.busy_timeout(milliseconds)
|
|
191
261
|
end
|
|
192
262
|
|
|
193
|
-
# Retries a statement that lost the single-writer race
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
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 ---
|
|
@@ -6,3 +6,4 @@ require "active_record/connection_adapters/beagle_turso/sync_manager"
|
|
|
6
6
|
require "active_record/connection_adapters/beagle_turso/sync_job"
|
|
7
7
|
require "active_record/connection_adapters/beagle_turso/errors"
|
|
8
8
|
require "active_record/connection_adapters/beagle_turso/migration_push"
|
|
9
|
+
require "active_record/connection_adapters/beagle_turso/test_process_release"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BeagleSoftwareUK
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- lib/active_record/connection_adapters/beagle_turso/migration_push.rb
|
|
98
98
|
- lib/active_record/connection_adapters/beagle_turso/sync_job.rb
|
|
99
99
|
- lib/active_record/connection_adapters/beagle_turso/sync_manager.rb
|
|
100
|
+
- lib/active_record/connection_adapters/beagle_turso/test_process_release.rb
|
|
100
101
|
- lib/active_record/connection_adapters/beagle_turso_adapter.rb
|
|
101
102
|
- lib/activerecord-beagle-turso.rb
|
|
102
103
|
- lib/activerecord/beagle_turso/version.rb
|