activerecord-enhancedsqlite3-adapter 0.3.0 → 0.4.0

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: 24efb62453474faa0b37357d98f5b0f8ba13fa98d7e633b5d3a7de5e16d875f6
4
- data.tar.gz: 0de2b8cae842c4262d81932c577df7731e570f347f0a22a2151ef9671bd8648d
3
+ metadata.gz: fd134c96488a861c3fdc36c32396547a31ea596c11243aff923995acff8ae4b5
4
+ data.tar.gz: d2029b2162c66df5689e65cb4e281f73d21049adafaecf9bbf78490570a10321
5
5
  SHA512:
6
- metadata.gz: a55c35f42fef85b0373df52f06d1b50b66c4191382844ee6a9836049af03386085bfe9616de8bfc259fcfd6325d5a84f4cc8ca396d721ff87fdcfc49c80dae17
7
- data.tar.gz: b50f2484872aa18a0681c31abfd4d6a56e044ff9610cb288c73611cb0f56ed8b8242a771b9c4e95a07119ec094aef5bcf6c85f1bc363fd08e59d45a57b162902
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
- # extends https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L691
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 == 0
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 = @elapsed_time > timeout
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 0.001 # sleep 1 millisecond (or whatever)
72
+ sleep(retry_interval)
73
+ true
53
74
  end
54
75
  end
55
76
  end
56
77
 
57
78
  def configure_pragmas
58
- @config.fetch(:pragmas, []).each do |key, value|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EnhancedSQLite3
4
- VERSION = '0.3.0'
4
+ VERSION = "0.4.0"
5
5
  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.3.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-06 00:00:00.000000000 Z
11
+ date: 2023-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord