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.
@@ -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