Class: SolidObjects::DatabaseAdapters::Sqlite

Inherits:
SolidObjects::DatabaseAdapter show all
Defined in:
lib/solid_objects/database_adapters/sqlite.rb,
sig/generated/lib/solid_objects/database_adapters/sqlite.rbs

Constant Summary collapse

LOCK_RETRY_INTERVAL =

Returns:

  • (::Float)
0.001
MAXIMUM_BUSY_RETRY_INTERVAL =

Returns:

  • (::Float)
0.25
LOCK_RETRY_MUTEX =

Returns:

  • (Object)
Thread::Mutex.new
LOCK_RETRY_CONDITION =

Returns:

  • (Object)
Thread::ConditionVariable.new

Constants inherited from SolidObjects::DatabaseAdapter

SolidObjects::DatabaseAdapter::FAMILIES, SolidObjects::DatabaseAdapter::TRANSACTION_CLOCK, SolidObjects::DatabaseAdapter::TRANSACTION_CLOCK_SCOPE

Instance Attribute Summary

Attributes inherited from SolidObjects::DatabaseAdapter

#connection_pool, #fixed_connection

Instance Method Summary collapse

Methods inherited from SolidObjects::DatabaseAdapter

#additional_server_reasons, #claim_lock, #configure_transaction_deadline, #database_now, family, for, #initialize, #lock_candidates, #read_database_now, #server_version, #supports_skip_locked?, #unsupported_server_reasons, #with_connection, #with_transaction_clock

Constructor Details

This class inherits a constructor from SolidObjects::DatabaseAdapter

Instance Method Details

#busy_error?(error) ⇒ Boolean

RBS:

  • (Exception) -> bool

Parameters:

  • (Exception)

Returns:

  • (Boolean)


154
155
156
157
158
159
160
161
162
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 154

def busy_error?(error)
  cause = error
  while cause
    return true if cause.class.name.match?(/BusyException|BusyError/)

    cause = cause.cause
  end
  false
end

#configured_busy_handler_timeout(connection) ⇒ Integer?

RBS:

  • (untyped) -> Integer?

Parameters:

  • (Object)

Returns:

  • (Integer, nil)


129
130
131
132
133
134
135
136
137
138
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 129

def configured_busy_handler_timeout(connection)
  return nil unless connection.respond_to?(:raw_connection)
  return nil unless connection.raw_connection.respond_to?(:busy_handler_timeout=)

  pool = connection.respond_to?(:pool) ? connection.pool : nil
  return nil unless pool.respond_to?(:db_config)

  timeout = pool.db_config.configuration_hash[:timeout]
  timeout&.to_i
end

#current_time_expressionString

RBS:

  • () -> String

Returns:

  • (String)


17
18
19
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 17

def current_time_expression
  "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
end

#deadline_error?(error) ⇒ Boolean

RBS:

  • (Exception) -> bool

Parameters:

  • (Exception)

Returns:

  • (Boolean)


141
142
143
144
145
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 141

def deadline_error?(error)
  return false unless SyncDeadline.active?

  busy_error?(error) || reconnect_blocked_error?(error)
end

#minimum_server_versionGem::Version?

RBS:

  • () -> Gem::Version?

Returns:

  • (Gem::Version, nil)


12
13
14
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 12

def minimum_server_version
  Gem::Version.new("3.35")
end

#reconnect_blocked_error?(error) ⇒ Boolean

RBS:

  • (Exception) -> bool

Parameters:

  • (Exception)

Returns:

  • (Boolean)


148
149
150
151
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 148

def reconnect_blocked_error?(error)
  error.message.include?("SQLite3::CantOpenException") &&
    error.message.include?("PRAGMA journal_mode='wal'")
end

#restorable_busy_wait(connection) ⇒ Hash[Symbol, untyped]?

RBS:

  • (untyped) -> Hash[Symbol, untyped]?

Parameters:

  • (Object)

Returns:

  • (Hash[Symbol, untyped], nil)


107
108
109
110
111
112
113
114
115
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 107

def restorable_busy_wait(connection)
  handler_timeout = configured_busy_handler_timeout(connection)
  return { handler_timeout: } if handler_timeout

  pragma_timeout = connection.select_value("PRAGMA busy_timeout").to_i
  return nil unless pragma_timeout.positive?

  { pragma_timeout: }
end

#restore_busy_wait(connection, busy_wait) ⇒ void

This method returns an undefined value.

RBS:

  • (untyped, Hash[Symbol, untyped]) -> void

Parameters:

  • (Object)
  • (Hash[Symbol, untyped])


118
119
120
121
122
123
124
125
126
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 118

def restore_busy_wait(connection, busy_wait)
  handler_timeout = busy_wait[:handler_timeout]
  if handler_timeout
    connection.raw_connection.busy_handler_timeout = handler_timeout
    return
  end

  connection.execute("PRAGMA busy_timeout = #{busy_wait.fetch(:pragma_timeout)}")
end

#transaction { ... } ⇒ Object

RBS:

  • () { () -> untyped } -> untyped

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


22
23
24
25
26
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 22

def transaction(&block)
  return with_lock_retry { super } if SyncDeadline.active?

  with_busy_retry { super }
end

#wait_before_busy_retry(attempts) ⇒ void

This method returns an undefined value.

RBS:

  • (Integer) -> void

Parameters:

  • (Integer)


165
166
167
168
169
170
171
172
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 165

def wait_before_busy_retry(attempts)
  LOCK_RETRY_MUTEX.synchronize do
    LOCK_RETRY_CONDITION.wait(
      LOCK_RETRY_MUTEX,
      [ LOCK_RETRY_INTERVAL * (2**(attempts - 1)), MAXIMUM_BUSY_RETRY_INTERVAL ].min
    )
  end
end

#wait_before_retryvoid

This method returns an undefined value.

The deadline can expire between the check above and this wait, which would otherwise ask for a negative interval.

RBS:

  • () -> void



177
178
179
180
181
182
183
184
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 177

def wait_before_retry
  interval = [ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min
  return unless interval.positive?

  LOCK_RETRY_MUTEX.synchronize do
    LOCK_RETRY_CONDITION.wait(LOCK_RETRY_MUTEX, interval)
  end
end

#with_busy_retry { ... } ⇒ Object

A write outside a synchronous deadline has no Ruby-level budget, so it depends entirely on SQLite's busy handler. Concurrent writers can exhaust that, which surfaces as a lock error the caller cannot retry.

RBS:

  • () { () -> untyped } -> untyped

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 32

def with_busy_retry
  attempts = 0
  begin
    yield
  rescue => error
    raise unless busy_error?(error)

    attempts += 1
    raise if attempts > SolidObjects.configuration.lock_retry_attempts

    wait_before_busy_retry(attempts)
    retry
  end
end

#with_lock_probe { ... } ⇒ Object

RBS:

  • () { () -> untyped } -> untyped

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 75

def with_lock_probe
  return yield unless SyncDeadline.active?

  with_connection do |connection|
    with_transaction_deadline(connection) { yield }
  end
rescue => error
  raise unless deadline_error?(error)

  raise DatabaseDeadlineExceeded,
    "database remained locked at the synchronous invocation deadline",
    cause: error
end

#with_lock_retry { ... } ⇒ Object

RBS:

  • () { () -> untyped } -> untyped

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 48

def with_lock_retry
  return yield unless SyncDeadline.active?

  raise DatabaseDeadlineExceeded, "synchronous invocation deadline expired" if SyncDeadline.expired?

  with_connection do |connection|
    with_transaction_deadline(connection) { yield }
  end
rescue DatabaseDeadlineExceeded
  raise if SyncDeadline.expired?

  wait_before_retry
  retry
rescue => error
  raise unless deadline_error?(error)

  if SyncDeadline.expired?
    raise DatabaseDeadlineExceeded,
      "database lock wait exceeded the synchronous invocation deadline",
      cause: error
  end

  wait_before_retry
  retry
end

#with_transaction_deadline(connection) { ... } ⇒ Object

RBS:

  • (untyped) { () -> untyped } -> untyped

Parameters:

  • (Object)

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/solid_objects/database_adapters/sqlite.rb', line 92

def with_transaction_deadline(connection)
  return yield unless SyncDeadline.active?

  busy_wait = restorable_busy_wait(connection)
  return yield unless busy_wait

  begin
    connection.execute("PRAGMA busy_timeout = 0")
    yield
  ensure
    restore_busy_wait(connection, busy_wait)
  end
end