spider-gazelle 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ca80be46b162d37bbcb510ba2522bbfd0ea6460d
4
- data.tar.gz: fcec86de21cd802686919983912c130a34762ad4
3
+ metadata.gz: 883af96ffb1054dc17b7f623ca7a6a1b6afe48cf
4
+ data.tar.gz: 9dda42f38e4cb802d4c6a11632438c16c2868cde
5
5
  SHA512:
6
- metadata.gz: a3be697d78d1e77e23017ba627b4fd33cc29ecb3bf45d51081daf1321c7956e5e95e521be8253ee2c0572b62af922c9c84a948f6746bc4e8c0bd3c755c68f164
7
- data.tar.gz: 84045ad878fdced61b7c8e4739e099291c52cdcd53d1fc0916cbe802b5a7e76f1a5fc33b0db52ede7f4194864e1dca319887afe38c65368185cb6d0ae81d8edc
6
+ metadata.gz: 848ba774b73a281f074505d8a0c92e8742e78cc6608ed441e283497f26fdfda6b3f992547b2df4eae4960cb3649e93ca135ffdf0173e7b97e22c67f193e3f8ab
7
+ data.tar.gz: 170c737ed7502cbff903f1ef8d04f8050d0632471d50077384125f284608b5362fe3585e8c823faa30973d21f0fb55901154c579c3c861ebd724d32190f0b2d6
data/bin/sg CHANGED
@@ -1,67 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
-
4
3
  require 'spider-gazelle'
5
- require 'optparse'
6
-
7
-
8
- options = {
9
- Host: "0.0.0.0",
10
- Port: 3000,
11
- environment: ENV['RACK_ENV'] || 'development',
12
- rackup: "#{Dir.pwd}/config.ru",
13
- Verbose: false
14
- }
15
-
16
- parser = OptionParser.new do |opts|
17
- opts.on "-p", "--port PORT", Integer, "Define what port TCP port to bind to (default: 3000)" do |arg|
18
- options[:Port] = arg
19
- end
20
-
21
- opts.on "-a", "--address HOST", "bind to HOST address (default: 0.0.0.0)" do |arg|
22
- options[:Host] = arg
23
- end
24
-
25
- opts.on "-v", "--verbose", "loud output" do
26
- options[:Verbose] = true
27
- end
28
-
29
- opts.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default: development)" do |arg|
30
- options[:environment] = arg
31
- end
32
-
33
- opts.on "-r", "--rackup FILE", "Load Rack config from this file (default: config.ru)" do |arg|
34
- options[:rackup] = arg
35
- end
36
-
37
- opts.on "-l", "--log FILE", "Location of the servers log file (default: logs/server.log)" do |arg|
38
- ENV['SG_LOG'] = arg
39
- end
40
-
41
- opts.on "-m", "--mode MODE", "Either thread, process or no_ipc (default: thread)" do |arg|
42
- ENV['SG_MODE'] = arg
43
- end
44
- end
45
-
46
- parser.banner = "sg <options> <rackup file>"
47
- parser.on_tail "-h", "--help", "Show help" do
48
- puts parser
49
- exit 1
50
- end
51
-
52
- parser.parse!(ARGV)
53
-
54
- if ARGV.last =~ /\.ru$/
55
- options[:rackup] = ARGV.shift
56
- end
57
-
58
- unless File.exists?(options[:rackup])
59
- abort "No rackup found at #{options[:rackup]}"
60
- end
61
-
62
- # Force process mode on Windows (sockets over pipes not working at the moment)
63
- ENV['SG_MODE'] = 'no_ipc' if ::FFI::Platform.windows?
64
-
65
- ::SpiderGazelle::Spider.run options[:rackup], options
66
-
67
- puts "\nSpider-Gazelle leaps through the veldt"
4
+ SpiderGazelle::LaunchControl.instance.exec(ARGV)
@@ -1,37 +1,28 @@
1
1
  require "rack/handler"
2
2
  require "spider-gazelle"
3
- require "spider-gazelle/const"
4
3
 
5
4
  module Rack
6
- module Handler
7
- module SpiderGazelle
8
- DEFAULT_OPTIONS = {
9
- :Host => "0.0.0.0",
10
- :Port => 8080,
11
- :Verbose => false
12
- }
5
+ module Handler
6
+ module SpiderGazelle
7
+ def self.run(app, options = {})
8
+
9
+ # Replace the rackup with app
10
+ options = ::SpiderGazelle::Options::DEFAULTS.merge(options)
11
+ options.delete(:rackup)
12
+ options[:app] = app
13
13
 
14
- def self.run(app, options = {})
15
- options = DEFAULT_OPTIONS.merge(options)
14
+ # Can't pass an object over a pipe
15
+ options[:isolate] = true
16
+ options[:mode] = :thread if options[:mode] == :process
16
17
 
17
- if options[:Verbose]
18
- app = Rack::CommonLogger.new(app, STDOUT)
19
- end
18
+ # Ensure the environment is set
19
+ options[:environment] ||= ENV['RACK_ENV'] || 'development'
20
+ ENV['RACK_ENV'] = options[:environment]
20
21
 
21
- if options[:environment]
22
- ENV["RACK_ENV"] = options[:environment].to_s
22
+ ::SpiderGazelle::LaunchControl.instance.launch([options])
23
+ end
23
24
  end
24
25
 
25
- ::SpiderGazelle::Spider.run app, options
26
- end
27
-
28
- def self.valid_options
29
- { "Host=HOST" => "Hostname to listen on (default: 0.0.0.0)",
30
- "Port=PORT" => "Port to listen on (default: 8080)",
31
- "Quiet" => "Don't report each request" }
32
- end
26
+ register :"spider-gazelle", SpiderGazelle
33
27
  end
34
-
35
- register :"spider-gazelle", SpiderGazelle
36
- end
37
28
  end
@@ -4,36 +4,36 @@ require 'rack/body_proxy'
4
4
  require 'rack/lock' # ensure this loads first
5
5
 
6
6
  module Rack
7
- # Rack::Lock locks every request inside a mutex, so that every request
8
- # will effectively be executed synchronously.
9
- class Lock
10
- # FLAG = 'rack.multithread'.freeze # defined in rack/lock
11
- RACK_MULTITHREAD ||= FLAG
7
+ # Rack::Lock locks every request inside a mutex, so that every request
8
+ # will effectively be executed synchronously.
9
+ class Lock
10
+ # FLAG = 'rack.multithread'.freeze # defined in rack/lock
11
+ RACK_MULTITHREAD ||= FLAG
12
12
 
13
- def initialize(app, mutex = Mutex.new)
14
- @app, @mutex = app, mutex
15
- @sig = ConditionVariable.new
16
- @count = 0
17
- end
13
+ def initialize(app, mutex = Mutex.new)
14
+ @app, @mutex = app, mutex
15
+ @sig = ConditionVariable.new
16
+ @count = 0
17
+ end
18
18
 
19
- def call(env)
20
- @mutex.lock
21
- @count += 1
22
- @sig.wait(@mutex) if @count > 1
23
- response = @app.call(env.merge(RACK_MULTITHREAD => false))
24
- returned = response << BodyProxy.new(response.pop) {
25
- @mutex.synchronize { unlock }
26
- }
27
- ensure
28
- unlock unless returned
29
- @mutex.unlock
30
- end
19
+ def call(env)
20
+ @mutex.lock
21
+ @count += 1
22
+ @sig.wait(@mutex) if @count > 1
23
+ response = @app.call(env.merge(RACK_MULTITHREAD => false))
24
+ returned = response << BodyProxy.new(response.pop) {
25
+ @mutex.synchronize { unlock }
26
+ }
27
+ ensure
28
+ unlock unless returned
29
+ @mutex.unlock
30
+ end
31
31
 
32
- private
32
+ private
33
33
 
34
- def unlock
35
- @count -= 1
36
- @sig.signal
34
+ def unlock
35
+ @count -= 1
36
+ @sig.signal
37
+ end
37
38
  end
38
- end
39
39
  end
@@ -1,22 +1,171 @@
1
- require "http-parser" # C based, fast, http parser
2
- require "libuv" # Ruby Libuv FFI wrapper
3
- require "rack" # Ruby webserver abstraction
4
- require "rack/lock_patch" # Serialize execution in development mode
1
+ require 'thread'
2
+ require 'singleton'
5
3
 
6
- require "spider-gazelle/request" # Holds request information and handles request processing
7
- require "spider-gazelle/connection" # Holds connection information and handles request pipelining
8
- require "spider-gazelle/gazelle" # Processes data received from connections
9
4
 
10
- require "spider-gazelle/app_store" # Holds references to the loaded rack applications
11
- require "spider-gazelle/binding" # Holds a reference to a bound port and associated rack application
12
- require "spider-gazelle/spider" # Accepts connections and offloads them to gazelles
5
+ require 'spider-gazelle/options'
6
+ require 'spider-gazelle/logger'
7
+ require 'spider-gazelle/reactor'
8
+ require 'spider-gazelle/signaller'
13
9
 
14
- # Reactor aware websocket implementation
15
- require "spider-gazelle/upgrades/websocket"
16
10
 
17
11
  module SpiderGazelle
18
- # Delegate pipe used for passing sockets to the gazelles
19
- DELEGATE_PIPE = ENV['SG_DELEGATE_PIPE'] || "/tmp/spider-gazelle.delegate"
20
- # Signal pipe used to pass control signals
21
- SIGNAL_PIPE = ENV['SG_SIGNAL_PIPE'] || "/tmp/spider-gazelle.signal"
12
+ VERSION = '2.0.0'.freeze
13
+ EXEC_NAME = 'sg'.freeze
14
+ INTERNAL_PIPE_BACKLOG = 128
15
+
16
+ # Signaller is used to communicate:
17
+ # * command line requests
18
+ # * Startup and shutdown requests
19
+ # * Live updates (bindings passed by this pipe)
20
+ SIGNAL_SERVER = '/tmp/sg-signaller.pipe'.freeze
21
+
22
+ # Spider server is used to
23
+ # * Track gazelles
24
+ # * Signal shutdown as required
25
+ # * Pass sockets
26
+ SPIDER_SERVER = '/tmp/sg-spider.pipe.'.freeze
27
+
28
+ MODES = [:process, :thread, :no_ipc].freeze
29
+ APP_MODE = [:thread_pool, :fiber_pool, :libuv, :eventmachine, :celluloid]
30
+
31
+
32
+ class LaunchControl
33
+ include Singleton
34
+
35
+
36
+ attr_reader :password, :args
37
+
38
+
39
+ def exec(args)
40
+ options = SpiderGazelle::Options.sanitize(args)
41
+ @args = args
42
+ launch(options)
43
+ end
44
+
45
+ def launch(options)
46
+ # Enable verbose messages if requested
47
+ Logger.instance.verbose! if options[0][:verbose]
48
+
49
+ # Start the Libuv Event Loop
50
+ reactor = ::SpiderGazelle::Reactor.instance
51
+ reactor.run do
52
+
53
+ # Check if SG is already running
54
+ signaller = ::SpiderGazelle::Signaller.instance
55
+
56
+ if options[0][:isolate]
57
+ # This ensures this process will load the spider code
58
+ options[0][:spider] = true
59
+ boot(true, signaller, options)
60
+ else
61
+ signaller.check.then do |running|
62
+ boot(running, signaller, options)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def shutdown
69
+ reactor = Reactor.instance
70
+ reactor.thread.schedule do
71
+ reactor.shutdown
72
+ end
73
+ end
74
+
75
+
76
+ # ---------------------------------------
77
+ # SPIDER LAUNCH CONTROL
78
+ # ---------------------------------------
79
+ def launch_spider(args)
80
+ require 'securerandom'
81
+ require 'thread'
82
+
83
+ @password ||= SecureRandom.hex
84
+ #cmd = "#{EXEC_NAME} -s #{@password} #{Shellwords.join(args)}"
85
+ cmd = [EXEC_NAME, '-s', @password] + args
86
+
87
+ Thread.new do
88
+ result = system(*cmd)
89
+
90
+ # TODO:: We need to detect a failed load
91
+ # This is a little more tricky as spiders
92
+ # may come and go without this process exiting
93
+ end
94
+ end
95
+
96
+ # This is called when a spider process starts
97
+ def start_spider(signaller, logger, options)
98
+ logger.set_client signaller.pipe unless options[0][:isolate]
99
+
100
+ require 'spider-gazelle/spider'
101
+ Spider.instance.run!(options)
102
+ end
103
+
104
+
105
+
106
+ # ---------------------------------------
107
+ # GAZELLE LAUNCH CONTROL
108
+ # ---------------------------------------
109
+ def start_gazelle(signaller, logger, options)
110
+ logger.set_client signaller.pipe
111
+
112
+ require 'spider-gazelle/gazelle'
113
+ ::SpiderGazelle::Gazelle.new(logger.thread, :process).run!(options)
114
+ end
115
+
116
+
117
+ # ---------------------------------------
118
+ # TTY SIGNALLING CONTROL
119
+ # ---------------------------------------
120
+ def signal_master(reactor, signaller, logger, options)
121
+ # This is a signal request
122
+ promise = signaller.request(options)
123
+
124
+ promise.then do |result|
125
+ logger.info "signal recieved #{result}"
126
+ end
127
+ promise.catch do |error|
128
+ logger.info "there was an error #{error}"
129
+ end
130
+ promise.finally do
131
+ reactor.shutdown
132
+ end
133
+ end
134
+
135
+
136
+ protected
137
+
138
+
139
+ def boot(running, signaller, options)
140
+ logger = ::SpiderGazelle::Logger.instance
141
+
142
+ begin
143
+ # What do we want to do?
144
+ master = options[0]
145
+
146
+ if running
147
+ if master[:spider]
148
+ logger.verbose "Starting Spider".freeze
149
+ start_spider(signaller, logger, options)
150
+ elsif master[:gazelle]
151
+ logger.verbose "Starting Gazelle".freeze
152
+ start_gazelle(signaller, logger, options)
153
+ else
154
+ logger.verbose "Sending signal to SG Master".freeze
155
+ signal_master(reactor, signaller, logger, options)
156
+ end
157
+
158
+ elsif master[:debug]
159
+ logger.verbose "SG is now running in debug mode".freeze
160
+ else
161
+ logger.verbose "SG was not running, launching Spider".freeze
162
+ launch_spider(@args)
163
+ end
164
+ rescue => e
165
+ logger.verbose "Error performing requested operation".freeze
166
+ logger.print_error(e)
167
+ shutdown
168
+ end
169
+ end
170
+ end
22
171
  end
@@ -1,162 +1,179 @@
1
- require 'spider-gazelle/const'
2
- require 'set'
1
+ require "rack" # Ruby webserver abstraction
2
+ require "rack/lock_patch" # Serialize execution in development mode
3
+ require 'spider-gazelle/gazelle/app_store'
4
+ require 'spider-gazelle/gazelle/http1'
3
5
 
4
- module SpiderGazelle
5
- class Gazelle
6
- include Const
7
6
 
8
- attr_reader :parser_cache, :connections, :logger
7
+ # Reactor aware websocket implementation
8
+ require "spider-gazelle/upgrades/websocket"
9
9
 
10
- def set_instance_type(inst)
11
- inst.type = :request
12
- end
13
10
 
14
- def initialize(loop, logger, mode)
15
- @gazelle = loop
16
- # Set of active connections on this thread
17
- @connections = Set.new
18
- # Stale parser objects cached for reuse
19
- @parser_cache = []
11
+ module SpiderGazelle
12
+ class Gazelle
13
+ SPACE = ' '.freeze
14
+
15
+ def initialize(thread, type)
16
+ raise ArgumentError, "type must be one of #{MODES}" unless MODES.include?(type)
17
+
18
+ @type = type
19
+ @logger = Logger.instance
20
+ @thread = thread
21
+
22
+ @http1_cache = []
23
+ @http2_cache = []
24
+ @return_http1 = method(:return_http1)
25
+ @return_http2 = method(:return_http2)
26
+ @parser_count = 0
27
+
28
+ @on_progress = method(:on_progress)
29
+ @set_protocol = method(:set_protocol)
30
+
31
+ # Register the gazelle with the signaller so we can shutdown elegantly
32
+ if @type == :process
33
+ Signaller.instance.gazelle = self
34
+ end
35
+ end
20
36
 
21
- @mode = mode
22
- @logger = logger
23
- @app_cache = {}
24
- @connection_queue = ::Libuv::Q::ResolvedPromise.new @gazelle, true
37
+ def run!(options)
38
+ @options = options
39
+ @logger.verbose { "Gazelle: #{@type} Pid: #{Process.pid} started" }
25
40
 
26
- # A single parser instance for processing requests for each gazelle
27
- @parser = ::HttpParser::Parser.new self
28
- @set_instance_type = method :set_instance_type
41
+ connect_to_spider unless @type == :no_ipc
29
42
 
30
- # Single progress callback for each gazelle
31
- @on_progress = method :on_progress
32
- end
43
+ load_required_applications
44
+ self
45
+ end
33
46
 
34
- def run
35
- @gazelle.run do |logger|
36
- logger.progress do |level, errorid, error|
37
- begin
38
- msg = "Gazelle log: #{level}: #{errorid}\n#{error.message}\n#{error.backtrace.join("\n") if error.backtrace}\n"
39
- @logger.error msg
40
- rescue Exception
41
- puts 'error in gazelle logger'
42
- end
47
+ def new_app(options)
48
+ # TODO:: load this app into all of the gazelles dynamically
43
49
  end
44
50
 
45
- unless @mode == :no_ipc
46
- # A pipe used to forward connections to different threads
47
- @socket_server = @gazelle.pipe true
48
- @socket_server.connect(DELEGATE_PIPE) do
49
- @socket_server.progress &method(:new_connection)
50
- @socket_server.start_read
51
- end
52
-
53
- # A pipe used to signal various control commands (shutdown, etc)
54
- @signal_server = @gazelle.pipe
55
- @signal_server.connect(SIGNAL_PIPE) do
56
- @signal_server.progress &method(:process_signal)
57
- @signal_server.start_read
58
- end
51
+ def new_connection(data, binding)
52
+ socket = @pipe.check_pending
53
+ return if socket.nil?
54
+ process_connection(socket, data.to_i)
59
55
  end
60
- end
61
- end
62
56
 
63
- # HTTP Parser callbacks:
64
- def on_message_begin(parser)
65
- @connection.start_parsing
66
- end
57
+ def shutdown(finished = nil)
58
+ # Wait for the requests to finish (give them 15 seconds)
59
+ # TODO::
60
+
61
+ @logger.verbose { "Gazelle: #{@type} Pid: #{Process.pid} shutting down" }
62
+
63
+ # Then stop the current thread if we are in threaded mode
64
+ if @type == :thread
65
+ # In threaded mode the gazelle has the power
66
+ @thread.stop
67
+ else
68
+ # Both no_ipc and process need to know when the requests
69
+ # have completed to shutdown
70
+ finished.resolve(true)
71
+ end
72
+ end
67
73
 
68
- def on_url(parser, url)
69
- @connection.parsing.url << url
70
- end
71
74
 
72
- def on_header_field(parser, header)
73
- req = @connection.parsing
74
- req.header.frozen? ? req.header = header : req.header << header
75
- end
75
+ protected
76
+
77
+
78
+ def connect_to_spider
79
+ @pipe = @thread.pipe :ipc
80
+ @pipe.connect(@options[0][:gazelle_ipc]) do |client|
81
+ client.progress method(:new_connection)
82
+ client.start_read
83
+
84
+ authenticate
85
+ end
76
86
 
77
- def on_header_value(parser, value)
78
- req = @connection.parsing
79
- if req.header.frozen?
80
- req.env[req.header] << value
81
- else
82
- header = req.header
83
- header.upcase!
84
- header.gsub!(DASH, UNDERSCORE)
85
- header.prepend(HTTP_META)
86
- header.freeze
87
- if req.env[header]
88
- req.env[header] << COMMA
89
- req.env[header] << value
90
- else
91
- req.env[header] = value
87
+ @pipe.catch do |error|
88
+ @logger.print_error(error)
89
+ end
90
+
91
+ @pipe.finally do
92
+ if @type == :process
93
+ Reactor.instance.shutdown
94
+ else
95
+ # Threaded mode
96
+ shutdown
97
+ end
98
+ end
92
99
  end
93
- end
94
- end
95
100
 
96
- def on_headers_complete(parser)
97
- @connection.parsing.env[REQUEST_METHOD] = @connection.state.http_method.to_s
98
- end
101
+ def authenticate
102
+ @pipe.write "#{@options[0][:gazelle]} #{@type}"
103
+ end
99
104
 
100
- def on_body(parser, data)
101
- @connection.parsing.body << data
102
- end
105
+ def load_required_applications
106
+ @options.each do |app|
107
+ if app[:rackup]
108
+ AppStore.load(app[:rackup], app)
109
+ elsif app[:app]
110
+ AppStore.add(app[:app], app)
111
+ end
112
+ end
113
+ end
103
114
 
104
- def on_message_complete(parser)
105
- @connection.finished_parsing
106
- end
107
115
 
108
- def discard(connection)
109
- @connections.delete(connection)
110
- state = connection.state
111
- state.reset!
112
- @parser_cache << state
113
- end
116
+ # ---------------------
117
+ # Connection Management
118
+ # ---------------------
119
+ def process_connection(socket, app_id)
120
+ # Put application details in the socket storage as we negotiate protocols
121
+ details = AppStore.get(app_id)
122
+ socket.storage = details
123
+ tls = details[-1]
124
+
125
+ # Hook up the socket and kick off TLS if required
126
+ if tls
127
+ socket.on_handshake @set_protocol
128
+ socket.start_tls(tls)
129
+ else
130
+ set_protocol(socket, :http1)
131
+ end
132
+
133
+ socket.start_read
134
+ socket.enable_nodelay
135
+ end
114
136
 
115
- protected
137
+ def on_progress(data, socket)
138
+ # Storage contains the parser for this connection
139
+ parser = socket.storage
140
+ parser.parse(data)
141
+ end
116
142
 
117
- def on_progress(data, socket)
118
- # Keep track of which connection we are processing for the callbacks
119
- @connection = socket.storage
143
+ def set_protocol(socket, version)
144
+ app, app_mode, port, tls = socket.storage
120
145
 
121
- # Check for errors during the parsing of the request
122
- @connection.parsing_error if @parser.parse(@connection.state, data)
123
- end
146
+ parser = if version == :h2
147
+ @http2_cache.pop || new_http2_parser
148
+ else
149
+ @http1_cache.pop || new_http1_parser
150
+ end
124
151
 
125
- def new_connection(data, socket)
126
- # When in no IPC mode @socket_server is nil and the socket parameter is the socket
127
- # If socket server exists then we are expecting sockets over the pipe
128
- if @socket_server
129
- socket = @socket_server.check_pending
130
- return if socket.nil?
131
- end
132
-
133
- # Data == "TLS_indicator Port APP_ID"
134
- tls, port, app_id = data.split(SPACE, 3)
135
- app = @app_cache[app_id.to_sym] ||= AppStore.get(app_id)
136
- inst = @parser_cache.pop || ::HttpParser::Parser.new_instance(&@set_instance_type)
137
-
138
- # process any data coming from the socket
139
- socket.progress @on_progress
140
- # TODO:: Allow some globals for supplying the certs
141
- # --> We could store these in the AppStore
142
- socket.start_tls(:server => true) if tls == USE_TLS
143
-
144
- # Keep track of the connection
145
- connection = Connection.new self, @gazelle, socket, port, inst, app, @connection_queue
146
- @connections.add connection
147
- # This allows us to re-use the one proc for parsing
148
- socket.storage = connection
149
-
150
- socket.start_read
151
- end
152
+ parser.load(socket, port, app, app_mode, tls)
153
+ socket.progress @on_progress
154
+ socket.storage = parser
155
+ end
152
156
 
153
- def process_signal(data, pipe)
154
- shutdown if data == KILL_GAZELLE
155
- end
156
157
 
157
- def shutdown
158
- # TODO:: do this nicely. Need to signal the connections to close
159
- @gazelle.stop
158
+ def new_http1_parser
159
+ @h1_parser_obj ||= Http1::Callbacks.new
160
+
161
+ @parser_count += 1
162
+ Http1.new(@return_http1, @h1_parser_obj, @thread, @logger)
163
+ end
164
+
165
+ def return_http1(parser)
166
+ @http1_cache.push parser
167
+ end
168
+
169
+ def new_http2_parser
170
+ raise NotImplementedError.new 'TODO:: Create HTTP2 parser class'
171
+ @parser_count += 1
172
+ Http2.new(@return_http2)
173
+ end
174
+
175
+ def return_http2(parser)
176
+ @http2_cache << parser
177
+ end
160
178
  end
161
- end
162
179
  end