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,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