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 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