rainbows 2.1.0 → 3.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.
Files changed (71) hide show
  1. data/GIT-VERSION-GEN +1 -1
  2. data/GNUmakefile +2 -3
  3. data/Rakefile +5 -2
  4. data/lib/rainbows.rb +72 -54
  5. data/lib/rainbows/base.rb +7 -9
  6. data/lib/rainbows/client.rb +25 -4
  7. data/lib/rainbows/const.rb +1 -1
  8. data/lib/rainbows/coolio.rb +6 -3
  9. data/lib/rainbows/coolio/client.rb +78 -57
  10. data/lib/rainbows/coolio/core.rb +1 -4
  11. data/lib/rainbows/coolio/heartbeat.rb +2 -3
  12. data/lib/rainbows/coolio/master.rb +3 -2
  13. data/lib/rainbows/coolio/{deferred_chunk_response.rb → response_chunk_pipe.rb} +1 -2
  14. data/lib/rainbows/coolio/{deferred_response.rb → response_pipe.rb} +1 -1
  15. data/lib/rainbows/coolio/thread_client.rb +4 -6
  16. data/lib/rainbows/coolio_fiber_spawn.rb +1 -1
  17. data/lib/rainbows/coolio_thread_pool.rb +1 -1
  18. data/lib/rainbows/coolio_thread_pool/watcher.rb +2 -4
  19. data/lib/rainbows/coolio_thread_spawn.rb +1 -1
  20. data/lib/rainbows/dev_fd_response.rb +2 -2
  21. data/lib/rainbows/error.rb +5 -7
  22. data/lib/rainbows/ev_core.rb +20 -7
  23. data/lib/rainbows/event_machine.rb +6 -5
  24. data/lib/rainbows/event_machine/client.rb +46 -53
  25. data/lib/rainbows/event_machine/response_pipe.rb +2 -3
  26. data/lib/rainbows/fiber/base.rb +5 -5
  27. data/lib/rainbows/fiber/body.rb +4 -13
  28. data/lib/rainbows/fiber/coolio/heartbeat.rb +1 -3
  29. data/lib/rainbows/fiber/coolio/server.rb +4 -7
  30. data/lib/rainbows/fiber_pool.rb +1 -1
  31. data/lib/rainbows/fiber_spawn.rb +2 -2
  32. data/lib/rainbows/http_parser.rb +12 -0
  33. data/lib/rainbows/http_server.rb +5 -7
  34. data/lib/rainbows/max_body.rb +2 -2
  35. data/lib/rainbows/never_block/core.rb +1 -1
  36. data/lib/rainbows/process_client.rb +15 -29
  37. data/lib/rainbows/queue_pool.rb +1 -3
  38. data/lib/rainbows/rack_input.rb +3 -3
  39. data/lib/rainbows/response.rb +164 -38
  40. data/lib/rainbows/revactor.rb +5 -65
  41. data/lib/rainbows/revactor/client.rb +60 -0
  42. data/lib/rainbows/revactor/{body.rb → client/methods.rb} +14 -14
  43. data/lib/rainbows/revactor/{tee_socket.rb → client/tee_socket.rb} +1 -1
  44. data/lib/rainbows/sendfile.rb +1 -2
  45. data/lib/rainbows/server_token.rb +1 -1
  46. data/lib/rainbows/thread_pool.rb +9 -9
  47. data/lib/rainbows/thread_spawn.rb +7 -6
  48. data/lib/rainbows/thread_timeout.rb +1 -1
  49. data/lib/rainbows/writer_thread_pool.rb +9 -25
  50. data/lib/rainbows/writer_thread_pool/client.rb +44 -1
  51. data/lib/rainbows/writer_thread_spawn.rb +2 -11
  52. data/lib/rainbows/writer_thread_spawn/client.rb +53 -13
  53. data/rainbows.gemspec +3 -12
  54. data/t/async_chunk_app.ru +62 -0
  55. data/t/byte-range-common.sh +142 -0
  56. data/t/t0022-copy_stream-byte-range.sh +2 -111
  57. data/t/t0023-sendfile-byte-range.sh +2 -32
  58. data/t/t0025-write-on-close.sh +23 -0
  59. data/t/t0040-keepalive_requests-setting.sh +0 -5
  60. data/t/t0402-async-keepalive.sh +146 -0
  61. data/t/t0500-cramp-streaming.sh +2 -0
  62. data/t/t0501-cramp-rainsocket.sh +2 -0
  63. data/t/t9000-rack-app-pool.sh +1 -1
  64. data/t/test_isolate.rb +5 -10
  65. data/t/test_isolate_cramp.rb +26 -0
  66. data/t/write-on-close.ru +11 -0
  67. metadata +33 -30
  68. data/lib/rainbows/coolio/sendfile.rb +0 -17
  69. data/lib/rainbows/response/body.rb +0 -127
  70. data/lib/rainbows/response/range.rb +0 -34
  71. data/lib/rainbows/timed_read.rb +0 -28
@@ -8,15 +8,12 @@ module Rainbows::Coolio::Core
8
8
  # given a INT, QUIT, or TERM signal)
9
9
  def worker_loop(worker)
10
10
  Rainbows::Response.setup(Rainbows::Coolio::Client)
11
- require 'rainbows/coolio/sendfile'
12
- Rainbows::Coolio::Client.__send__(:include, Rainbows::Coolio::Sendfile)
13
11
  init_worker_process(worker)
14
12
  mod = Rainbows.const_get(@use)
15
13
  rloop = Rainbows::Coolio::Server.const_set(:LOOP, Coolio::Loop.default)
16
- Rainbows::Coolio::Client.const_set(:LOOP, rloop)
17
14
  Rainbows::Coolio::Server.const_set(:MAX, @worker_connections)
18
15
  Rainbows::Coolio::Server.const_set(:CL, mod.const_get(:Client))
19
- Rainbows::EvCore.const_set(:APP, G.server.app)
16
+ Rainbows::EvCore.const_set(:APP, Rainbows.server.app)
20
17
  Rainbows::EvCore.setup
21
18
  Rainbows::Coolio::Heartbeat.new(1, true).attach(rloop)
22
19
  LISTENERS.map! { |s| Rainbows::Coolio::Server.new(s).attach(rloop) }
@@ -8,13 +8,12 @@
8
8
  class Rainbows::Coolio::Heartbeat < Coolio::TimerWatcher
9
9
  KATO = Rainbows::Coolio::KATO
10
10
  CONN = Rainbows::Coolio::CONN
11
- G = Rainbows::G
12
11
 
13
12
  def on_timer
14
- if (ot = G.kato) >= 0
13
+ if (ot = Rainbows.keepalive_timeout) >= 0
15
14
  ot = Time.now - ot
16
15
  KATO.delete_if { |client, time| time < ot and client.timeout? }
17
16
  end
18
- exit if (! G.tick && CONN.size <= 0)
17
+ exit if (! Rainbows.tick && CONN.size <= 0)
19
18
  end
20
19
  end
@@ -7,15 +7,16 @@ class Rainbows::Coolio::Master < Coolio::IOWatcher
7
7
  @reader, @writer = Kgio::Pipe.new
8
8
  super(@reader)
9
9
  @queue = queue
10
+ @wbuf, @rbuf = "\0", "\0"
10
11
  end
11
12
 
12
13
  def <<(output)
13
14
  @queue << output
14
- @writer.kgio_trywrite("\0")
15
+ @writer.kgio_trywrite(@wbuf)
15
16
  end
16
17
 
17
18
  def on_readable
18
- if String === @reader.kgio_tryread(1)
19
+ if String === @reader.kgio_tryread(1, @rbuf)
19
20
  client, response = @queue.pop
20
21
  client.response_write(response)
21
22
  end
@@ -2,8 +2,7 @@
2
2
  # :enddoc:
3
3
  #
4
4
  # this is class is specific to Coolio for proxying IO-derived objects
5
- class Rainbows::Coolio::DeferredChunkResponse <
6
- Rainbows::Coolio::DeferredResponse
5
+ class Rainbows::Coolio::ResponseChunkPipe < Rainbows::Coolio::ResponsePipe
7
6
  def on_read(data)
8
7
  @client.write("#{data.size.to_s(16)}\r\n")
9
8
  @client.write(data)
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # this is class is specific to Coolio for writing large static files
5
5
  # or proxying IO-derived objects
6
- class Rainbows::Coolio::DeferredResponse < Coolio::IO
6
+ class Rainbows::Coolio::ResponsePipe < Coolio::IO
7
7
  def initialize(io, client, body)
8
8
  super(io)
9
9
  @client, @body = client, body
@@ -14,11 +14,9 @@ class Rainbows::Coolio::ThreadClient < Rainbows::Coolio::Client
14
14
 
15
15
  # this is only called in the master thread
16
16
  def response_write(response)
17
- alive = @hp.next? && G.alive
18
- coolio_write_response(response, alive)
19
- return quit unless alive && :close != @state
20
-
21
- @state = :headers
17
+ ev_write_response(*response, @hp.next?)
18
+ rescue => e
19
+ handle_error(e)
22
20
  end
23
21
 
24
22
  # fails-safe application dispatch, we absolutely cannot
@@ -27,7 +25,7 @@ class Rainbows::Coolio::ThreadClient < Rainbows::Coolio::Client
27
25
  def app_response
28
26
  begin
29
27
  @env[REMOTE_ADDR] = @_io.kgio_addr
30
- APP.call(@env.update(RACK_DEFAULTS))
28
+ APP.call(@env.merge!(RACK_DEFAULTS))
31
29
  rescue => e
32
30
  Rainbows::Error.app(e) # we guarantee this does not raise
33
31
  [ 500, {}, [] ]
@@ -18,7 +18,7 @@ module Rainbows::CoolioFiberSpawn
18
18
  init_worker_process(worker)
19
19
  Server.const_set(:MAX, @worker_connections)
20
20
  Rainbows::Fiber::Base.setup(Server, nil)
21
- Server.const_set(:APP, G.server.app)
21
+ Server.const_set(:APP, Rainbows.server.app)
22
22
  Heartbeat.new(1, true).attach(Coolio::Loop.default)
23
23
  LISTENERS.map! { |s| Server.new(s).attach(Coolio::Loop.default) }
24
24
  Coolio::Loop.default.run
@@ -16,6 +16,7 @@
16
16
  # users are NOT advised to use this due to high CPU usage.
17
17
  module Rainbows::CoolioThreadPool
18
18
  # :stopdoc:
19
+ autoload :Client, 'rainbows/coolio_thread_pool/client'
19
20
  DEFAULTS = {
20
21
  :pool_size => 20, # same default size as ThreadPool (w/o Coolio)
21
22
  }
@@ -53,5 +54,4 @@ module Rainbows::CoolioThreadPool
53
54
  end
54
55
  end
55
56
  # :enddoc:
56
- require 'rainbows/coolio_thread_pool/client'
57
57
  require 'rainbows/coolio_thread_pool/watcher'
@@ -1,14 +1,12 @@
1
1
  # -*- encoding: binary -*-
2
2
  # :enddoc:
3
3
  class Rainbows::CoolioThreadPool::Watcher < Coolio::TimerWatcher
4
- G = Rainbows::G
5
-
6
4
  def initialize(threads)
7
5
  @threads = threads
8
- super(G.server.timeout, true)
6
+ super(Rainbows.server.timeout, true)
9
7
  end
10
8
 
11
9
  def on_timer
12
- @threads.each { |t| t.join(0) and G.quit! }
10
+ @threads.each { |t| t.join(0) and Rainbows.quit! }
13
11
  end
14
12
  end
@@ -15,6 +15,7 @@
15
15
  # users are NOT advised to use this due to high CPU usage.
16
16
  module Rainbows::CoolioThreadSpawn
17
17
  include Rainbows::Coolio::Core
18
+ autoload :Client, 'rainbows/coolio_thread_spawn/client'
18
19
 
19
20
  def init_worker_process(worker) # :nodoc:
20
21
  super
@@ -24,4 +25,3 @@ module Rainbows::CoolioThreadSpawn
24
25
  end
25
26
  end
26
27
  # :enddoc:
27
- require 'rainbows/coolio_thread_spawn/client'
@@ -39,7 +39,7 @@ class Rainbows::DevFdResponse < Struct.new(:app)
39
39
  io ||= File.open(body.to_path) if body.respond_to?(:to_path)
40
40
  return response if io.nil?
41
41
 
42
- headers = HeaderHash.new(headers)
42
+ headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
43
43
  st = io.stat
44
44
  fileno = io.fileno
45
45
  FD_MAP[fileno] = io
@@ -47,7 +47,7 @@ class Rainbows::DevFdResponse < Struct.new(:app)
47
47
  headers['Content-Length'] ||= st.size.to_s
48
48
  headers.delete('Transfer-Encoding')
49
49
  elsif st.pipe? || st.socket? # epoll-able things
50
- unless headers['Content-Length']
50
+ unless headers.include?('Content-Length')
51
51
  if env['rainbows.autochunk']
52
52
  headers['Transfer-Encoding'] = 'chunked'
53
53
  else
@@ -2,8 +2,6 @@
2
2
  # :enddoc:
3
3
  module Rainbows::Error
4
4
 
5
- G = Rainbows::G
6
-
7
5
  # if we get any error, try to write something back to the client
8
6
  # assuming we haven't closed the socket, but don't get hung up
9
7
  # if the socket is already closed or broken. We'll always ensure
@@ -20,15 +18,15 @@ module Rainbows::Error
20
18
  end
21
19
 
22
20
  def self.app(e)
23
- G.server.logger.error "app error: #{e.inspect}"
24
- G.server.logger.error e.backtrace.join("\n")
21
+ Rainbows.server.logger.error "app error: #{e.inspect}"
22
+ Rainbows.server.logger.error e.backtrace.join("\n")
25
23
  rescue
26
24
  end
27
25
 
28
26
  def self.listen_loop(e)
29
- G.alive or return
30
- G.server.logger.error "listen loop error: #{e.inspect}."
31
- G.server.logger.error e.backtrace.join("\n")
27
+ Rainbows.alive or return
28
+ Rainbows.server.logger.error "listen loop error: #{e.inspect}."
29
+ Rainbows.server.logger.error e.backtrace.join("\n")
32
30
  rescue
33
31
  end
34
32
 
@@ -4,16 +4,28 @@
4
4
  module Rainbows::EvCore
5
5
  include Rainbows::Const
6
6
  include Rainbows::Response
7
- G = Rainbows::G
8
7
  NULL_IO = Unicorn::HttpRequest::NULL_IO
9
- HttpParser = Unicorn::HttpParser
8
+ HttpParser = Rainbows::HttpParser
10
9
  autoload :CapInput, 'rainbows/ev_core/cap_input'
11
10
 
12
11
  # Apps may return this Rack response: AsyncResponse = [ -1, {}, [] ]
13
12
  ASYNC_CALLBACK = "async.callback".freeze
14
-
15
13
  ASYNC_CLOSE = "async.close".freeze
16
14
 
15
+ def write_async_response(response)
16
+ status, headers, body = response
17
+ if alive = @hp.next?
18
+ # we can't do HTTP keepalive without Content-Length or
19
+ # "Transfer-Encoding: chunked", and the async.callback stuff
20
+ # isn't Rack::Lint-compatible, so we have to enforce it here.
21
+ headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
22
+ alive = headers.include?(Content_Length) ||
23
+ !!(%r{\Achunked\z}i =~ headers[Transfer_Encoding])
24
+ end
25
+ @deferred = nil
26
+ ev_write_response(status, headers, body, alive)
27
+ end
28
+
17
29
  def post_init
18
30
  @hp = HttpParser.new
19
31
  @env = @hp.env
@@ -36,14 +48,15 @@ module Rainbows::EvCore
36
48
  end
37
49
 
38
50
  # returns whether to enable response chunking for autochunk models
39
- def stream_response_headers(status, headers)
40
- if headers['Content-Length']
51
+ def stream_response_headers(status, headers, alive)
52
+ headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
53
+ if headers.include?(Content_Length)
41
54
  rv = false
42
55
  else
43
- rv = !!(headers['Transfer-Encoding'] =~ %r{\Achunked\z}i)
56
+ rv = !!(headers[Transfer_Encoding] =~ %r{\Achunked\z}i)
44
57
  rv = false if headers.delete('X-Rainbows-Autochunk') == 'no'
45
58
  end
46
- write(response_header(status, headers))
59
+ write_headers(status, headers, alive)
47
60
  rv
48
61
  end
49
62
 
@@ -44,6 +44,7 @@ module Rainbows::EventMachine
44
44
  autoload :ResponsePipe, 'rainbows/event_machine/response_pipe'
45
45
  autoload :ResponseChunkPipe, 'rainbows/event_machine/response_chunk_pipe'
46
46
  autoload :TryDefer, 'rainbows/event_machine/try_defer'
47
+ autoload :Client, 'rainbows/event_machine/client'
47
48
 
48
49
  include Rainbows::Base
49
50
 
@@ -57,8 +58,9 @@ module Rainbows::EventMachine
57
58
  # given a INT, QUIT, or TERM signal)
58
59
  def worker_loop(worker) # :nodoc:
59
60
  init_worker_process(worker)
60
- G.server.app.respond_to?(:deferred?) and
61
- G.server.app = Rainbows::EventMachine::TryDefer[G.server.app]
61
+ server = Rainbows.server
62
+ server.app.respond_to?(:deferred?) and
63
+ server.app = TryDefer.new(server.app)
62
64
 
63
65
  # enable them both, should be non-fatal if not supported
64
66
  EM.epoll
@@ -68,14 +70,14 @@ module Rainbows::EventMachine
68
70
  max = worker_connections + LISTENERS.size
69
71
  Rainbows::EventMachine::Server.const_set(:MAX, max)
70
72
  Rainbows::EventMachine::Server.const_set(:CL, client_class)
71
- client_class.const_set(:APP, G.server.app)
73
+ client_class.const_set(:APP, Rainbows.server.app)
72
74
  Rainbows::EvCore.setup
73
75
  EM.run {
74
76
  conns = EM.instance_variable_get(:@conns) or
75
77
  raise RuntimeError, "EM @conns instance variable not accessible!"
76
78
  Rainbows::EventMachine::Server.const_set(:CUR, conns)
77
79
  EM.add_periodic_timer(1) do
78
- unless G.tick
80
+ unless Rainbows.tick
79
81
  conns.each_value { |c| client_class === c and c.quit }
80
82
  EM.stop if conns.empty? && EM.reactor_running?
81
83
  end
@@ -89,5 +91,4 @@ module Rainbows::EventMachine
89
91
  end
90
92
  end
91
93
  # :enddoc:
92
- require 'rainbows/event_machine/client'
93
94
  require 'rainbows/event_machine/server'
@@ -1,12 +1,11 @@
1
1
  # -*- encoding: binary -*-
2
2
  # :enddoc:
3
3
  class Rainbows::EventMachine::Client < EM::Connection
4
- attr_writer :body
5
4
  include Rainbows::EvCore
6
5
 
7
6
  def initialize(io)
8
7
  @_io = io
9
- @body = nil
8
+ @deferred = nil
10
9
  end
11
10
 
12
11
  alias write send_data
@@ -15,7 +14,7 @@ class Rainbows::EventMachine::Client < EM::Connection
15
14
  # To avoid clobbering the current streaming response
16
15
  # (often a static file), we do not attempt to process another
17
16
  # request on the same connection until the first is complete
18
- if @body
17
+ if @deferred
19
18
  if data
20
19
  @buf << data
21
20
  @_io.shutdown(Socket::SHUT_RD) if @buf.size > 0x1c000
@@ -35,83 +34,77 @@ class Rainbows::EventMachine::Client < EM::Connection
35
34
  set_comm_inactivity_timeout 0
36
35
  @env[RACK_INPUT] = @input
37
36
  @env[REMOTE_ADDR] = @_io.kgio_addr
38
- @env[ASYNC_CALLBACK] = method(:em_write_response)
37
+ @env[ASYNC_CALLBACK] = method(:write_async_response)
39
38
  @env[ASYNC_CLOSE] = EM::DefaultDeferrable.new
39
+ status, headers, body = catch(:async) {
40
+ APP.call(@env.merge!(RACK_DEFAULTS))
41
+ }
40
42
 
41
- response = catch(:async) { APP.call(@env.update(RACK_DEFAULTS)) }
42
-
43
- # too tricky to support pipelining with :async since the
44
- # second (pipelined) request could be a stuck behind a
45
- # long-running async response
46
- (response.nil? || -1 == response[0]) and return @state = :close
43
+ (nil == status || -1 == status) ? @deferred = true :
44
+ ev_write_response(status, headers, body, @hp.next?)
45
+ end
47
46
 
48
- if @hp.next? && G.alive && G.kato > 0
49
- @state = :headers
50
- em_write_response(response, true)
51
- if @buf.empty?
52
- set_comm_inactivity_timeout(G.kato)
53
- elsif @body.nil?
54
- EM.next_tick { receive_data(nil) }
55
- end
56
- else
57
- em_write_response(response, false)
47
+ def deferred_errback(orig_body)
48
+ @deferred.errback do
49
+ orig_body.close if orig_body.respond_to?(:close)
50
+ quit
58
51
  end
59
52
  end
60
53
 
61
- def em_write_response(response, alive = false)
62
- status, headers, body = response
63
- if @hp.headers?
64
- headers = HH.new(headers)
65
- headers[CONNECTION] = alive ? KEEP_ALIVE : CLOSE
66
- else
67
- headers = nil
54
+ def deferred_callback(orig_body, alive)
55
+ @deferred.callback do
56
+ orig_body.close if orig_body.respond_to?(:close)
57
+ @deferred = nil
58
+ alive ? receive_data(nil) : quit
68
59
  end
60
+ end
69
61
 
62
+ def ev_write_response(status, headers, body, alive)
63
+ @state = :headers if alive
70
64
  if body.respond_to?(:errback) && body.respond_to?(:callback)
71
- @body = body
72
- body.callback { quit }
73
- body.errback { quit }
74
- # async response, this could be a trickle as is in comet-style apps
75
- headers[CONNECTION] = CLOSE if headers
76
- alive = true
65
+ @deferred = body
66
+ deferred_errback(body)
67
+ deferred_callback(body, alive)
77
68
  elsif body.respond_to?(:to_path)
78
69
  st = File.stat(path = body.to_path)
79
70
 
80
71
  if st.file?
81
- write(response_header(status, headers)) if headers
82
- @body = stream_file_data(path)
83
- @body.errback do
84
- body.close if body.respond_to?(:close)
85
- quit
86
- end
87
- @body.callback do
88
- body.close if body.respond_to?(:close)
89
- @body = nil
90
- alive ? receive_data(nil) : quit
91
- end
72
+ write_headers(status, headers, alive)
73
+ @deferred = stream_file_data(path)
74
+ deferred_errback(body)
75
+ deferred_callback(body, alive)
92
76
  return
93
77
  elsif st.socket? || st.pipe?
94
- @body = io = body_to_io(body)
95
- chunk = stream_response_headers(status, headers) if headers
78
+ io = body_to_io(@deferred = body)
79
+ chunk = stream_response_headers(status, headers, alive)
96
80
  m = chunk ? Rainbows::EventMachine::ResponseChunkPipe :
97
81
  Rainbows::EventMachine::ResponsePipe
98
- return EM.watch(io, m, self, alive, body).notify_readable = true
82
+ return EM.watch(io, m, self).notify_readable = true
99
83
  end
100
84
  # char or block device... WTF? fall through to body.each
101
85
  end
102
-
103
- write(response_header(status, headers)) if headers
104
- write_body_each(self, body)
105
- quit unless alive
86
+ write_response(status, headers, body, alive)
87
+ if alive
88
+ if @deferred.nil?
89
+ if @buf.empty?
90
+ set_comm_inactivity_timeout(Rainbows.keepalive_timeout)
91
+ else
92
+ EM.next_tick { receive_data(nil) }
93
+ end
94
+ end
95
+ else
96
+ quit unless @deferred
97
+ end
106
98
  end
107
99
 
108
100
  def next!
109
- @hp.keepalive? ? receive_data(@body = nil) : quit
101
+ @deferred.close if @deferred.respond_to?(:close)
102
+ @hp.keepalive? ? receive_data(@deferred = nil) : quit
110
103
  end
111
104
 
112
105
  def unbind
113
106
  async_close = @env[ASYNC_CLOSE] and async_close.succeed
114
- @body.respond_to?(:fail) and @body.fail
107
+ @deferred.respond_to?(:fail) and @deferred.fail
115
108
  begin
116
109
  @_io.close
117
110
  rescue Errno::EBADF