groonga-query-log 1.0.1 → 1.0.2

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.
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2011-2012 Kouhei Sutou <kou@clear-code.com>
3
+ # Copyright (C) 2011-2013 Kouhei Sutou <kou@clear-code.com>
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
@@ -38,6 +38,7 @@ module Groonga
38
38
  def parse(input, &block)
39
39
  current_statistics = {}
40
40
  input.each_line do |line|
41
+ next unless line.valid_encoding?
41
42
  case line
42
43
  when /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\.(\d+)\|(.+?)\|([>:<])/
43
44
  year, month, day, hour, minutes, seconds, micro_seconds =
@@ -72,7 +73,7 @@ module Groonga
72
73
  :elapsed => elapsed.to_i,
73
74
  :n_records => n_records)
74
75
  when "<"
75
- return unless /\A(\d+) rc=(\d+)/ =~ rest
76
+ return unless /\A(\d+) rc=(-?\d+)/ =~ rest
76
77
  elapsed = $1
77
78
  return_code = $2
78
79
  statistic = current_statistics.delete(context_id)
@@ -0,0 +1,196 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
9
+ #
10
+ # This library is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public
16
+ # License along with this library; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
+
19
+ require "time"
20
+ require "thread"
21
+
22
+ require "groonga/client"
23
+
24
+ require "groonga/query-log/parser"
25
+
26
+ module Groonga
27
+ module QueryLog
28
+ class Replayer
29
+ def initialize(options)
30
+ @options = options
31
+ @queue = SizedQueue.new(@options.request_queue_size)
32
+ @responses = Queue.new
33
+ end
34
+
35
+ def replay(input)
36
+ producer = run_producer(input)
37
+ consumers = run_consumers
38
+ response_logger = run_response_logger
39
+ producer.join
40
+ consumers.each(&:join)
41
+ response_logger.join
42
+ end
43
+
44
+ private
45
+ def run_producer(input)
46
+ Thread.new do
47
+ parser = Parser.new
48
+ id = 0
49
+ @options.create_request_output do |output|
50
+ parser.parse(input) do |statistic|
51
+ next unless target_command?(statistic.command)
52
+ # TODO: validate orignal_source is one line
53
+ output.puts(statistic.command.original_source)
54
+ @queue.push([id, statistic])
55
+ id += 1
56
+ end
57
+ end
58
+ @options.n_clients.times do
59
+ @queue.push(nil)
60
+ end
61
+ end
62
+ end
63
+
64
+ def run_consumers
65
+ @options.n_clients.times.collect do
66
+ Thread.new do
67
+ loop do
68
+ break if run_consumer
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def run_consumer
75
+ @options.create_client do |client|
76
+ loop do
77
+ id, statistic = @queue.pop
78
+ if id.nil?
79
+ @responses.push(nil)
80
+ return true
81
+ end
82
+ begin
83
+ replay_command(client, id, statistic.command)
84
+ rescue Groonga::Client::Connection::Error
85
+ # TODO: add error log mechanism
86
+ $stderr.puts(Time.now.iso8601)
87
+ $stderr.puts(statistic.command.original_source)
88
+ $stderr.puts($!.raw_error.message)
89
+ $stderr.puts($!.raw_error.backtrace)
90
+ return false
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def replay_command(client, id, command)
97
+ command["cache"] = "no" if @options.disable_cache?
98
+ response = client.execute(command)
99
+ @responses.push(response)
100
+ end
101
+
102
+ def run_response_logger
103
+ Thread.new do
104
+ @options.create_responses_output do |output|
105
+ loop do
106
+ response = @responses.pop
107
+ break if response.nil?
108
+ # TODO: ensure response is one line
109
+ # TODO: reorder by ID
110
+ output.puts(response.raw)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def target_command?(command)
117
+ @options.target_command_name?(command.name)
118
+ end
119
+
120
+ class NullOutput
121
+ class << self
122
+ def open
123
+ output = new
124
+ if block_given?
125
+ yield(output)
126
+ else
127
+ output
128
+ end
129
+ end
130
+ end
131
+
132
+ def puts(string)
133
+ end
134
+ end
135
+
136
+ class Options
137
+ attr_accessor :host
138
+ attr_accessor :port
139
+ attr_accessor :protocol
140
+ attr_accessor :n_clients
141
+ attr_writer :request_queue_size
142
+ attr_accessor :target_command_names
143
+ def initialize
144
+ @host = "127.0.0.1"
145
+ @port = 10041
146
+ @protocol = :gqtp
147
+ @n_clients = 8
148
+ @request_queue_size = nil
149
+ @disable_cache = false
150
+ @requests_path = nil
151
+ @responses_path = nil
152
+ @target_command_names = ["*"]
153
+ end
154
+
155
+ def create_client(&block)
156
+ Groonga::Client.open(:host => @host,
157
+ :port => @port,
158
+ :protocol => @protocol,
159
+ &block)
160
+ end
161
+
162
+ def create_request_output(&block)
163
+ if @requests_path
164
+ File.open(@requests_path, "w", &block)
165
+ else
166
+ NullOutput.open(&block)
167
+ end
168
+ end
169
+
170
+ def create_responses_output(&block)
171
+ if @responses_path
172
+ File.open(@responses_path, "w", &block)
173
+ else
174
+ NullOutput.open(&block)
175
+ end
176
+ end
177
+
178
+ def request_queue_size
179
+ @request_queue_size || @n_clients * 3
180
+ end
181
+
182
+ def disable_cache?
183
+ @disable_cache
184
+ end
185
+
186
+ def target_command_name?(name)
187
+ @target_command_names.any? do |name_pattern|
188
+ flags = 0
189
+ flags |= File::FNM_EXTGLOB if File.const_defined?(:FNM_EXTGLOB)
190
+ File.fnmatch(name_pattern, name, flags)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
@@ -18,6 +18,6 @@
18
18
 
19
19
  module Groonga
20
20
  module QueryLog
21
- VERSION = "1.0.1"
21
+ VERSION = "1.0.2"
22
22
  end
23
23
  end
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
- # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
@@ -18,20 +18,35 @@
18
18
 
19
19
  $VERBOSE = true
20
20
 
21
- $KCODE = "u" if RUBY_VERSION < "1.9"
21
+ require "pathname"
22
22
 
23
- base_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
24
- lib_dir = File.join(base_dir, "lib")
25
- test_dir = File.join(base_dir, "test")
23
+ base_dir = Pathname.new(__FILE__).dirname.parent.expand_path
24
+ top_dir = base_dir.parent
25
+
26
+ gqtp_base_dir = top_dir + "gqtp"
27
+ gqtp_lib_dir = gqtp_base_dir + "lib"
28
+ $LOAD_PATH.unshift(gqtp_lib_dir.to_s)
29
+
30
+ groonga_client_base_dir = top_dir + "groonga-client"
31
+ groonga_client_lib_dir = groonga_client_base_dir + "lib"
32
+ $LOAD_PATH.unshift(groonga_client_lib_dir.to_s)
33
+
34
+ groonga_command_base_dir = top_dir + "groonga-command"
35
+ groonga_command_lib_dir = groonga_command_base_dir + "lib"
36
+ $LOAD_PATH.unshift(groonga_command_lib_dir.to_s)
37
+
38
+ lib_dir = base_dir + "lib"
39
+ test_dir = base_dir + "test"
26
40
 
27
41
  require "test-unit"
28
42
  require "test/unit/notify"
43
+ require "test/unit/rr"
29
44
 
30
45
  Test::Unit::Priority.enable
31
46
 
32
- $LOAD_PATH.unshift(lib_dir)
47
+ $LOAD_PATH.unshift(lib_dir.to_s)
33
48
 
34
- $LOAD_PATH.unshift(test_dir)
49
+ $LOAD_PATH.unshift(test_dir.to_s)
35
50
  require "groonga-query-log-test-utils"
36
51
 
37
52
  Dir.glob("#{base_dir}/test/**/test{_,-}*.rb") do |file|
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2011-2012 Kouhei Sutou <kou@clear-code.com>
3
+ # Copyright (C) 2011-2013 Kouhei Sutou <kou@clear-code.com>
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
@@ -18,16 +18,27 @@
18
18
 
19
19
  class ParserTest < Test::Unit::TestCase
20
20
  def test_load
21
- @log = <<-EOL
21
+ statistics = parse(<<-LOG)
22
22
  2012-12-13 11:15:21.628105|0x7fff148c8a50|>load --table Video
23
23
  2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=0
24
- EOL
24
+ LOG
25
+ parsed_command = statistics.first.command
26
+ assert_instance_of(Groonga::Command::Load, parsed_command)
27
+ end
28
+
29
+ def test_ignore_invalid_line
30
+ garbage = "\x80"
31
+ statistics = parse(<<-LOG)
32
+ 2012-12-13 11:15:20.628105|0x7fff148c8a50|>#{garbage}
33
+ 2012-12-13 11:15:21.628105|0x7fff148c8a50|>load --table Video
34
+ 2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=0
35
+ LOG
25
36
  parsed_command = statistics.first.command
26
37
  assert_instance_of(Groonga::Command::Load, parsed_command)
27
38
  end
28
39
 
29
40
  private
30
- def statistics
41
+ def parse(log)
31
42
  statistics = []
32
43
  parser = Groonga::QueryLog::Parser.new
33
44
  parser.parse(StringIO.new(log)) do |statistic|
@@ -36,13 +47,9 @@ EOL
36
47
  statistics
37
48
  end
38
49
 
39
- def log
40
- @log
41
- end
42
-
43
50
  class StatisticOperationTest < self
44
51
  def setup
45
- @log = <<-EOL
52
+ @statistics = parse(<<-LOG)
46
53
  2011-06-02 16:27:04.731685|5091e5c0|>/d/select.join?table=Entries&filter=local_name+%40+%22gsub%22+%26%26+description+%40+%22string%22&sortby=_score&output_columns=_key&drilldown=name,class
47
54
  2011-06-02 16:27:04.733539|5091e5c0|:000000001849451 filter(15)
48
55
  2011-06-02 16:27:04.734978|5091e5c0|:000000003293459 filter(13)
@@ -52,11 +59,12 @@ EOL
52
59
  2011-06-02 16:27:04.735606|5091e5c0|:000000003921419 drilldown(3)
53
60
  2011-06-02 16:27:04.735762|5091e5c0|:000000004077552 drilldown(2)
54
61
  2011-06-02 16:27:04.735808|5091e5c0|<000000004123726 rc=0
55
- EOL
62
+ LOG
63
+ @statistic = @statistics.first
56
64
  end
57
65
 
58
66
  def test_context
59
- operations = statistics.first.operations.collect do |operation|
67
+ operations = @statistic.operations.collect do |operation|
60
68
  [operation[:name], operation[:context]]
61
69
  end
62
70
  expected = [
@@ -72,7 +80,7 @@ EOL
72
80
  end
73
81
 
74
82
  def test_n_records
75
- operations = statistics.first.operations.collect do |operation|
83
+ operations = @statistic.operations.collect do |operation|
76
84
  [operation[:name], operation[:n_records]]
77
85
  end
78
86
  expected = [
@@ -87,4 +95,22 @@ EOL
87
95
  assert_equal(expected, operations)
88
96
  end
89
97
  end
98
+
99
+ class TestRC < self
100
+ def test_success
101
+ statistics = parse(<<-LOG)
102
+ 2012-12-13 11:15:21.628105|0x7fff148c8a50|>table_create --name Videos
103
+ 2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=0
104
+ LOG
105
+ assert_equal([0], statistics.collect(&:return_code))
106
+ end
107
+
108
+ def test_fialure
109
+ statistics = parse(<<-LOG)
110
+ 2012-12-13 11:15:21.628105|0x7fff148c8a50|>table_create --name Videos
111
+ 2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=-22
112
+ LOG
113
+ assert_equal([-22], statistics.collect(&:return_code))
114
+ end
115
+ end
90
116
  end
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
9
+ #
10
+ # This library is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public
16
+ # License along with this library; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
+
19
+ class ReplayerTest < Test::Unit::TestCase
20
+ class OptionTest < self
21
+ class ClientTest < self
22
+ def setup
23
+ @options = Groonga::QueryLog::Replayer::Options.new
24
+ @options.n_clients = 1
25
+ end
26
+
27
+ def test_host
28
+ host = "example.com"
29
+ @options.host = host
30
+ mock_client_open(:host => host)
31
+ replay
32
+ end
33
+
34
+ def test_port
35
+ port = 2929
36
+ @options.port = 2929
37
+ mock_client_open(:port => 2929)
38
+ replay
39
+ end
40
+
41
+ private
42
+ def replay
43
+ replayer = Groonga::QueryLog::Replayer.new(@options)
44
+ replayer.replay(StringIO.new(""))
45
+ end
46
+
47
+ def mock_client_open(expected_options)
48
+ client = Object.new
49
+ default_options = {
50
+ :host => "127.0.0.1",
51
+ :port => 10041,
52
+ :protocol => :gqtp,
53
+ }
54
+ expected_open_options = default_options.merge(expected_options)
55
+ mock(Groonga::Client).open(expected_open_options).yields(client) do
56
+ client
57
+ end
58
+ end
59
+ end
60
+
61
+ class TargetCommandNameTest < self
62
+ def setup
63
+ @options = Groonga::QueryLog::Replayer::Options.new
64
+ end
65
+
66
+ def test_default
67
+ assert_true(@options.target_command_name?("shutdown"))
68
+ end
69
+
70
+ class GlobTest < self
71
+ def setup
72
+ super
73
+ @options.target_command_names = ["se*"]
74
+ end
75
+
76
+ def test_match
77
+ assert_true(@options.target_command_name?("select"))
78
+ end
79
+
80
+ def test_not_match
81
+ assert_false(@options.target_command_name?("status"))
82
+ end
83
+ end
84
+
85
+ class ExtGlobTest < self
86
+ def setup
87
+ super
88
+ @options.target_command_names = ["s{elect,tatus}"]
89
+ unless File.const_defined?(:FNM_EXTGLOB)
90
+ omit("File:::FNM_EXTGLOB (Ruby 2.0.0 or later) is required.")
91
+ end
92
+ end
93
+
94
+ def test_match
95
+ assert_true(@options.target_command_name?("select"))
96
+ end
97
+
98
+ def test_not_match
99
+ assert_false(@options.target_command_name?("selectX"))
100
+ end
101
+ end
102
+
103
+ class ExactMatchTest < self
104
+ def setup
105
+ super
106
+ @options.target_command_names = ["select"]
107
+ end
108
+
109
+ def test_match
110
+ assert_true(@options.target_command_name?("select"))
111
+ end
112
+
113
+ def test_not_match
114
+ assert_false(@options.target_command_name?("selectX"))
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end