activerecord-enhancedsqlite3-adapter 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/enhanced_sqlite3/adapter.rb +60 -10
- data/lib/enhanced_sqlite3/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd134c96488a861c3fdc36c32396547a31ea596c11243aff923995acff8ae4b5
|
4
|
+
data.tar.gz: d2029b2162c66df5689e65cb4e281f73d21049adafaecf9bbf78490570a10321
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6803168a5f067839e6ea7416f2f067bbc044202d14fd21a9536d068bebe35d7d89a481cbfcf5e230d4063705a7b5241aeb76d8f92846e2ecf5cdc2afa8b805f0
|
7
|
+
data.tar.gz: 2845ea0983129dd0e6e66b66e8f10fca3538192dcdaec62ec55f497ebb8f82f52bdc2352eb1fdf85b859bbd7436e86b57d15da4ce1135c00e23f8996db5cfc59
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.4.0] - 2023-12-10
|
4
|
+
|
5
|
+
- Ensure transactions are IMMEDIATE and not DEFERRED
|
6
|
+
- Ensure that our `busy_handler` is the very first configuration to be set on a connection
|
7
|
+
- Simplify and speed up our `busy_handler` implementation
|
8
|
+
|
3
9
|
## [0.3.0] - 2023-12-06
|
4
10
|
|
5
11
|
- Added a more performant implementation of the the `timeout` mechanism
|
@@ -10,6 +10,27 @@ require "enhanced_sqlite3/supports_deferrable_constraints"
|
|
10
10
|
|
11
11
|
module EnhancedSQLite3
|
12
12
|
module Adapter
|
13
|
+
# Setup the Rails SQLite3 adapter instance.
|
14
|
+
#
|
15
|
+
# extends https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L90
|
16
|
+
def initialize(...)
|
17
|
+
super
|
18
|
+
# Ensure that all connections default to immediate transaction mode.
|
19
|
+
# This is necessary to prevent SQLite from deadlocking when concurrent processes open write transactions.
|
20
|
+
# By default, SQLite opens transactions in deferred mode, which means that a transactions acquire
|
21
|
+
# a shared lock on the database, but will attempt to upgrade that lock to an exclusive lock if/when
|
22
|
+
# a write is attempted. Because SQLite is in the middle of a transaction, it cannot retry the transaction
|
23
|
+
# if a BUSY exception is raised, and so it will immediately raise a SQLITE_BUSY exception without calling
|
24
|
+
# the `busy_handler`. Because Rails only wraps writes in transactions, this means that all transactions
|
25
|
+
# will attempt to acquire an exclusive lock on the database. Thus, under any concurrent load, you are very
|
26
|
+
# likely to encounter a SQLITE_BUSY exception.
|
27
|
+
# By setting the default transaction mode to immediate, SQLite will instead attempt to acquire
|
28
|
+
# an exclusive lock as soon as the transaction is opened. If the lock cannot be acquired, it will
|
29
|
+
# immediately call the `busy_handler` to retry the transaction. This allows concurrent processes to
|
30
|
+
# coordinate and linearize their transactions, avoiding deadlocks.
|
31
|
+
@connection_parameters.merge!(default_transaction_mode: :immediate)
|
32
|
+
end
|
33
|
+
|
13
34
|
# Perform any necessary initialization upon the newly-established
|
14
35
|
# @raw_connection -- this is the place to modify the adapter's
|
15
36
|
# connection settings, run queries to configure any application-global
|
@@ -18,11 +39,10 @@ module EnhancedSQLite3
|
|
18
39
|
# Implementations may assume this method will only be called while
|
19
40
|
# holding @lock (or from #initialize).
|
20
41
|
#
|
21
|
-
#
|
42
|
+
# overrides https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L691
|
22
43
|
def configure_connection
|
23
|
-
super
|
24
|
-
|
25
44
|
configure_busy_handler_timeout
|
45
|
+
check_version
|
26
46
|
configure_pragmas
|
27
47
|
configure_extensions
|
28
48
|
|
@@ -36,26 +56,56 @@ module EnhancedSQLite3
|
|
36
56
|
return unless @config.key?(:timeout)
|
37
57
|
|
38
58
|
timeout = self.class.type_cast_config_to_integer(@config[:timeout])
|
59
|
+
timeout_seconds = timeout.fdiv(1000)
|
60
|
+
retry_interval = 6e-5 # 60 microseconds
|
61
|
+
|
39
62
|
@raw_connection.busy_handler do |count|
|
40
63
|
timed_out = false
|
41
|
-
# capture the start time of this blocked write
|
42
|
-
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) if count == 0
|
43
64
|
# keep track of elapsed time every 100 iterations (to lower load)
|
44
|
-
if count % 100
|
45
|
-
@elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
|
65
|
+
if (count % 100).zero?
|
46
66
|
# fail if we exceed the timeout value (captured from the timeout config option, converted to seconds)
|
47
|
-
timed_out =
|
67
|
+
timed_out = (count * retry_interval) > timeout_seconds
|
48
68
|
end
|
49
69
|
if timed_out
|
50
70
|
false # this will cause the BusyException to be raised
|
51
71
|
else
|
52
|
-
sleep
|
72
|
+
sleep(retry_interval)
|
73
|
+
true
|
53
74
|
end
|
54
75
|
end
|
55
76
|
end
|
56
77
|
|
57
78
|
def configure_pragmas
|
58
|
-
|
79
|
+
defaults = {
|
80
|
+
# Enforce foreign key constraints
|
81
|
+
# https://www.sqlite.org/pragma.html#pragma_foreign_keys
|
82
|
+
# https://www.sqlite.org/foreignkeys.html
|
83
|
+
"foreign_keys" => "ON",
|
84
|
+
# Impose a limit on the WAL file to prevent unlimited growth
|
85
|
+
# https://www.sqlite.org/pragma.html#pragma_journal_size_limit
|
86
|
+
"journal_size_limit" => 64.megabytes,
|
87
|
+
# Set the local connection cache to 2000 pages
|
88
|
+
# https://www.sqlite.org/pragma.html#pragma_cache_size
|
89
|
+
"cache_size" => 2000
|
90
|
+
}
|
91
|
+
unless @memory_database
|
92
|
+
defaults.merge!(
|
93
|
+
# Journal mode WAL allows for greater concurrency (many readers + one writer)
|
94
|
+
# https://www.sqlite.org/pragma.html#pragma_journal_mode
|
95
|
+
"journal_mode" => "WAL",
|
96
|
+
# Set more relaxed level of database durability
|
97
|
+
# 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
|
98
|
+
# https://www.sqlite.org/pragma.html#pragma_synchronous
|
99
|
+
"synchronous" => "NORMAL",
|
100
|
+
# Set the global memory map so all processes can share some data
|
101
|
+
# https://www.sqlite.org/pragma.html#pragma_mmap_size
|
102
|
+
# https://www.sqlite.org/mmap.html
|
103
|
+
"mmap_size" => 128.megabytes
|
104
|
+
)
|
105
|
+
end
|
106
|
+
pragmas = defaults.merge(@config.fetch(:pragmas, {}))
|
107
|
+
|
108
|
+
pragmas.each do |key, value|
|
59
109
|
execute("PRAGMA #{key} = #{value}", "SCHEMA")
|
60
110
|
end
|
61
111
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-enhancedsqlite3-adapter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Margheim
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-12-
|
11
|
+
date: 2023-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|