txnap 0.1.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 +7 -0
- data/Appraisals +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +233 -0
- data/Rakefile +9 -0
- data/benchmark/transaction.rb +54 -0
- data/gemfiles/rails_7_2.gemfile +7 -0
- data/gemfiles/rails_8_0.gemfile +7 -0
- data/gemfiles/rails_main.gemfile +5 -0
- data/lib/txnap/call_site.rb +31 -0
- data/lib/txnap/configuration.rb +66 -0
- data/lib/txnap/errors.rb +7 -0
- data/lib/txnap/execution_state.rb +28 -0
- data/lib/txnap/formatter.rb +50 -0
- data/lib/txnap/lock_heuristics.rb +93 -0
- data/lib/txnap/notifier.rb +51 -0
- data/lib/txnap/railtie.rb +9 -0
- data/lib/txnap/registry.rb +51 -0
- data/lib/txnap/report.rb +58 -0
- data/lib/txnap/sql_event_classifier.rb +26 -0
- data/lib/txnap/subscribers/sql_subscriber.rb +76 -0
- data/lib/txnap/subscribers/transaction_subscriber.rb +44 -0
- data/lib/txnap/trace.rb +124 -0
- data/lib/txnap/version.rb +5 -0
- data/lib/txnap.rb +82 -0
- data/script/event_probe.rb +94 -0
- metadata +202 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Txnap
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize(&warning_handler)
|
|
6
|
+
@warning_handler = warning_handler
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
@traces = {}.compare_by_identity
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def start(connection, trace)
|
|
12
|
+
stale_trace = @mutex.synchronize do
|
|
13
|
+
stale = @traces[connection]
|
|
14
|
+
@traces[connection] = trace
|
|
15
|
+
stale
|
|
16
|
+
end
|
|
17
|
+
@warning_handler&.call("discarded a stale trace before a new BEGIN") if stale_trace
|
|
18
|
+
trace
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fetch(connection)
|
|
22
|
+
@mutex.synchronize { @traces[connection] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def bind_transaction(connection, transaction)
|
|
26
|
+
return unless transaction
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize { @traces[connection]&.bind_transaction(transaction) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def finalize(connection, transaction)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
trace = @traces[connection]
|
|
34
|
+
return unless trace
|
|
35
|
+
matches_transaction = trace.matches_transaction?(transaction)
|
|
36
|
+
matches_terminal_without_binding = trace.transaction.nil? && trace.terminal?
|
|
37
|
+
return unless matches_transaction || matches_terminal_without_binding
|
|
38
|
+
|
|
39
|
+
@traces.delete(connection)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def size
|
|
44
|
+
@mutex.synchronize { @traces.size }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset!
|
|
48
|
+
@mutex.synchronize { @traces.clear }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/txnap/report.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Txnap
|
|
4
|
+
EventMetadata = Data.define(:sql, :name, :call_site) do
|
|
5
|
+
def to_h
|
|
6
|
+
{sql: sql, name: name, call_site: call_site}
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Gap = Data.define(:duration, :after, :before, :locks) do
|
|
11
|
+
def to_h
|
|
12
|
+
{
|
|
13
|
+
duration_ms: duration * 1000.0,
|
|
14
|
+
after: after&.to_h,
|
|
15
|
+
before: before&.to_h
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
LockCandidate = Data.define(:statement, :table, :kind, :call_site) do
|
|
21
|
+
def to_h
|
|
22
|
+
{statement: statement, table: table, kind: kind, call_site: call_site}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Report
|
|
27
|
+
attr_reader :outcome,
|
|
28
|
+
:transaction_duration,
|
|
29
|
+
:database_time,
|
|
30
|
+
:longest_gap,
|
|
31
|
+
:gaps,
|
|
32
|
+
:locks,
|
|
33
|
+
:sql_count
|
|
34
|
+
|
|
35
|
+
def initialize(outcome:, transaction_duration:, database_time:, gaps:, sql_count:)
|
|
36
|
+
@outcome = outcome
|
|
37
|
+
@transaction_duration = transaction_duration
|
|
38
|
+
@database_time = database_time
|
|
39
|
+
@gaps = gaps.freeze
|
|
40
|
+
@longest_gap = gaps.first
|
|
41
|
+
@locks = (@longest_gap&.locks || []).freeze
|
|
42
|
+
@sql_count = sql_count
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
outcome: outcome,
|
|
48
|
+
transaction_duration_ms: transaction_duration * 1000.0,
|
|
49
|
+
database_time_ms: database_time * 1000.0,
|
|
50
|
+
longest_gap_ms: longest_gap.duration * 1000.0,
|
|
51
|
+
sql_count: sql_count,
|
|
52
|
+
gap: longest_gap.to_h,
|
|
53
|
+
gaps: gaps.map(&:to_h),
|
|
54
|
+
locks: locks.map(&:to_h)
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Txnap
|
|
4
|
+
module SqlEventClassifier
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def classify(payload)
|
|
8
|
+
return :cache if payload[:cached] == true || payload[:name] == "CACHE"
|
|
9
|
+
return :schema if payload[:name] == "SCHEMA"
|
|
10
|
+
return :sql unless payload[:name] == "TRANSACTION"
|
|
11
|
+
|
|
12
|
+
classify_transaction_sql(payload[:sql])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def classify_transaction_sql(sql)
|
|
16
|
+
statement = Array(sql).join(" ").strip
|
|
17
|
+
|
|
18
|
+
return :begin if statement.match?(/\A(?:BEGIN\b|START\s+TRANSACTION\b)/i)
|
|
19
|
+
return :commit if statement.match?(/\ACOMMIT\b/i)
|
|
20
|
+
return :transaction if statement.match?(/\AROLLBACK\s+TO\b/i)
|
|
21
|
+
return :rollback if statement.match?(/\AROLLBACK\b/i)
|
|
22
|
+
|
|
23
|
+
:transaction
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Txnap
|
|
4
|
+
module Subscribers
|
|
5
|
+
class SqlSubscriber
|
|
6
|
+
class << self
|
|
7
|
+
def install
|
|
8
|
+
@subscription ||= begin
|
|
9
|
+
subscriber = new
|
|
10
|
+
ActiveSupport::Notifications.monotonic_subscribe("sql.active_record") do |*args|
|
|
11
|
+
subscriber.call(*args)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(_name, start, finish, _id, payload)
|
|
18
|
+
connection = payload[:connection]
|
|
19
|
+
return unless connection
|
|
20
|
+
|
|
21
|
+
case SqlEventClassifier.classify(payload)
|
|
22
|
+
when :cache, :schema
|
|
23
|
+
nil
|
|
24
|
+
when :begin
|
|
25
|
+
start_trace(connection, start, finish, payload)
|
|
26
|
+
when :commit, :rollback
|
|
27
|
+
registry.fetch(connection)&.record_terminal(start: start, finish: finish, sql: payload[:sql])
|
|
28
|
+
when :transaction
|
|
29
|
+
registry.fetch(connection)&.record_transaction(start: start, finish: finish, sql: payload[:sql])
|
|
30
|
+
when :sql
|
|
31
|
+
record_sql(connection, start, finish, payload)
|
|
32
|
+
end
|
|
33
|
+
rescue => error
|
|
34
|
+
Txnap.warn("sql subscriber failed: #{error.class}: #{error.message}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def registry
|
|
40
|
+
Txnap.registry
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def start_trace(connection, start, finish, payload)
|
|
44
|
+
registry.start(
|
|
45
|
+
connection,
|
|
46
|
+
Trace.new(start: start, finish: finish, sql: payload[:sql])
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def record_sql(connection, start, finish, payload)
|
|
51
|
+
trace = registry.fetch(connection)
|
|
52
|
+
return unless trace
|
|
53
|
+
|
|
54
|
+
call_site = capture_call_site(trace, start)
|
|
55
|
+
lock_candidate = LockHeuristics.detect(payload[:sql], call_site: call_site)
|
|
56
|
+
trace.record_sql(
|
|
57
|
+
start: start,
|
|
58
|
+
finish: finish,
|
|
59
|
+
sql: payload[:sql],
|
|
60
|
+
name: payload[:name],
|
|
61
|
+
call_site: call_site,
|
|
62
|
+
lock_candidate: lock_candidate
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def capture_call_site(trace, start)
|
|
67
|
+
case Txnap.configuration.capture_call_sites
|
|
68
|
+
when :always
|
|
69
|
+
CallSite.capture
|
|
70
|
+
when :sampled
|
|
71
|
+
CallSite.capture if trace.would_update_longest_gap?(start)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Txnap
|
|
4
|
+
module Subscribers
|
|
5
|
+
class TransactionSubscriber
|
|
6
|
+
class << self
|
|
7
|
+
def install
|
|
8
|
+
@subscriptions ||= begin
|
|
9
|
+
subscriber = new
|
|
10
|
+
[
|
|
11
|
+
ActiveSupport::Notifications.monotonic_subscribe("start_transaction.active_record") do |*args|
|
|
12
|
+
subscriber.start(*args)
|
|
13
|
+
end,
|
|
14
|
+
ActiveSupport::Notifications.monotonic_subscribe("transaction.active_record") do |*args|
|
|
15
|
+
subscriber.finish(*args)
|
|
16
|
+
end
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start(_name, _start, _finish, _id, payload)
|
|
23
|
+
connection = payload[:connection]
|
|
24
|
+
return unless connection
|
|
25
|
+
|
|
26
|
+
Txnap.registry.bind_transaction(connection, payload[:transaction])
|
|
27
|
+
rescue => error
|
|
28
|
+
Txnap.warn("transaction start subscriber failed: #{error.class}: #{error.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def finish(_name, _start, _finish, _id, payload)
|
|
32
|
+
connection = payload[:connection]
|
|
33
|
+
return unless connection
|
|
34
|
+
|
|
35
|
+
trace = Txnap.registry.finalize(connection, payload[:transaction])
|
|
36
|
+
Txnap.notify(trace&.to_report(payload[:outcome]))
|
|
37
|
+
rescue IdleGapDetected
|
|
38
|
+
raise
|
|
39
|
+
rescue => error
|
|
40
|
+
Txnap.warn("transaction finish subscriber failed: #{error.class}: #{error.message}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/txnap/trace.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Txnap
|
|
4
|
+
class Trace
|
|
5
|
+
TOP_GAPS_LIMIT = 3
|
|
6
|
+
LOCK_CANDIDATES_LIMIT = 20
|
|
7
|
+
SQL_LENGTH_LIMIT = 500
|
|
8
|
+
|
|
9
|
+
attr_reader :started_at, :database_time, :sql_count, :transaction
|
|
10
|
+
|
|
11
|
+
def initialize(start:, finish:, sql:)
|
|
12
|
+
@started_at = finish
|
|
13
|
+
@previous_finish = finish
|
|
14
|
+
@previous_event = metadata(sql, "TRANSACTION", nil)
|
|
15
|
+
@database_time = positive_duration(start, finish)
|
|
16
|
+
@sql_count = 0
|
|
17
|
+
@gaps = []
|
|
18
|
+
@lock_candidates = []
|
|
19
|
+
@transaction = nil
|
|
20
|
+
@ended_at = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def bind_transaction(transaction)
|
|
24
|
+
@transaction ||= transaction
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def matches_transaction?(candidate)
|
|
28
|
+
transaction && candidate && transaction.equal?(candidate)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def terminal?
|
|
32
|
+
!@ended_at.nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def would_update_longest_gap?(start)
|
|
36
|
+
gap_duration(start) > longest_gap_duration
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def record_sql(start:, finish:, sql:, name:, call_site:, lock_candidate: nil)
|
|
40
|
+
event = metadata(sql, name, call_site)
|
|
41
|
+
observe_event(start: start, finish: finish, event: event)
|
|
42
|
+
@sql_count += 1
|
|
43
|
+
add_lock_candidate(lock_candidate)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def record_transaction(start:, finish:, sql:)
|
|
47
|
+
observe_event(
|
|
48
|
+
start: start,
|
|
49
|
+
finish: finish,
|
|
50
|
+
event: metadata(sql, "TRANSACTION", nil)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def record_terminal(start:, finish:, sql:)
|
|
55
|
+
effective_start = [start, @previous_finish].max
|
|
56
|
+
@ended_at = effective_start
|
|
57
|
+
record_transaction(start: start, finish: finish, sql: sql)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_report(outcome)
|
|
61
|
+
return unless terminal?
|
|
62
|
+
|
|
63
|
+
Report.new(
|
|
64
|
+
outcome: outcome,
|
|
65
|
+
transaction_duration: positive_duration(started_at, @ended_at),
|
|
66
|
+
database_time: database_time,
|
|
67
|
+
gaps: @gaps.dup,
|
|
68
|
+
sql_count: sql_count
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def observe_event(start:, finish:, event:)
|
|
75
|
+
effective_start = [start, @previous_finish].max
|
|
76
|
+
effective_finish = [finish, effective_start].max
|
|
77
|
+
|
|
78
|
+
observe_gap(effective_start, event)
|
|
79
|
+
@database_time += effective_finish - effective_start
|
|
80
|
+
@previous_finish = effective_finish
|
|
81
|
+
@previous_event = event
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def observe_gap(next_start, next_event)
|
|
85
|
+
gap = Gap.new(
|
|
86
|
+
duration: gap_duration(next_start),
|
|
87
|
+
after: @previous_event,
|
|
88
|
+
before: next_event,
|
|
89
|
+
locks: @lock_candidates.dup.freeze
|
|
90
|
+
)
|
|
91
|
+
@gaps << gap
|
|
92
|
+
@gaps.sort_by! { |candidate| -candidate.duration }
|
|
93
|
+
@gaps.pop while @gaps.length > TOP_GAPS_LIMIT
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def gap_duration(next_start)
|
|
97
|
+
[next_start - @previous_finish, 0.0].max
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def longest_gap_duration
|
|
101
|
+
@gaps.first&.duration || 0.0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add_lock_candidate(candidate)
|
|
105
|
+
return unless candidate
|
|
106
|
+
return if @lock_candidates.any? do |existing|
|
|
107
|
+
existing.statement == candidate.statement && existing.table == candidate.table
|
|
108
|
+
end
|
|
109
|
+
return if @lock_candidates.length >= LOCK_CANDIDATES_LIMIT
|
|
110
|
+
|
|
111
|
+
@lock_candidates << candidate
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def metadata(sql, name, call_site)
|
|
115
|
+
normalized_sql = Array(sql).join(" ").gsub(/\s+/, " ").strip
|
|
116
|
+
normalized_sql = "#{normalized_sql[0, SQL_LENGTH_LIMIT - 1]}…" if normalized_sql.length > SQL_LENGTH_LIMIT
|
|
117
|
+
EventMetadata.new(sql: normalized_sql.freeze, name: name, call_site: call_site).freeze
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def positive_duration(start, finish)
|
|
121
|
+
[finish - start, 0.0].max
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/txnap.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/isolated_execution_state"
|
|
7
|
+
require "active_support/notifications"
|
|
8
|
+
|
|
9
|
+
require_relative "txnap/version"
|
|
10
|
+
require_relative "txnap/errors"
|
|
11
|
+
require_relative "txnap/configuration"
|
|
12
|
+
require_relative "txnap/execution_state"
|
|
13
|
+
require_relative "txnap/sql_event_classifier"
|
|
14
|
+
require_relative "txnap/call_site"
|
|
15
|
+
require_relative "txnap/lock_heuristics"
|
|
16
|
+
require_relative "txnap/report"
|
|
17
|
+
require_relative "txnap/trace"
|
|
18
|
+
require_relative "txnap/registry"
|
|
19
|
+
require_relative "txnap/formatter"
|
|
20
|
+
require_relative "txnap/notifier"
|
|
21
|
+
require_relative "txnap/subscribers/sql_subscriber"
|
|
22
|
+
require_relative "txnap/subscribers/transaction_subscriber"
|
|
23
|
+
|
|
24
|
+
module Txnap
|
|
25
|
+
class << self
|
|
26
|
+
def configure
|
|
27
|
+
yield(configuration)
|
|
28
|
+
configuration.validate!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configuration
|
|
32
|
+
@configuration ||= Configuration.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def registry
|
|
36
|
+
@registry ||= Registry.new { |message| warn(message) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def install!
|
|
40
|
+
return false if @installed
|
|
41
|
+
|
|
42
|
+
Subscribers::SqlSubscriber.install
|
|
43
|
+
Subscribers::TransactionSubscriber.install
|
|
44
|
+
@installed = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def suppress(&block)
|
|
48
|
+
ExecutionState.suppress(&block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def logger
|
|
52
|
+
configuration.logger || rails_logger || default_logger
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def warn(message)
|
|
56
|
+
logger.warn("[txnap] #{message}")
|
|
57
|
+
rescue
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def notify(report)
|
|
62
|
+
Notifier.new(configuration: configuration, logger: logger).call(report)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reset!
|
|
66
|
+
@configuration = Configuration.new
|
|
67
|
+
registry.reset!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def rails_logger
|
|
73
|
+
Rails.logger if defined?(Rails) && Rails.respond_to?(:logger)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def default_logger
|
|
77
|
+
@default_logger ||= Logger.new($stderr)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
require_relative "txnap/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
active_record_version = ARGV.fetch(0) { ENV.fetch("ACTIVE_RECORD_VERSION") }
|
|
6
|
+
adapter = ARGV.fetch(1) { ENV.fetch("ADAPTER", "sqlite3") }
|
|
7
|
+
|
|
8
|
+
gem "activerecord", active_record_version
|
|
9
|
+
gem (adapter == "postgresql") ? "pg" : "sqlite3"
|
|
10
|
+
|
|
11
|
+
require "active_record"
|
|
12
|
+
require "active_support/notifications"
|
|
13
|
+
|
|
14
|
+
database_config =
|
|
15
|
+
if adapter == "postgresql"
|
|
16
|
+
{
|
|
17
|
+
adapter: "postgresql",
|
|
18
|
+
url: ARGV.fetch(2) do
|
|
19
|
+
ENV.fetch("DATABASE_URL", "postgresql://postgres:postgres@127.0.0.1:55433/txnap")
|
|
20
|
+
end
|
|
21
|
+
}
|
|
22
|
+
else
|
|
23
|
+
{adapter: "sqlite3", database: ":memory:"}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
ActiveRecord::Base.establish_connection(database_config)
|
|
27
|
+
connection = ActiveRecord::Base.connection
|
|
28
|
+
connection.create_table(:event_probe_records, force: true) do |table|
|
|
29
|
+
table.string :name, null: false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class EventProbeRecord < ActiveRecord::Base
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
events = []
|
|
36
|
+
subscriptions = %w[
|
|
37
|
+
sql.active_record
|
|
38
|
+
start_transaction.active_record
|
|
39
|
+
transaction.active_record
|
|
40
|
+
].map do |event_name|
|
|
41
|
+
ActiveSupport::Notifications.monotonic_subscribe(event_name) do |name, start, finish, _id, payload|
|
|
42
|
+
events << {
|
|
43
|
+
event: name,
|
|
44
|
+
start: start,
|
|
45
|
+
finish: finish,
|
|
46
|
+
duration_ms: (finish - start) * 1000,
|
|
47
|
+
name: payload[:name],
|
|
48
|
+
sql: payload[:sql],
|
|
49
|
+
cached: payload[:cached],
|
|
50
|
+
outcome: payload[:outcome],
|
|
51
|
+
connection_id: payload[:connection]&.object_id,
|
|
52
|
+
transaction_id: payload[:transaction]&.object_id
|
|
53
|
+
}.compact
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
ActiveRecord::Base.transaction do
|
|
58
|
+
sleep 0.01
|
|
59
|
+
EventProbeRecord.create!(name: "commit")
|
|
60
|
+
|
|
61
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
|
62
|
+
EventProbeRecord.create!(name: "savepoint")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
connection.cache do
|
|
66
|
+
EventProbeRecord.first
|
|
67
|
+
EventProbeRecord.first
|
|
68
|
+
end
|
|
69
|
+
sleep 0.01
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
ActiveRecord::Base.transaction do
|
|
74
|
+
EventProbeRecord.create!(name: "rollback")
|
|
75
|
+
raise ActiveRecord::Rollback
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
ActiveRecord::Base.transaction do
|
|
79
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
|
80
|
+
EventProbeRecord.create!(name: "restart")
|
|
81
|
+
raise ActiveRecord::Rollback
|
|
82
|
+
end
|
|
83
|
+
EventProbeRecord.create!(name: "after restart")
|
|
84
|
+
end
|
|
85
|
+
ensure
|
|
86
|
+
subscriptions.each { |subscription| ActiveSupport::Notifications.unsubscribe(subscription) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
puts JSON.pretty_generate(
|
|
90
|
+
active_record: ActiveRecord.version.to_s,
|
|
91
|
+
adapter: connection.adapter_name,
|
|
92
|
+
connection_id: connection.object_id,
|
|
93
|
+
events: events
|
|
94
|
+
)
|