iomultiplex 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/lib/iomultiplex.rb +26 -0
- data/lib/iomultiplex/iomultiplex.rb +126 -0
- data/lib/iomultiplex/ioreactor.rb +118 -0
- data/lib/iomultiplex/ioreactor/buffered.rb +55 -0
- data/lib/iomultiplex/ioreactor/openssl.rb +91 -0
- data/lib/iomultiplex/mixins/callback.rb +44 -0
- data/lib/iomultiplex/mixins/ioreactor/read.rb +180 -0
- data/lib/iomultiplex/mixins/ioreactor/write.rb +109 -0
- data/lib/iomultiplex/mixins/logger.rb +64 -0
- data/lib/iomultiplex/mixins/logslow.rb +40 -0
- data/lib/iomultiplex/mixins/openssl.rb +148 -0
- data/lib/iomultiplex/mixins/post.rb +89 -0
- data/lib/iomultiplex/mixins/select.rb +145 -0
- data/lib/iomultiplex/mixins/state.rb +79 -0
- data/lib/iomultiplex/mixins/timer.rb +87 -0
- data/lib/iomultiplex/pool.rb +124 -0
- data/lib/iomultiplex/stringbuffer.rb +87 -0
- data/lib/iomultiplex/tcplistener.rb +56 -0
- data/lib/iomultiplex/version.rb +4 -0
- metadata +98 -0
@@ -0,0 +1,180 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Copyright 2014-2016 Jason Woods
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module IOMultiplex
|
18
|
+
class NotEnoughData < StandardError; end
|
19
|
+
|
20
|
+
module Mixins
|
21
|
+
module IOReactor
|
22
|
+
# Read mixin for IOReactor
|
23
|
+
module Read
|
24
|
+
# TODO: Make these customisable?
|
25
|
+
READ_BUFFER_MAX = 16_384
|
26
|
+
READ_SIZE = 16_384
|
27
|
+
|
28
|
+
def handle_read
|
29
|
+
begin
|
30
|
+
do_read
|
31
|
+
rescue EOFError, IOError, Errno::ECONNRESET => e
|
32
|
+
read_exception e
|
33
|
+
end
|
34
|
+
|
35
|
+
handle_data
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_data
|
40
|
+
@was_read_full = read_full?
|
41
|
+
process unless @read_buffer.empty?
|
42
|
+
nil
|
43
|
+
rescue NotEnoughData
|
44
|
+
return send_eof if @eof_scheduled
|
45
|
+
|
46
|
+
# Allow overfilling of the read buffer in the event
|
47
|
+
# read(>=READ_BUFFER_MAX) was called
|
48
|
+
reschedule_read true
|
49
|
+
else
|
50
|
+
return send_eof if @eof_scheduled && @read_buffer.empty?
|
51
|
+
|
52
|
+
reschedule_read
|
53
|
+
end
|
54
|
+
|
55
|
+
def read(n)
|
56
|
+
raise 'Socket is not attached' unless @attached
|
57
|
+
raise IOError, 'Socket is closed' if @io.closed?
|
58
|
+
raise NotEnoughData, 'Not enough data', nil if @read_buffer.length < n
|
59
|
+
|
60
|
+
@read_buffer.read n
|
61
|
+
end
|
62
|
+
|
63
|
+
def discard
|
64
|
+
@read_buffer.reset
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
# Pause read processing
|
69
|
+
# Takes effect on the next reschedule, which occurs after each read
|
70
|
+
# processing takes place
|
71
|
+
def pause
|
72
|
+
return if @pause
|
73
|
+
log_debug 'pause read'
|
74
|
+
@pause = true
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# Resume read processing
|
79
|
+
def resume
|
80
|
+
return unless @pause
|
81
|
+
log_debug 'resume read'
|
82
|
+
@pause = false
|
83
|
+
reschedule_read
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def read_full?
|
88
|
+
@read_buffer.length >= READ_BUFFER_MAX
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
def do_read
|
94
|
+
read_action
|
95
|
+
rescue IO::WaitReadable, Errno::EINTR, Errno::EAGAIN
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_eof
|
100
|
+
unless @exception.nil?
|
101
|
+
exception @exception if respond_to?(:exception)
|
102
|
+
force_close
|
103
|
+
return
|
104
|
+
end
|
105
|
+
|
106
|
+
eof if respond_to?(:eof)
|
107
|
+
close
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
# To balance threads, process is allowed to return without processing
|
112
|
+
# all data, and will get called again after one round even if read not
|
113
|
+
# ready again. This allows us to spread processing more evenly if the
|
114
|
+
# processor is smart
|
115
|
+
# If the read buffer is >=4096 we can also skip read polling otherwise
|
116
|
+
# we will add another 4096 bytes and not process it as fast as we are
|
117
|
+
# adding the data
|
118
|
+
# Also, allow the processor to pause read which has the same effect -
|
119
|
+
# it is expected a timer or something will then resume read - this can
|
120
|
+
# be if the client is waiting on a background thread
|
121
|
+
# NOTE: Processor should be careful, if it processes nothing this can
|
122
|
+
# cause a busy loop
|
123
|
+
def reschedule_read(overfill = false)
|
124
|
+
if @pause
|
125
|
+
@multiplexer.stop_read self
|
126
|
+
return
|
127
|
+
end
|
128
|
+
|
129
|
+
if read_full? && !overfill
|
130
|
+
# Stop reading, the buffer is too full, let the processor catch up
|
131
|
+
log_info 'Holding read due to full read buffer'
|
132
|
+
@multiplexer.stop_read self
|
133
|
+
@multiplexer.defer self
|
134
|
+
return
|
135
|
+
end
|
136
|
+
|
137
|
+
# Only schedule read if write isn't full - this allows us to drain
|
138
|
+
# write buffer before reading again and prevents a client from sending
|
139
|
+
# large amounts of data without receiving responses
|
140
|
+
if @w && write_full?
|
141
|
+
log_info 'Holding read due to full write buffer'
|
142
|
+
@multiplexer.stop_read self
|
143
|
+
return
|
144
|
+
end
|
145
|
+
|
146
|
+
schedule_read
|
147
|
+
end
|
148
|
+
|
149
|
+
# Schedules the next read action, can be overrided if necessary to
|
150
|
+
# change how the next read should be scheduled
|
151
|
+
def schedule_read
|
152
|
+
@multiplexer.defer self unless @read_buffer.empty?
|
153
|
+
|
154
|
+
# Resume read signal if we had paused due to full buffer
|
155
|
+
@multiplexer.wait_read self if @was_read_full
|
156
|
+
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Can be overridden for other IO objects
|
161
|
+
def read_nonblock(n)
|
162
|
+
log_debug 'read_nonblock', count: n
|
163
|
+
@io.read_nonblock(n)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Can be overriden for other IO objects
|
167
|
+
def read_action
|
168
|
+
@read_buffer << read_nonblock(READ_SIZE)
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
|
172
|
+
def read_exception(e)
|
173
|
+
@eof_scheduled = true
|
174
|
+
@exception = e unless e.is_a?(EOFError)
|
175
|
+
@multiplexer.stop_read self
|
176
|
+
end
|
177
|
+
end # ::Read
|
178
|
+
end # ::IOReactor
|
179
|
+
end # ::Mixins
|
180
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Copyright 2014-2016 Jason Woods
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module IOMultiplex
|
18
|
+
module Mixins
|
19
|
+
module IOReactor
|
20
|
+
# Write mixin for IOReactor
|
21
|
+
module Write
|
22
|
+
# TODO: Make these customisable?
|
23
|
+
WRITE_BUFFER_MAX = 16_384
|
24
|
+
WRITE_SIZE = 4_096
|
25
|
+
|
26
|
+
def handle_write
|
27
|
+
begin
|
28
|
+
do_write
|
29
|
+
rescue IOError, Errno::ECONNRESET => e
|
30
|
+
write_exception e
|
31
|
+
end
|
32
|
+
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def write(data)
|
37
|
+
raise 'Socket is not attached' unless @attached
|
38
|
+
raise IOError, 'Socket is closed' if @io.closed?
|
39
|
+
|
40
|
+
@write_buffer.push data
|
41
|
+
handle_write if @write_immediately
|
42
|
+
|
43
|
+
# Write buffer too large - pause read polling
|
44
|
+
if @r && write_full?
|
45
|
+
log_debug 'write buffer full, pausing read',
|
46
|
+
count: @write_buffer.length
|
47
|
+
@multiplexer.stop_read self
|
48
|
+
@multiplexer.remove_post self
|
49
|
+
end
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def write_full?
|
54
|
+
@write_buffer.length >= WRITE_BUFFER_MAX
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def reading?
|
60
|
+
@r && !@pause
|
61
|
+
end
|
62
|
+
|
63
|
+
def do_write
|
64
|
+
@was_read_held = reading? && write_full?
|
65
|
+
@write_buffer.shift write_action
|
66
|
+
|
67
|
+
if @write_buffer.empty?
|
68
|
+
force_close if @close_scheduled
|
69
|
+
return
|
70
|
+
end
|
71
|
+
|
72
|
+
check_read_throttle
|
73
|
+
rescue IO::WaitWritable, Errno::EINTR, Errno::EAGAIN
|
74
|
+
# Wait for write
|
75
|
+
@write_immediately = false
|
76
|
+
@multiplexer.wait_write self
|
77
|
+
else
|
78
|
+
@write_immediately = true
|
79
|
+
@multiplexer.stop_write self
|
80
|
+
end
|
81
|
+
|
82
|
+
def check_read_throttle
|
83
|
+
return unless @was_read_held && !write_full?
|
84
|
+
|
85
|
+
log_debug 'write buffer no longer full, resuming read',
|
86
|
+
count: @write_buffer.length
|
87
|
+
@multiplexer.wait_read self
|
88
|
+
reschedule_read
|
89
|
+
end
|
90
|
+
|
91
|
+
# Can be overridden for other IO objects
|
92
|
+
def write_nonblock(data)
|
93
|
+
log_debug 'write_nonblock', count: data.length
|
94
|
+
@io.write_nonblock(data)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Can be overriden for other IO objects
|
98
|
+
def write_action
|
99
|
+
write_nonblock @write_buffer.peek(WRITE_SIZE)
|
100
|
+
end
|
101
|
+
|
102
|
+
def write_exception(e)
|
103
|
+
exception e if respond_to?(:exception)
|
104
|
+
force_close
|
105
|
+
end
|
106
|
+
end # ::Write
|
107
|
+
end # ::IOReactor
|
108
|
+
end # ::Mixins
|
109
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Copyright 2014-2016 Jason Woods
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module IOMultiplex
|
18
|
+
module Mixins
|
19
|
+
# Logger provides ability for object specific context in logs
|
20
|
+
module Logger
|
21
|
+
attr_reader :logger
|
22
|
+
attr_reader :logger_context
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def initialize_logger(logger = nil, logger_context = nil)
|
27
|
+
@logger = logger || Cabin::Channel.get(IOMultiplex)
|
28
|
+
@logger_context = logger_context.nil? ? {} : logger_context
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_logger_context(key, value)
|
32
|
+
@logger_context[key] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def clear_logger_context
|
36
|
+
@logger_context = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
%w(fatal error warn info debug).each do |level|
|
40
|
+
method = ('log_' + level).to_sym
|
41
|
+
pmethod = ('log_' + level + '?').to_sym
|
42
|
+
logger_method = level.to_sym
|
43
|
+
logger_pmethod = (level + '?').to_sym
|
44
|
+
|
45
|
+
define_method(method) do |*args|
|
46
|
+
return unless @logger.send(logger_pmethod)
|
47
|
+
|
48
|
+
args[1] ||= {}
|
49
|
+
|
50
|
+
unless args[1].is_a?(Hash)
|
51
|
+
raise ArgumentError 'Second argument must be a hash'
|
52
|
+
end
|
53
|
+
|
54
|
+
args[1].merge! @logger_context unless @logger_context.nil?
|
55
|
+
@logger.send logger_method, *args
|
56
|
+
end
|
57
|
+
|
58
|
+
define_method(pmethod) do
|
59
|
+
@logger.send logger_pmethod
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end # ::Logger
|
63
|
+
end # ::Mixins
|
64
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Copyright 2014-2016 Jason Woods
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module IOMultiplex
|
18
|
+
module Mixins
|
19
|
+
# LogSlow mixin to log slow function calls
|
20
|
+
module LogSlow
|
21
|
+
protected
|
22
|
+
|
23
|
+
# Wrap a method and report to the logger if it runs slowly
|
24
|
+
def log_slow(func, args = [], max_duration = 100, diagnostics = nil)
|
25
|
+
sub_start_time = Time.now
|
26
|
+
func.call(*args)
|
27
|
+
duration = ((Time.now - sub_start_time) * 1000).to_i
|
28
|
+
return unless duration > max_duration
|
29
|
+
extra = {
|
30
|
+
:duration_ms => duration,
|
31
|
+
:client => monitor.value.id
|
32
|
+
}
|
33
|
+
extra = diagnostics.call unless diagnostics.nil?
|
34
|
+
log_warn \
|
35
|
+
'Slow ' + func.to_s,
|
36
|
+
extra
|
37
|
+
end
|
38
|
+
end # ::LogSlow
|
39
|
+
end # ::Mixins
|
40
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Copyright 2014-2016 Jason Woods
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'openssl'
|
18
|
+
|
19
|
+
module IOMultiplex
|
20
|
+
module Mixins
|
21
|
+
# OpenSSL mixin, shared code amongst the OpenSSL IOReactors
|
22
|
+
module OpenSSL
|
23
|
+
def handle_read
|
24
|
+
if @write_on_read
|
25
|
+
handle_write
|
26
|
+
|
27
|
+
# Still need more reads to complete a write?
|
28
|
+
return if @write_on_read
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
|
33
|
+
# If we were waiting for a write signal so we could complete a read
|
34
|
+
# call, clear it since we now completed it
|
35
|
+
reset_read_on_write if @read_on_write
|
36
|
+
rescue IO::WaitWritable
|
37
|
+
# TODO: handle_data should really be triggered
|
38
|
+
# This captures an OpenSSL read wanting a write
|
39
|
+
@multiplexer.stop_read self
|
40
|
+
@multiplexer.wait_write self
|
41
|
+
@read_on_write = true
|
42
|
+
|
43
|
+
log_debug 'OpenSSL wants read on write'
|
44
|
+
end
|
45
|
+
|
46
|
+
def handle_write
|
47
|
+
if @read_on_write
|
48
|
+
handle_read
|
49
|
+
|
50
|
+
# Still need more writes to complete a read?
|
51
|
+
return if @read_on_write
|
52
|
+
end
|
53
|
+
|
54
|
+
super
|
55
|
+
|
56
|
+
# If we were waiting for a read signal so we could complete a write
|
57
|
+
# call, clear it since we now completed it
|
58
|
+
reset_write_on_read if @write_on_read
|
59
|
+
rescue IO::WaitReadable
|
60
|
+
# Write needs a read
|
61
|
+
@multiplexer.stop_write self
|
62
|
+
@multiplexer.wait_read self
|
63
|
+
@write_on_read = true
|
64
|
+
|
65
|
+
log_debug 'OpenSSL wants write on read'
|
66
|
+
end
|
67
|
+
|
68
|
+
def peer_cert
|
69
|
+
@ssl.peer_cert
|
70
|
+
end
|
71
|
+
|
72
|
+
def peer_cert_cn
|
73
|
+
return nil unless peer_cert
|
74
|
+
return @peer_cert_cn unless @peer_cert_cn.nil?
|
75
|
+
@peer_cert_cn = peer_cert.subject.to_a.find do |oid, value|
|
76
|
+
return value if oid == 'CN'
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def handshake_completed?
|
82
|
+
@handshake_completed
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def initialize_ssl(ssl_ctx)
|
88
|
+
@ssl = ::OpenSSL::SSL::SSLSocket.new(@io, ssl_ctx)
|
89
|
+
@ssl_ctx = ssl_ctx
|
90
|
+
@handshake_completed = false
|
91
|
+
@read_on_write = false
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def ssl_read_nonblock(n)
|
96
|
+
read = @ssl.read_nonblock n
|
97
|
+
rescue IO::WaitReadable
|
98
|
+
# OpenSSL wraps these, keep it flowing throw
|
99
|
+
raise
|
100
|
+
rescue ::OpenSSL::SSL::SSLError => e
|
101
|
+
# Throw back OpenSSL errors as IOErrors
|
102
|
+
raise IOError, "#{e.class.name}: #{e}"
|
103
|
+
ensure
|
104
|
+
log_debug 'SSL read_nonblock',
|
105
|
+
count: n, read: read.nil? ? nil : read.length
|
106
|
+
end
|
107
|
+
|
108
|
+
def ssl_write_nonblock(data)
|
109
|
+
written = @ssl.write_nonblock data
|
110
|
+
rescue IO::WaitWritable
|
111
|
+
# OpenSSL wraps these, keep it flowing throw
|
112
|
+
raise
|
113
|
+
rescue ::OpenSSL::SSL::SSLError => e
|
114
|
+
# Throw back OpenSSL errors as IOErrors
|
115
|
+
raise IOError, "#{e.class.name}: #{e}"
|
116
|
+
ensure
|
117
|
+
log_debug 'SSL write_nonblock',
|
118
|
+
count: data.length, written: written
|
119
|
+
end
|
120
|
+
|
121
|
+
def reset_read_on_write
|
122
|
+
@read_on_write = false
|
123
|
+
@multiplexer.wait_read self unless write_full?
|
124
|
+
end
|
125
|
+
|
126
|
+
def reset_write_on_read
|
127
|
+
@write_on_read = false
|
128
|
+
@write_immediately = true
|
129
|
+
end
|
130
|
+
|
131
|
+
def process_handshake
|
132
|
+
@ssl.accept_nonblock
|
133
|
+
@handshake_completed = true
|
134
|
+
add_logger_context 'peer_cert_cn', peer_cert_cn
|
135
|
+
|
136
|
+
handshake_completed if respond_to?(:handshake_completed)
|
137
|
+
|
138
|
+
log_debug 'Handshake completed'
|
139
|
+
rescue IO::WaitReadable, IO::WaitWritable
|
140
|
+
# OpenSSL wraps these, keep it flowing throw
|
141
|
+
raise
|
142
|
+
rescue ::OpenSSL::SSL::SSLError => e
|
143
|
+
# Throw back OpenSSL errors as IOErrors
|
144
|
+
raise IOError, "#{e.class.name}: #{e}"
|
145
|
+
end
|
146
|
+
end # ::OpenSSL
|
147
|
+
end # ::Mixins
|
148
|
+
end # ::IOMultiplex
|