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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f368e18d7818a9897c2a59309e2850bb44ad977d97513ae210996d2762f138f7
|
|
4
|
+
data.tar.gz: ffe739174f2998da3bb0bba9de535687dc6143914c123db6df718e9fe2c361df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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 ---
|