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,89 @@
|
|
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
|
+
# Post processing - used by OpenSSL etc to force a read until wait required
|
20
|
+
# in case there is buffered data
|
21
|
+
module Post
|
22
|
+
def defer(client)
|
23
|
+
post_process client, POST_DEFER
|
24
|
+
end
|
25
|
+
|
26
|
+
def force_read(client)
|
27
|
+
post_process client, POST_FORCE_READ
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove_post(client)
|
31
|
+
@post_processing.delete client
|
32
|
+
return if @scheduled_post_processing.nil?
|
33
|
+
@scheduled_post_processing.delete client
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
POST_DEFER = 1
|
40
|
+
POST_FORCE_READ = 2
|
41
|
+
|
42
|
+
def initialize_post
|
43
|
+
@post_processing = {}
|
44
|
+
@scheduled_post_processing = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def schedule_post_processing
|
48
|
+
return false if @post_processing.empty?
|
49
|
+
|
50
|
+
# Run deferred after we finish this loop
|
51
|
+
# New defers then carry to next loop
|
52
|
+
@scheduled_post_processing = @post_processing
|
53
|
+
@post_processing = {}
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def trigger_post_processing
|
58
|
+
return if @scheduled_post_processing.nil?
|
59
|
+
|
60
|
+
@scheduled_post_processing.each do |client, flag|
|
61
|
+
# During handle_read a handle_data happens so if we have both defer
|
62
|
+
# and read we also should use handle_read
|
63
|
+
if flag & POST_FORCE_READ != 0
|
64
|
+
log_debug 'Post processing', :client => client.id, :what => 'read'
|
65
|
+
client.handle_read
|
66
|
+
else
|
67
|
+
log_debug 'Post processing', :client => client.id, :what => 'defer'
|
68
|
+
client.handle_data
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@scheduled_post_processing = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def post_process(client, flag)
|
76
|
+
current = @post_processing.key?(client) ? @post_processing[client] : nil
|
77
|
+
return if !current.nil? && current & flag != 0
|
78
|
+
|
79
|
+
log_debug 'Scheduled post processing',
|
80
|
+
:client => client.id,
|
81
|
+
:what => flag & POST_FORCE_READ != 0 ? 'read' : 'defer'
|
82
|
+
|
83
|
+
@post_processing[client] ||= 0
|
84
|
+
@post_processing[client] |= flag
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end # ::Post
|
88
|
+
end # ::Mixins
|
89
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,145 @@
|
|
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 'nio'
|
18
|
+
|
19
|
+
module IOMultiplex
|
20
|
+
module Mixins
|
21
|
+
# IO Select mixin
|
22
|
+
# Depends on Mixins::State
|
23
|
+
module Select
|
24
|
+
def wait_read(client)
|
25
|
+
set_wait client, State::STATE_WAIT_READ, true
|
26
|
+
end
|
27
|
+
|
28
|
+
def wait_write(client)
|
29
|
+
set_wait client, State::STATE_WAIT_WRITE, true
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop_read(client)
|
33
|
+
set_wait client, State::STATE_WAIT_READ, false
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop_write(client)
|
37
|
+
set_wait client, State::STATE_WAIT_WRITE, false
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop_all(client)
|
41
|
+
state = must_get_state(client)
|
42
|
+
return if \
|
43
|
+
state & (State::STATE_WAIT_READ | State::STATE_WAIT_WRITE) == 0
|
44
|
+
|
45
|
+
state = remove_state client,
|
46
|
+
State::STATE_WAIT_READ | State::STATE_WAIT_WRITE
|
47
|
+
update_select client, state
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def initialize_select(options)
|
53
|
+
setup_select_logslow options if options[:log_slow]
|
54
|
+
|
55
|
+
@nio = NIO::Selector.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_wait(client, flag, desired)
|
59
|
+
state = must_get_state(client)
|
60
|
+
|
61
|
+
if desired
|
62
|
+
return unless state & flag == 0
|
63
|
+
state = add_state(client, flag)
|
64
|
+
else
|
65
|
+
return if state & flag == 0
|
66
|
+
state = remove_state(client, flag)
|
67
|
+
end
|
68
|
+
|
69
|
+
update_select client, state
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_select(client, state)
|
74
|
+
@nio.deregister client.io
|
75
|
+
|
76
|
+
if state & State::STATE_WAIT_READ == 0 &&
|
77
|
+
state & State::STATE_WAIT_WRITE == 0
|
78
|
+
log_debug 'NIO::Select interest updated',
|
79
|
+
:client => client.id, :interests => nil
|
80
|
+
return
|
81
|
+
elsif state & State::STATE_WAIT_READ == 0
|
82
|
+
interests = :w
|
83
|
+
elsif state & State::STATE_WAIT_WRITE == 0
|
84
|
+
interests = :r
|
85
|
+
else
|
86
|
+
interests = :rw
|
87
|
+
end
|
88
|
+
|
89
|
+
monitor = @nio.register(client.io, interests)
|
90
|
+
monitor.value = client
|
91
|
+
log_debug 'NIO::Select interest updated',
|
92
|
+
:client => client.id, :interests => interests
|
93
|
+
end
|
94
|
+
|
95
|
+
def select_io(timeout)
|
96
|
+
log_debug 'NIO::Select enter', :timeout => timeout
|
97
|
+
|
98
|
+
@nio.select(timeout) do |monitor|
|
99
|
+
next if get_state(monitor.value).nil?
|
100
|
+
if monitor.readable?
|
101
|
+
log_debug 'NIO::Select signalling',
|
102
|
+
:what => :read, :client => monitor.value.id
|
103
|
+
monitor.value.handle_read
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check we didn't remove the socket before we call monitor.writable?
|
107
|
+
# otherwise it will throw a Java CancelledKeyException wrapped in
|
108
|
+
# NativeException because we tried to access a removed monitor
|
109
|
+
next if get_state(monitor.value).nil? || !monitor.writable?
|
110
|
+
log_debug 'NIO::Select signalling',
|
111
|
+
:what => :write, :client => monitor.value.id
|
112
|
+
monitor.value.handle_write
|
113
|
+
end
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def setup_select_logslow
|
118
|
+
require 'iomultiplex/logslow.rb'
|
119
|
+
class <<self
|
120
|
+
extend LogSlow
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
alias_method :_orig_select_io, :select_io
|
125
|
+
define_method :select_io do |next_timer|
|
126
|
+
log_slow _orig_select_io, [next_timer], 100, _select_io_diagnostics
|
127
|
+
end
|
128
|
+
|
129
|
+
define_method :_select_io_diagnostics do
|
130
|
+
timeout = next_timer
|
131
|
+
if timeout.nil?
|
132
|
+
timer_due = 'None'
|
133
|
+
timer_delay = 'N/A'
|
134
|
+
else
|
135
|
+
timer_due = timeout.to_f.ceil
|
136
|
+
timer_delay = now > timer ? ((now - timeout) * 1000).to_i : 'None'
|
137
|
+
end
|
138
|
+
{ :timer_due => timer_due, :timer_delay => timer_delay }
|
139
|
+
end
|
140
|
+
end # <<self
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
end # ::Select
|
144
|
+
end # ::Mixins
|
145
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,79 @@
|
|
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
|
+
# State handling methods
|
20
|
+
module State
|
21
|
+
protected
|
22
|
+
|
23
|
+
STATE_REGISTERED = 0
|
24
|
+
STATE_WAIT_READ = 1
|
25
|
+
STATE_WAIT_WRITE = 2
|
26
|
+
STATE_TIMER = 4
|
27
|
+
|
28
|
+
def initialize_state
|
29
|
+
@lookup = Hash.new do |h, k|
|
30
|
+
h[k] = STATE_REGISTERED
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the state for a given client or throws an exception if it
|
35
|
+
# isn't registered
|
36
|
+
def must_get_state(client)
|
37
|
+
lookup = get_state client
|
38
|
+
raise ArgumentError, 'Client is not registered' if lookup.nil?
|
39
|
+
lookup
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return the state for a given client
|
43
|
+
# Returns nil if the client is not registered
|
44
|
+
def get_state(client)
|
45
|
+
@lookup.key?(client) ? @lookup[client] : nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Adds a state flag for a client
|
49
|
+
# If the client is not registered, creates a registration
|
50
|
+
def add_state(client, flag)
|
51
|
+
@lookup[client] |= flag
|
52
|
+
end
|
53
|
+
|
54
|
+
# Removes a state flag for a client
|
55
|
+
# If the client is not registered, creates a registration
|
56
|
+
def remove_state(client, flag)
|
57
|
+
@lookup[client] ^= flag
|
58
|
+
end
|
59
|
+
|
60
|
+
# Register a client
|
61
|
+
def register_state(client)
|
62
|
+
raise ArgumentError, 'Client is already registered' if get_state(client)
|
63
|
+
@lookup[client]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Deregister a client
|
67
|
+
def deregister_state(client)
|
68
|
+
@lookup.delete client
|
69
|
+
end
|
70
|
+
|
71
|
+
# Loop registered clients (non-timer clients)
|
72
|
+
def each_registered_client
|
73
|
+
@lookup.each do |client, state|
|
74
|
+
yield client unless state & STATE_REGISTERED == 0
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end # ::State
|
78
|
+
end # ::Mixins
|
79
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,87 @@
|
|
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
|
+
# Timer handling methods
|
20
|
+
# Depends on Mixins::State
|
21
|
+
# TODO: We should use monotonic clock here!
|
22
|
+
module Timer
|
23
|
+
def add_timer(timer, at)
|
24
|
+
raise ArgumentError, 'Timer must response to "timer"' \
|
25
|
+
unless timer.respond_to? :timer
|
26
|
+
|
27
|
+
state = get_state(timer)
|
28
|
+
exists = state & State::STATE_TIMER != 0
|
29
|
+
@timers.delete [@timers_time[timer], timer] if exists
|
30
|
+
|
31
|
+
entry = [at, timer]
|
32
|
+
@timers.add entry
|
33
|
+
@timers_time[timer] = at
|
34
|
+
|
35
|
+
add_state timer, State::STATE_TIMER unless exists
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_timer(timer)
|
40
|
+
state = must_get_state(timer)
|
41
|
+
return unless state & State::STATE_TIMER != 0
|
42
|
+
|
43
|
+
entry = [@timers_time[timer], timer]
|
44
|
+
remove_timer_state entry
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def initialize_timers
|
51
|
+
@timers = SortedSet.new
|
52
|
+
@timers_time = {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def next_timer
|
56
|
+
entry = @timers.first
|
57
|
+
entry.nil? ? nil : entry[0]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Trigger available timers
|
61
|
+
def trigger_timers
|
62
|
+
return if @timers.empty?
|
63
|
+
|
64
|
+
now = Time.now
|
65
|
+
until @timers.empty?
|
66
|
+
entry = @timers.first
|
67
|
+
break if entry[0] > now
|
68
|
+
remove_timer_state entry
|
69
|
+
entry[1].timer
|
70
|
+
end
|
71
|
+
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove a timer from the internal state
|
76
|
+
def remove_timer_state(entry)
|
77
|
+
timer = entry[1]
|
78
|
+
@timers.delete entry
|
79
|
+
@timers_time.delete timer
|
80
|
+
|
81
|
+
state = remove_state(timer, State::STATE_TIMER)
|
82
|
+
deregister_state timer if state == 0
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end # ::Timer
|
86
|
+
end # ::Mixins
|
87
|
+
end # ::IOMultiplex
|
@@ -0,0 +1,124 @@
|
|
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
|
+
# TODO: Refactor and finish
|
18
|
+
|
19
|
+
require 'iomultiplex/mixins/logger'
|
20
|
+
|
21
|
+
module IOMultiplex
|
22
|
+
# A pool of multiplexers amongst which incoming connections can be distributed
|
23
|
+
class MultiplexerPool
|
24
|
+
include Mixins::Logger
|
25
|
+
|
26
|
+
attr_reader :id
|
27
|
+
|
28
|
+
def initialize(options)
|
29
|
+
%w(num_workers).each do |k|
|
30
|
+
raise ArgumentError, "Required option missing: #{k}" \
|
31
|
+
unless options[k.to_sym]
|
32
|
+
end
|
33
|
+
|
34
|
+
initialize_logger options[:logger], options[:logger_context]
|
35
|
+
|
36
|
+
@id = options[:id] || object_id
|
37
|
+
add_logger_context 'multiplexer_pool', @id
|
38
|
+
|
39
|
+
@num_workers = options[:num_workers]
|
40
|
+
@queued_clients = []
|
41
|
+
|
42
|
+
reset_state
|
43
|
+
end
|
44
|
+
|
45
|
+
def start
|
46
|
+
raise 'Already started' unless @workers.empty?
|
47
|
+
|
48
|
+
@num_workers.times do |i|
|
49
|
+
@workers[i] = Multiplexer.new \
|
50
|
+
logger: logger,
|
51
|
+
logger_context: logger_context,
|
52
|
+
id: "Worker-#{i}"
|
53
|
+
|
54
|
+
@queues[i] = Queue.new
|
55
|
+
|
56
|
+
@threads[i] = Thread.new(@workers[i], &:run)
|
57
|
+
end
|
58
|
+
|
59
|
+
distribute_queued_clients
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def distribute(client)
|
64
|
+
return queue_client(client) if @workers.empty?
|
65
|
+
|
66
|
+
s = nil
|
67
|
+
c = 0
|
68
|
+
@workers.each_index do |i|
|
69
|
+
connections = @workers[i].connections
|
70
|
+
# TODO: Make customisable this maxmium
|
71
|
+
next unless connections < 1000 && (s.nil? || c > connections)
|
72
|
+
s = i
|
73
|
+
c = connections
|
74
|
+
end
|
75
|
+
|
76
|
+
return false if s.nil?
|
77
|
+
worker = @workers[s]
|
78
|
+
|
79
|
+
log_debug 'Distributing new client',
|
80
|
+
:client => client.id, :worker => worker.id
|
81
|
+
@queues[s] << client
|
82
|
+
worker.callback do
|
83
|
+
process s
|
84
|
+
end
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def shutdown
|
89
|
+
raise 'Not started' if @workers.empty?
|
90
|
+
|
91
|
+
# Raise shutdown in all client threads and join then
|
92
|
+
@workers.each(&:shutdown)
|
93
|
+
@threads.each(&:join)
|
94
|
+
|
95
|
+
reset_state
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def reset_state
|
102
|
+
@workers = []
|
103
|
+
@queues = []
|
104
|
+
@threads = []
|
105
|
+
end
|
106
|
+
|
107
|
+
def queue_client(client)
|
108
|
+
@queued_clients << client
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
|
112
|
+
def distribute_queued_clients
|
113
|
+
distribute @queued_clients.pop until @queued_clients.empty?
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def process(i)
|
118
|
+
# Sockets for the worker
|
119
|
+
log_debug 'Receiving new sockets', length: @queues[i].length
|
120
|
+
@workers[i].add @queues[i].pop until @queues[i].empty?
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|