awesome_explain 0.3.0 → 1.0.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 +5 -5
- data/.github/workflows/mongodb.yml +53 -0
- data/.github/workflows/postgres.yml +56 -0
- data/.gitignore +11 -0
- data/Appraisals +11 -0
- data/Gemfile.lock +209 -49
- data/LICENSE.txt +4 -20
- data/README.md +155 -7
- data/Rakefile +35 -1
- data/app/models/awesome_explain/application_record.rb +5 -0
- data/app/models/awesome_explain/controller.rb +20 -0
- data/app/models/awesome_explain/delayed_job.rb +7 -0
- data/app/models/awesome_explain/explain.rb +23 -0
- data/app/models/awesome_explain/log.rb +7 -0
- data/app/models/awesome_explain/pg_dml_stat.rb +4 -0
- data/app/models/awesome_explain/pg_seq_scan.rb +4 -0
- data/app/models/awesome_explain/plan_node.rb +52 -0
- data/app/models/awesome_explain/plan_tree.rb +66 -0
- data/app/models/awesome_explain/sidekiq_worker.rb +7 -0
- data/app/models/awesome_explain/sql_explain.rb +14 -0
- data/app/models/awesome_explain/sql_plan_node.rb +73 -0
- data/app/models/awesome_explain/sql_plan_stats.rb +34 -0
- data/app/models/awesome_explain/sql_plan_tree.rb +133 -0
- data/app/models/awesome_explain/sql_query.rb +7 -0
- data/app/models/awesome_explain/stacktrace.rb +11 -0
- data/awesome_explain.gemspec +16 -5
- data/bin/rails +14 -0
- data/data/mongodb/customers.bson +0 -0
- data/data/mongodb/customers.metadata.json +1 -0
- data/data/mongodb/line_items.bson +0 -0
- data/data/mongodb/line_items.metadata.json +1 -0
- data/data/mongodb/orders.bson +0 -0
- data/data/mongodb/orders.metadata.json +1 -0
- data/data/mongodb/products.bson +0 -0
- data/data/mongodb/products.metadata.json +1 -0
- data/data/postgresql/dvdrental.tar +0 -0
- data/db/migrate/20200507214801_stacktraces.rb +12 -0
- data/db/migrate/20200507214949_controllers.rb +16 -0
- data/db/migrate/20200507215205_logs.rb +22 -0
- data/db/migrate/20200507215243_explains.rb +27 -0
- data/gemfiles/rails_4.gemfile +7 -0
- data/gemfiles/rails_4.gemfile.lock +208 -0
- data/gemfiles/rails_5.gemfile +7 -0
- data/gemfiles/rails_5.gemfile.lock +209 -0
- data/gemfiles/rails_6.gemfile +7 -0
- data/gemfiles/rails_6.gemfile.lock +233 -0
- data/images/universe.png +0 -0
- data/lib/awesome_explain.rb +79 -2
- data/lib/awesome_explain/config.rb +196 -0
- data/lib/awesome_explain/engine.rb +5 -0
- data/lib/awesome_explain/insights/active_record_insights.rb +137 -0
- data/lib/awesome_explain/insights/base.rb +18 -0
- data/lib/awesome_explain/insights/mongoid_insights.rb +44 -0
- data/lib/awesome_explain/insights/sql_plans_insights.rb +64 -0
- data/lib/awesome_explain/kernel.rb +17 -0
- data/lib/awesome_explain/mongodb/base.rb +4 -0
- data/lib/awesome_explain/mongodb/command_start.rb +84 -0
- data/lib/awesome_explain/mongodb/command_success.rb +58 -0
- data/lib/awesome_explain/mongodb/formatter.rb +62 -0
- data/lib/awesome_explain/mongodb/helpers.rb +71 -0
- data/lib/awesome_explain/queue/command.rb +17 -0
- data/lib/awesome_explain/queue/simple_queue.rb +88 -0
- data/lib/awesome_explain/renderers/active_record.rb +114 -0
- data/lib/awesome_explain/renderers/base.rb +2 -0
- data/lib/awesome_explain/renderers/mongoid.rb +20 -33
- data/lib/awesome_explain/sidekiq_middleware.rb +17 -0
- data/lib/awesome_explain/stats/postgresql.rb +54 -0
- data/lib/awesome_explain/subscribers/active_record_passive_subscriber.rb +82 -0
- data/lib/awesome_explain/subscribers/active_record_subscriber.rb +187 -0
- data/lib/awesome_explain/subscribers/base.rb +3 -0
- data/lib/awesome_explain/subscribers/command_subscriber.rb +53 -0
- data/lib/awesome_explain/tasks/db.rb +325 -0
- data/lib/awesome_explain/utils/color.rb +16 -0
- data/lib/awesome_explain/version.rb +1 -1
- data/lib/tasks/ae.rake +28 -0
- data/lib/tasks/awesome_explain_tasks.rake +4 -0
- metadata +242 -25
- data/.travis.yml +0 -19
@@ -0,0 +1,44 @@
|
|
1
|
+
module AwesomeExplain::Insights
|
2
|
+
class MongoidInsights
|
3
|
+
attr_accessor :command_subscriber, :metrics
|
4
|
+
|
5
|
+
def self.analyze(metrics = [], &block)
|
6
|
+
instance = new
|
7
|
+
instance.init
|
8
|
+
instance.metrics = metrics
|
9
|
+
block_result = instance.instance_eval(&block)
|
10
|
+
instance.tear_down
|
11
|
+
block_result unless metrics.size.positive?
|
12
|
+
end
|
13
|
+
|
14
|
+
def init
|
15
|
+
command_subscribers = Mongoid.default_client.send(:monitoring).subscribers['Command'] || Mongo::Monitoring::Global.subscribers['Command']
|
16
|
+
@command_subscriber = command_subscribers.select do |s|
|
17
|
+
s.class == AwesomeExplain::CommandSubscriber
|
18
|
+
end.first
|
19
|
+
@command_subscriber.clear
|
20
|
+
Thread.current['ae_analyze'] = true
|
21
|
+
Thread.current['ae_source'] = 'console'
|
22
|
+
end
|
23
|
+
|
24
|
+
def tear_down
|
25
|
+
if @command_subscriber.nil?
|
26
|
+
puts 'Configure the command subscriber then try again.'
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
if @metrics.size.positive?
|
31
|
+
result = {}
|
32
|
+
@metrics.each do |m|
|
33
|
+
result[m] = @command_subscriber.get(m)
|
34
|
+
end
|
35
|
+
|
36
|
+
@command_subscriber.clear
|
37
|
+
return result
|
38
|
+
else
|
39
|
+
@command_subscriber.clear
|
40
|
+
end
|
41
|
+
Thread.current['ae_analyze'] = false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module AwesomeExplain::Insights
|
2
|
+
class SqlPlansInsights
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_accessor :plans_stats, :queries
|
6
|
+
attr_reader :mutex
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@plans_stats = []
|
10
|
+
@queries = []
|
11
|
+
@mutex = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(plan_stats)
|
15
|
+
with_mutex { @plans_stats << plan_stats }
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_query(query)
|
19
|
+
with_mutex {
|
20
|
+
query = Niceql::Prettifier.prettify_sql query
|
21
|
+
@queries << query
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def plans_stats
|
26
|
+
with_mutex { @plans_stats }
|
27
|
+
end
|
28
|
+
|
29
|
+
def queries
|
30
|
+
with_mutex { @queries }
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear
|
34
|
+
plans_stats.clear
|
35
|
+
queries.clear
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.add(plan_stats)
|
39
|
+
instance.add(plan_stats)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.add_query(query)
|
43
|
+
instance.add_query(query)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.plans_stats
|
47
|
+
instance.plans_stats
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.queries
|
51
|
+
instance.queries
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.clear
|
55
|
+
instance.clear
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def with_mutex
|
61
|
+
@mutex.synchronize { yield }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,9 +1,21 @@
|
|
1
1
|
module Kernel
|
2
2
|
def ae(query)
|
3
3
|
return AwesomeExplain::Renderers::Mongoid.new(query).print if mongoid_query?(query)
|
4
|
+
return AwesomeExplain::Renderers::ActiveRecord.new(query).print if active_record_query?(query)
|
5
|
+
|
4
6
|
query
|
5
7
|
end
|
6
8
|
|
9
|
+
def analyze(&block)
|
10
|
+
::AwesomeExplain::MongoiddInsights.analyze(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def analyze_ar(options = {}, &block)
|
14
|
+
Thread.current['ae_analyze'] = true
|
15
|
+
Thread.current['ae_source'] = 'console'
|
16
|
+
::AwesomeExplain::Insights::ActiveRecordInsights.analyze(options, &block)
|
17
|
+
end
|
18
|
+
|
7
19
|
private
|
8
20
|
|
9
21
|
def mongoid_query?(query)
|
@@ -11,4 +23,9 @@ module Kernel
|
|
11
23
|
defined?(Mongoid::Criteria) &&
|
12
24
|
(query.is_a?(Mongo::Collection::View::Aggregation) || query.is_a?(Mongoid::Criteria))
|
13
25
|
end
|
26
|
+
|
27
|
+
def active_record_query?(query)
|
28
|
+
defined?(ActiveRecord::Relation) &&
|
29
|
+
query.is_a?(ActiveRecord::Relation)
|
30
|
+
end
|
14
31
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module AwesomeExplain::Mongodb
|
2
|
+
module CommandStart
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def handle_command_start(event)
|
7
|
+
command = event.command
|
8
|
+
command_name = event.command_name.to_sym
|
9
|
+
|
10
|
+
if db_explain_enabled?(command_name)
|
11
|
+
case AwesomeExplain::Config.instance.enabled?
|
12
|
+
when Rails.env.development?
|
13
|
+
handle_command_start_sync(event)
|
14
|
+
when Rails.env.staging?
|
15
|
+
handle_command_start_async(event)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def process_command_start(event)
|
21
|
+
command = event.command
|
22
|
+
command_name = event.command_name.to_sym
|
23
|
+
|
24
|
+
begin
|
25
|
+
request_id = event.request_id
|
26
|
+
if command_name == :getMore
|
27
|
+
collection_name = event.command['collection']
|
28
|
+
else
|
29
|
+
collection_name = event.command[event.command_name]
|
30
|
+
end
|
31
|
+
@stats[:collections][collection_name] = Hash.new(0) if !@stats[:collections].include?(collection_name)
|
32
|
+
@stats[:collections][collection_name][command_name] += 1
|
33
|
+
@queries[request_id] = {
|
34
|
+
command_name: event.command_name,
|
35
|
+
command: command.include?('pipeline') ? command['pipeline'] : command.select {|k, v| COMMAND_ALLOWED_KEYS.include?(k)},
|
36
|
+
collection_name: collection_name,
|
37
|
+
stacktrace: caller,
|
38
|
+
lsid: command.dig('lsid', 'id').to_json
|
39
|
+
}.with_indifferent_access
|
40
|
+
|
41
|
+
command = event.command
|
42
|
+
if command.include?('aggregate')
|
43
|
+
command = {
|
44
|
+
'aggregate': command['aggregate'],
|
45
|
+
'pipeline': command['pipeline'],
|
46
|
+
'cursor': command['cursor'],
|
47
|
+
}
|
48
|
+
end
|
49
|
+
r = ::AwesomeExplain::Renderers::Mongoid.new(nil, Mongoid.default_client.database.command({explain: command}).documents.first)
|
50
|
+
exp = AwesomeExplain::Explain.create({
|
51
|
+
collection: collection_name,
|
52
|
+
source_name: AwesomeExplain::Config.instance.app_name,
|
53
|
+
command: @queries[request_id][:command].to_json,
|
54
|
+
winning_plan: r.winning_plan_data.first,
|
55
|
+
winning_plan_raw: r.winning_plan.to_json,
|
56
|
+
used_indexes: r.winning_plan_data.last.join(', '),
|
57
|
+
duration: (r.execution_stats&.dig('executionTimeMillis').to_f/1000).round(5),
|
58
|
+
documents_returned: r.execution_stats&.dig('nReturned'),
|
59
|
+
documents_examined: r.execution_stats&.dig('totalDocsExamined'),
|
60
|
+
keys_examined: r.execution_stats&.dig('totalKeysExamined'),
|
61
|
+
rejected_plans: r.rejected_plans&.size,
|
62
|
+
session_id: Thread.current[:ae_session_id],
|
63
|
+
lsid: @queries[request_id][:lsid],
|
64
|
+
stacktrace_id: resolve_stracktrace_id(request_id),
|
65
|
+
controller_id: resolve_controller_id,
|
66
|
+
})
|
67
|
+
@queries[request_id][:explain_id] = exp&.id
|
68
|
+
@queries[request_id][:collscan] = exp&.collscan
|
69
|
+
rescue => exception
|
70
|
+
logger.warn exception.to_s
|
71
|
+
logger.warn exception.backtrace[0..5]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_command_start_sync(event)
|
76
|
+
# process_command_start(event)
|
77
|
+
::AwesomeExplain::Config.instance.queue << ::AwesomeExplain::Queue::Command.new(:process_command_start, event, self)
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_command_start_async(event)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module AwesomeExplain::Mongodb
|
2
|
+
module CommandSuccess
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def handle_command_success(event)
|
7
|
+
if db_logging_enbled?
|
8
|
+
case AwesomeExplain::Config.instance.enabled?
|
9
|
+
when Rails.env.development?
|
10
|
+
handle_command_success_sync(event)
|
11
|
+
when Rails.env.staging?
|
12
|
+
handle_command_success_async(event)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def process_command_success(event)
|
18
|
+
begin
|
19
|
+
command_name = event.command_name.to_sym
|
20
|
+
request_id = event.request_id
|
21
|
+
duration = event.duration.round(5)
|
22
|
+
@stats[:performed_queries][command_name] += 1
|
23
|
+
@stats[:total_duration] += duration
|
24
|
+
@queries[request_id][:duration] = duration
|
25
|
+
|
26
|
+
log = {
|
27
|
+
operation: command_name,
|
28
|
+
app_name: AwesomeExplain::Config.instance.app_name,
|
29
|
+
source_name: resolve_source_name,
|
30
|
+
collscan: @queries[request_id][:collscan],
|
31
|
+
collection: @queries[request_id][:collection_name],
|
32
|
+
duration: duration,
|
33
|
+
command: @queries[request_id][:command].to_json,
|
34
|
+
session_id: Thread.current[:ae_session_id],
|
35
|
+
lsid: @queries[request_id][:lsid],
|
36
|
+
stacktrace_id: resolve_stracktrace_id(request_id),
|
37
|
+
explain_id: @queries[request_id][:explain_id],
|
38
|
+
controller_id: resolve_controller_id,
|
39
|
+
sidekiq_worker_id: resolve_sidekiq_class_id,
|
40
|
+
}
|
41
|
+
AwesomeExplain::Log.create(log)
|
42
|
+
rescue => exception
|
43
|
+
logger.warn exception.to_s
|
44
|
+
logger.warn exception.backtrace[0..5]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_command_success_sync(event)
|
49
|
+
# process_command_success(event)
|
50
|
+
::AwesomeExplain::Config.instance.queue << ::AwesomeExplain::Queue::Command.new(:process_command_success, event, self)
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_command_success_async(event)
|
54
|
+
# TODO: How to handle using Queues
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module AwesomeExplain::Mongodb
|
2
|
+
module Formatter
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def stats_table
|
7
|
+
table = Terminal::Table.new(title: 'Query Stats') do |t|
|
8
|
+
t << [
|
9
|
+
'Total Duration',
|
10
|
+
@stats[:total_duration] >= 1 ? "#{@stats[:total_duration]} seconds".purpleish : "#{@stats[:total_duration]} seconds".green
|
11
|
+
]
|
12
|
+
t << :separator
|
13
|
+
t << [
|
14
|
+
total_performed_queries >= 100 ? "Performed Queries [#{total_performed_queries}]".purpleish : "Performed Queries [#{total_performed_queries}]".green,
|
15
|
+
formatted_performed_queries
|
16
|
+
]
|
17
|
+
t << :separator
|
18
|
+
t << ['Collections Queried', formatted_collections]
|
19
|
+
|
20
|
+
sq = slowest_query
|
21
|
+
if sq[:duration] >= 0.5
|
22
|
+
t << :separator
|
23
|
+
t << ["Slowest Query [#{sq.dig(:duration)}]".purpleish, sq.dig(:command).inspect]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
puts table
|
27
|
+
end
|
28
|
+
|
29
|
+
def slowest_query
|
30
|
+
@queries.sort_by {|id, data| data[:duration]}.last[1]
|
31
|
+
end
|
32
|
+
|
33
|
+
def total_performed_queries
|
34
|
+
@stats[:performed_queries].sum {|op, count| count}
|
35
|
+
end
|
36
|
+
|
37
|
+
def formatted_performed_queries
|
38
|
+
find = @stats[:performed_queries][:find]
|
39
|
+
count = @stats[:performed_queries][:count]
|
40
|
+
distinct = @stats[:performed_queries][:distinct]
|
41
|
+
update = @stats[:performed_queries][:update]
|
42
|
+
insert = @stats[:performed_queries][:insert]
|
43
|
+
get_more = @stats[:performed_queries][:getMore]
|
44
|
+
delete = @stats[:performed_queries][:delete]
|
45
|
+
|
46
|
+
QUERIES.reject { |q| !@stats[:performed_queries][q].positive? }.sort_by {|q| @stats[:performed_queries][q]}.reverse.map do |q|
|
47
|
+
@stats[:performed_queries][q] >= 10 ? "#{q}: #{@stats[:performed_queries][q]}".purpleish : "#{q}: #{@stats[:performed_queries][q]}"
|
48
|
+
end.join(', ')
|
49
|
+
end
|
50
|
+
|
51
|
+
def formatted_collections
|
52
|
+
@stats[:collections].sort_by {|c, values| values.sum {|k, v| v}}.reverse.each.map do |data|
|
53
|
+
c = data[0]
|
54
|
+
sum = @stats[:collections][c].sum {|k, v| v}
|
55
|
+
cmds = @stats[:collections][c].map {|cmd, count| "#{cmd} [#{count}]"}.join(', ')
|
56
|
+
cmds = "#{c}: #{cmds}"
|
57
|
+
sum >= 10 ? cmds.purpleish : cmds
|
58
|
+
end.join("\n")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module AwesomeExplain::Mongodb
|
2
|
+
module Helpers
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def resolve_stracktrace_id(request_id)
|
7
|
+
stacktrace_str = @queries[request_id][:stacktrace]
|
8
|
+
.select {|c| c =~ /^#{Rails.root.to_s + '\/(lib|app|db)\/'}/ }
|
9
|
+
.map {|c| c.gsub Rails.root.to_s, ''}
|
10
|
+
.to_json
|
11
|
+
stacktrace = AwesomeExplain::Stacktrace.find_or_create_by({
|
12
|
+
stacktrace: stacktrace_str
|
13
|
+
})
|
14
|
+
|
15
|
+
stacktrace.id
|
16
|
+
end
|
17
|
+
|
18
|
+
def resolve_controller_id
|
19
|
+
data = controller_data
|
20
|
+
return nil unless data.present?
|
21
|
+
AwesomeExplain::Controller.find_or_create_by({
|
22
|
+
name: controller_data[:controller],
|
23
|
+
action: controller_data[:action],
|
24
|
+
path: controller_data[:path],
|
25
|
+
params: controller_data[:params].to_json,
|
26
|
+
session_id: Thread.current['ae_session_id']
|
27
|
+
}).id
|
28
|
+
end
|
29
|
+
|
30
|
+
def resolve_sidekiq_class_id
|
31
|
+
return unless Thread.current[:sidekiq_worker_class].present?
|
32
|
+
sidekiq_worker_class_str = Thread.current[:sidekiq_worker_class]
|
33
|
+
sidekiq_queue_str = Thread.current[:sidekiq_queue].to_s
|
34
|
+
sidekiq_worker = AwesomeExplain::SidekiqWorker.find_or_create_by({
|
35
|
+
worker: sidekiq_worker_class_str,
|
36
|
+
queue: sidekiq_queue_str,
|
37
|
+
jid: extract_sidekiq_jid(Thread.current[:sidekiq_job]),
|
38
|
+
params: Thread.current[:sidekiq_job].present? ? Thread.current[:sidekiq_job].to_json : {}
|
39
|
+
})
|
40
|
+
|
41
|
+
sidekiq_worker.id
|
42
|
+
end
|
43
|
+
|
44
|
+
def controller_data
|
45
|
+
Thread.current['ae_controller_data']
|
46
|
+
end
|
47
|
+
|
48
|
+
def extract_sidekiq_jid(args)
|
49
|
+
Thread.current[:sidekiq_job].dig('jid')
|
50
|
+
end
|
51
|
+
|
52
|
+
def resolve_source_name
|
53
|
+
Thread.current['ae_source'] || DEFAULT_SOURCE_NAME
|
54
|
+
end
|
55
|
+
|
56
|
+
def db_explain_enabled?(command_name)
|
57
|
+
return false if DML_COMMANDS.include?(command_name)
|
58
|
+
return false if command_name == :getMore
|
59
|
+
return true if Thread.current['ae_analyze']
|
60
|
+
return false if Rails.const_defined?('Console')
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def db_logging_enbled?
|
65
|
+
return true if Thread.current['ae_analyze']
|
66
|
+
return false if Rails.const_defined?('Console')
|
67
|
+
true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AwesomeExplain::Queue
|
2
|
+
class Command
|
3
|
+
attr_writer :method_name
|
4
|
+
attr_writer :event
|
5
|
+
attr_writer :object
|
6
|
+
|
7
|
+
def initialize(method_name, event, object)
|
8
|
+
@method_name = method_name
|
9
|
+
@event = event
|
10
|
+
@object = object
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
object.send method_name, event
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|