iomultiplex 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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