Skip to content

Commit a37e8d2

Browse files
committed
fix: pace SQLite lock retries
Yield between deadline-aware lock attempts so a contended caller does not hammer SQLite while preserving the original sync deadline. Assert bounded retry pressure without re-executing actor behavior.
1 parent 6cdbac8 commit a37e8d2

3 files changed

Lines changed: 38 additions & 8 deletions

File tree

lib/solid_objects/database_adapters/sqlite.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
module SolidObjects
44
module DatabaseAdapters
55
class Sqlite < DatabaseAdapter
6+
LOCK_RETRY_INTERVAL = 0.001
7+
LOCK_RETRY_MUTEX = Thread::Mutex.new
8+
LOCK_RETRY_CONDITION = Thread::ConditionVariable.new
9+
610
# @rbs () -> String
711
def current_time_expression
812
"STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
@@ -27,7 +31,7 @@ def with_lock_retry
2731
rescue DatabaseDeadlineExceeded
2832
raise if SyncDeadline.expired?
2933

30-
yield_before_retry
34+
wait_before_retry
3135
retry
3236
rescue => error
3337
raise unless deadline_error?(error)
@@ -38,7 +42,7 @@ def with_lock_retry
3842
cause: error
3943
end
4044

41-
yield_before_retry
45+
wait_before_retry
4246
retry
4347
end
4448

@@ -84,8 +88,13 @@ def deadline_error?(error)
8488
end
8589

8690
# @rbs () -> void
87-
def yield_before_retry
88-
Thread.pass
91+
def wait_before_retry
92+
LOCK_RETRY_MUTEX.synchronize do
93+
LOCK_RETRY_CONDITION.wait(
94+
LOCK_RETRY_MUTEX,
95+
[ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min
96+
)
97+
end
8998
end
9099
end
91100
end

sig/generated/lib/solid_objects/database_adapters/sqlite.rbs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
module SolidObjects
44
module DatabaseAdapters
55
class Sqlite < DatabaseAdapter
6+
LOCK_RETRY_INTERVAL: ::Float
7+
8+
LOCK_RETRY_MUTEX: untyped
9+
10+
LOCK_RETRY_CONDITION: untyped
11+
612
# @rbs () -> String
713
def current_time_expression: () -> String
814

@@ -24,7 +30,7 @@ module SolidObjects
2430
def deadline_error?: (Exception) -> bool
2531

2632
# @rbs () -> void
27-
def yield_before_retry: () -> void
33+
def wait_before_retry: () -> void
2834
end
2935
end
3036
end

test/integration/synchronous_invocation_test.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "database_test_helper"
44
require "solid_objects/mailbox"
5+
require "solid_objects/synchronous_invocation"
56
require "timeout"
67

78
class SynchronousInvocationTest < ActiveSupport::TestCase
@@ -483,11 +484,13 @@ def wait(timeout:)
483484
)
484485
lock = hold_sqlite_write_lock
485486

486-
error, elapsed = invoke_with_immediate_sqlite_lock_failure(message_reference)
487+
error, elapsed, attempts = invoke_with_immediate_sqlite_lock_failure(message_reference)
487488

488489
assert_instance_of SolidObjects::SyncTimeout, error
489490
assert_equal message_reference.id, error.message_id
490491
assert_operator elapsed, :<, 0.5
492+
assert_operator attempts, :>, 1
493+
assert_operator attempts, :<, 200
491494
assert_equal 0, LockRetryActor.executions
492495

493496
release_sqlite_write_lock(lock)
@@ -512,11 +515,13 @@ def wait(timeout:)
512515
)
513516
lock = hold_sqlite_write_lock
514517

515-
error, elapsed = invoke_with_immediate_sqlite_lock_failure(message_reference)
518+
error, elapsed, attempts = invoke_with_immediate_sqlite_lock_failure(message_reference)
516519

517520
assert_instance_of SolidObjects::SyncTimeout, error
518521
assert_equal message_reference.id, error.message_id
519522
assert_operator elapsed, :<, 0.5
523+
assert_operator attempts, :>, 1
524+
assert_operator attempts, :<, 200
520525
assert_equal 0, LockRetryActor.executions
521526

522527
release_sqlite_write_lock(lock)
@@ -758,6 +763,10 @@ def invoke_with_immediate_sqlite_lock_failure(message_reference)
758763
invocation = Thread.new do
759764
error = nil
760765
elapsed = nil
766+
attempts = 0
767+
subscription = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
768+
attempts += 1 if process_write?(event.payload)
769+
end
761770
SolidObjects::Record.connection_pool.with_connection do |connection|
762771
previous_timeout = connection.select_value("PRAGMA busy_timeout").to_i
763772
connection.execute("PRAGMA busy_timeout = 0")
@@ -768,14 +777,20 @@ def invoke_with_immediate_sqlite_lock_failure(message_reference)
768777
elapsed = monotonic_now - started_at
769778
ensure
770779
connection.execute("PRAGMA busy_timeout = #{previous_timeout}")
780+
ActiveSupport::Notifications.unsubscribe(subscription)
771781
end
772-
result << [ error, elapsed ]
782+
result << [ error, elapsed, attempts ]
773783
end
774784
captured = Timeout.timeout(2) { result.pop }
775785
invocation.join
776786
captured
777787
end
778788

789+
def process_write?(payload)
790+
payload.fetch(:sql).match?(/\A(?:INSERT|UPDATE)/) &&
791+
payload.fetch(:sql).include?(SolidObjects::Process.table_name)
792+
end
793+
779794
def actor_instance(actor_id)
780795
SolidObjects::Instance.find_by!(
781796
actor_type: "synchronous-counter",

0 commit comments

Comments
 (0)