Skip to content

Commit 36e2f87

Browse files
Run specs in a transaction (#780)
This replaces truncating the database before every spec. In avram we're seeing specs run ~20x faster but Lucky apps might expect to see speed ups of around 4-5x.
1 parent 9b184d4 commit 36e2f87

14 files changed

+157
-41
lines changed

db/migrations/20190723233131_test_change_type.cr

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class TestChangeType::V20190723233131 < Avram::Migrator::Migration::V1
2727
change_type id : Int64
2828
end
2929

30-
TempUserInt64::BaseQuery.first # should not raise
30+
TempUserInt64::BaseQuery.first.delete # should not raise
3131
end
3232

3333
def rollback

spec/avram/database_spec.cr

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
require "../spec_helper"
22

33
describe Avram::Database do
4-
describe "listen" do
4+
describe "listen", tags: Avram::SpecHelper::TRUNCATE do
55
it "yields the payload from a notify" do
66
done = Channel(Nil).new
77
TestDatabase.listen("dinner_time") do |notification|

spec/avram/instrumentation_spec.cr

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ require "../spec_helper"
22

33
describe "Instrumentation" do
44
it "publishes the query and args" do
5-
TestDatabase.query "SELECT * FROM users"
5+
# using block form to make sure the ResultSet is closed
6+
TestDatabase.query "SELECT * FROM users" { }
67

78
event = Avram::Events::QueryEvent.logged_events.last
89
event.query.should eq("SELECT * FROM users")

spec/avram/model_spec.cr

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ describe Avram::Model do
3333
user = UserFactory.create
3434
post = PostFactory.create
3535

36-
user.id.should eq(post.id)
3736
user.should_not eq(post)
3837
end
3938

spec/avram/query_logging_spec.cr

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe "Query logging" do
1313
it "does not log truncate statements" do
1414
Avram::QueryLog.dexter.temp_config do |log_io|
1515
TestDatabase.truncate
16-
log_io.to_s.should eq("")
16+
log_io.to_s.should_not contain("TRUNCATE TABLE")
1717
end
1818
end
1919

spec/spec_helper.cr

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ Db::Create.new(quiet: true).run_task
1919
Db::Migrate.new(quiet: true).run_task
2020
Db::VerifyConnection.new(quiet: true).run_task
2121

22+
Avram::SpecHelper.use_transactional_specs(TestDatabase)
23+
2224
Spec.before_each do
23-
TestDatabase.truncate
2425
# All specs seem to run on the same Fiber,
2526
# so we set back to NullStore before each spec
2627
# to ensure queries aren't randomly cached

src/avram.cr

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require "db"
77
require "pg"
88
require "uuid"
99

10+
require "./ext/db/*"
1011
require "./avram/object_extensions"
1112
require "./avram/criteria"
1213
require "./avram/type"

src/avram/database.cr

+33-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ abstract class Avram::Database
33

44
@@db : DB::Database? = nil
55
@@lock = Mutex.new
6-
class_getter transactions = {} of FiberId => DB::Transaction
6+
class_getter connections = {} of FiberId => DB::Connection
7+
class_property lock_id : UInt64?
78

89
macro inherited
910
Habitat.create do
@@ -24,6 +25,16 @@ abstract class Avram::Database
2425
%}
2526
end
2627

28+
def self.setup_connection(&block : DB::Connection -> Nil)
29+
new.db.setup_connection do |conn|
30+
block.call conn
31+
end
32+
end
33+
34+
def self.verify_connection
35+
new.connection.open.close
36+
end
37+
2738
# Rollback the current transaction
2839
def self.rollback
2940
new.rollback
@@ -142,28 +153,36 @@ abstract class Avram::Database
142153

143154
# :nodoc:
144155
def run
145-
yield current_transaction.try(&.connection) || db
156+
yield current_connection || db
146157
end
147158

148159
# :nodoc:
149160
def listen(*channels : String, &block : PQ::Notification ->) : Nil
150161
connection.connect_listen(*channels, &block)
151162
end
152163

153-
private def connection : Avram::Connection
164+
protected def connection : Avram::Connection
154165
Avram::Connection.new(url, database_class: self.class)
155166
end
156167

157-
private def db : DB::Database
168+
protected def db : DB::Database
158169
@@db ||= @@lock.synchronize do
159170
# check @@db again because a previous request could have set it after
160171
# the first time it was checked
161172
@@db || connection.open
162173
end
163174
end
164175

176+
private def current_connection : DB::Connection
177+
connections[object_id] ||= db.checkout
178+
end
179+
180+
private def object_id : UInt64
181+
self.class.lock_id || Fiber.current.object_id
182+
end
183+
165184
private def current_transaction : DB::Transaction?
166-
transactions[Fiber.current.object_id]?
185+
current_connection._avram_stack.last?
167186
end
168187

169188
protected def truncate
@@ -180,7 +199,7 @@ abstract class Avram::Database
180199

181200
# :nodoc:
182201
def transaction : Bool
183-
if current_transaction
202+
if current_transaction.try(&._avram_joinable?)
184203
yield
185204
true
186205
else
@@ -190,20 +209,23 @@ abstract class Avram::Database
190209
end
191210
end
192211

193-
private def transactions
194-
self.class.transactions
212+
private def connections
213+
self.class.connections
195214
end
196215

197216
private def wrap_in_transaction
198-
db.transaction do |tx|
199-
transactions[Fiber.current.object_id] ||= tx
217+
(current_transaction || current_connection).transaction do
200218
yield
201219
end
202220
true
203221
rescue e : Avram::Rollback
204222
false
205223
ensure
206-
transactions.delete(Fiber.current.object_id)
224+
# TODO: not sure of this
225+
if current_connection._avram_in_transaction?
226+
current_connection.release
227+
connections.delete(object_id)
228+
end
207229
end
208230

209231
class DatabaseCleaner

src/avram/migrator/migration.cr

+13-14
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,17 @@ abstract class Avram::Migrator::Migration::V1
6363
end
6464

6565
def migrated?
66-
DB.open(Avram::Migrator::Runner.database_url) do |db|
67-
db.query_one? "SELECT id FROM migrations WHERE version = $1", version, as: MigrationId
68-
end
66+
Avram.settings
67+
.database_to_migrate
68+
.query_one? "SELECT id FROM migrations WHERE version = $1", version, as: MigrationId
6969
end
7070

71-
private def track_migration(tx : DB::Transaction)
72-
tx.connection.exec "INSERT INTO migrations(version) VALUES ($1)", version
71+
private def track_migration(db : Avram::Database.class)
72+
db.exec "INSERT INTO migrations(version) VALUES ($1)", version
7373
end
7474

75-
private def untrack_migration(tx : DB::Transaction)
76-
tx.connection.exec "DELETE FROM migrations WHERE version = $1", version
75+
private def untrack_migration(db : Avram::Database.class)
76+
db.exec "DELETE FROM migrations WHERE version = $1", version
7777
end
7878

7979
private def execute(statement : String)
@@ -87,16 +87,15 @@ abstract class Avram::Migrator::Migration::V1
8787
# # Usage
8888
#
8989
# ```
90-
# execute_in_transaction ["DROP TABLE comments;"] do |tx|
91-
# tx.connection.exec "DROP TABLE users;"
90+
# execute_in_transaction ["DROP TABLE comments;"] do |db|
91+
# db.exec "DROP TABLE users;"
9292
# end
9393
# ```
9494
private def execute_in_transaction(statements : Array(String))
95-
DB.open(Avram::Migrator::Runner.database_url) do |db|
96-
db.transaction do |tx|
97-
statements.each { |s| tx.connection.exec s }
98-
yield tx
99-
end
95+
database = Avram.settings.database_to_migrate
96+
database.transaction do
97+
statements.each { |s| database.exec s }
98+
yield database
10099
end
101100
rescue e : PQ::PQError
102101
raise FailedMigration.new(migration: self.class.name, statements: statements, cause: e)

src/avram/migrator/runner.cr

+2-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Avram::Migrator::Runner
1111
class_getter migrations = [] of Avram::Migrator::Migration::V1.class
1212

1313
def initialize(@quiet : Bool = false)
14+
Avram::Log.dexter.configure(:none)
1415
end
1516

1617
def self.db_name
@@ -37,10 +38,6 @@ class Avram::Migrator::Runner
3738
Avram.settings.database_to_migrate.credentials
3839
end
3940

40-
def self.database_url
41-
credentials.url
42-
end
43-
4441
def self.cmd_args
4542
String.build do |args|
4643
args << "-U #{self.db_user}" if self.db_user
@@ -106,9 +103,7 @@ class Avram::Migrator::Runner
106103
end
107104

108105
def self.setup_migration_tracking_tables
109-
DB.open(database_url) do |db|
110-
db.exec create_table_for_tracking_migrations
111-
end
106+
Avram.settings.database_to_migrate.exec create_table_for_tracking_migrations
112107
end
113108

114109
private def self.create_table_for_tracking_migrations

src/avram/spec_helper.cr

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module Avram::SpecHelper
2+
TRUNCATE = "truncate"
3+
4+
macro use_transactional_specs(*databases)
5+
Spec.around_each do |spec|
6+
Avram::SpecHelper.wrap_spec_in_transaction(spec, {{ databases.splat }})
7+
end
8+
end
9+
10+
def self.wrap_spec_in_transaction(spec : Spec::Example::Procsy, *databases)
11+
if use_truncation?(spec)
12+
spec.run
13+
databases.each(&.truncate)
14+
return
15+
end
16+
17+
tracked_transactions = [] of DB::Transaction
18+
19+
databases.each do |database|
20+
database.lock_id = Fiber.current.object_id
21+
database.connections.values.each do |conn|
22+
tracked_transactions << conn.begin_transaction.tap(&._avram_joinable=(false))
23+
end
24+
25+
database.setup_connection do |conn|
26+
tracked_transactions << conn.begin_transaction.tap(&._avram_joinable=(false))
27+
end
28+
end
29+
30+
spec.run
31+
32+
tracked_transactions.each do |transaction|
33+
next if transaction.closed? || transaction.connection.closed?
34+
35+
transaction.rollback
36+
transaction.connection.release
37+
end
38+
tracked_transactions.clear
39+
databases.each do |database|
40+
database.connections.clear
41+
database.setup_connection { }
42+
end
43+
end
44+
45+
private def self.use_truncation?(spec : Spec::Example::Procsy) : Bool
46+
current = spec.example
47+
while !current.is_a?(Spec::RootContext)
48+
temp = current.as(Spec::Item)
49+
return true if temp.tags.try(&.includes?(TRUNCATE))
50+
current = temp.parent
51+
end
52+
53+
false
54+
end
55+
end

src/avram/tasks/db/verify_connection.cr

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ class Db::VerifyConnection < BaseTask
2020
end
2121

2222
def run_task
23-
DB.open(Avram::Migrator::Runner.database_url) do |_db|
24-
end
23+
Avram.settings.database_to_migrate.verify_connection
2524
puts "✔ Connection verified" unless quiet?
26-
rescue PQ::ConnectionError | DB::ConnectionRefused
25+
rescue Avram::ConnectionError
2726
raise <<-ERROR
2827
Unable to connect to Postgres for database '#{Avram.settings.database_to_migrate}'.
2928

src/ext/db/connection.cr

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module DB
2+
abstract class Connection
3+
# :nodoc:
4+
getter _avram_stack = [] of DB::Transaction
5+
6+
# :nodoc:
7+
def _avram_in_transaction? : Bool
8+
!_avram_stack.empty?
9+
end
10+
end
11+
end

src/ext/db/transaction.cr

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module DB
2+
abstract class Transaction
3+
# :nodoc:
4+
property? _avram_joinable = true
5+
end
6+
7+
class TopLevelTransaction < Transaction
8+
def initialize(@connection : Connection)
9+
@nested_transaction = false
10+
@connection.perform_begin_transaction
11+
@connection._avram_stack.push(self)
12+
end
13+
14+
protected def do_close
15+
@connection.release_from_transaction
16+
@connection._avram_stack.pop
17+
end
18+
end
19+
20+
class SavePointTransaction < Transaction
21+
def initialize(@parent : Transaction, @savepoint_name : String)
22+
@nested_transaction = false
23+
@connection = @parent.connection
24+
@connection.perform_create_savepoint(@savepoint_name)
25+
@connection._avram_stack.push(self)
26+
end
27+
28+
protected def do_close
29+
@parent.release_from_nested_transaction
30+
@connection._avram_stack.pop
31+
end
32+
end
33+
end

0 commit comments

Comments
 (0)