iomultiplex 0.1.0

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