mt-uv-rays 2.4.7

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,386 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set' # ruby std lib
4
+ require 'bisect' # insert into a sorted array
5
+ require 'tzinfo' # timezone information
6
+ require 'mt-uv-rays/scheduler/time'
7
+
8
+ module MTUV
9
+
10
+ class ScheduledEvent < ::MTLibuv::Q::DeferredPromise
11
+ # Note:: Comparable should not effect Hashes
12
+ # it will however effect arrays
13
+ include Comparable
14
+
15
+ attr_reader :created
16
+ attr_reader :last_scheduled
17
+ attr_reader :next_scheduled
18
+ attr_reader :trigger_count
19
+
20
+ def initialize(scheduler)
21
+ # Create a dummy deferrable
22
+ reactor = scheduler.reactor
23
+ defer = reactor.defer
24
+
25
+ # Record a backtrace of where the schedule was created
26
+ @trace = caller
27
+
28
+ # Setup common event variables
29
+ @scheduler = scheduler
30
+ @created = reactor.now
31
+ @last_scheduled = @created
32
+ @trigger_count = 0
33
+
34
+ # init the promise
35
+ super(reactor, defer)
36
+ end
37
+
38
+ # Provide relevant inspect information
39
+ def inspect
40
+ insp = String.new("#<#{self.class}:#{"0x00%x" % (self.__id__ << 1)} ")
41
+ insp << "trigger_count=#{@trigger_count} "
42
+ insp << "config=#{info} " if self.respond_to?(:info, true)
43
+ insp << "next_scheduled=#{to_time(@next_scheduled)} "
44
+ insp << "last_scheduled=#{to_time(@last_scheduled)} created=#{to_time(@created)}>"
45
+ insp
46
+ end
47
+ alias_method :to_s, :inspect
48
+
49
+ def to_time(internal_time)
50
+ if internal_time
51
+ ((internal_time + @scheduler.time_diff) / 1000).to_i
52
+ end
53
+ end
54
+
55
+
56
+ # required for comparable
57
+ def <=>(anOther)
58
+ @next_scheduled <=> anOther.next_scheduled
59
+ end
60
+
61
+ # reject the promise
62
+ def cancel
63
+ @defer.reject(:cancelled)
64
+ end
65
+
66
+ # notify listeners of the event
67
+ def trigger
68
+ @trigger_count += 1
69
+ @defer.notify(@reactor.now, self)
70
+ end
71
+ end
72
+
73
+ class OneShot < ScheduledEvent
74
+ def initialize(scheduler, at)
75
+ super(scheduler)
76
+
77
+ @next_scheduled = at
78
+ end
79
+
80
+ # Updates the scheduled time
81
+ def update(time)
82
+ @last_scheduled = @reactor.now
83
+
84
+ parsed_time = Scheduler.parse_in(time, :quiet)
85
+ if parsed_time.nil?
86
+ # Parse at will throw an error if time is invalid
87
+ parsed_time = Scheduler.parse_at(time) - @scheduler.time_diff
88
+ else
89
+ parsed_time += @last_scheduled
90
+ end
91
+
92
+ @next_scheduled = parsed_time
93
+ @scheduler.reschedule(self)
94
+ end
95
+
96
+ # Runs the event and cancels the schedule
97
+ def trigger
98
+ super()
99
+ @defer.resolve(:triggered)
100
+ end
101
+ end
102
+
103
+ class Repeat < ScheduledEvent
104
+ def initialize(scheduler, every)
105
+ super(scheduler)
106
+
107
+ @every = every
108
+ next_time
109
+ end
110
+
111
+ # Update the time period of the repeating event
112
+ #
113
+ # @param schedule [String] a standard CRON job line or a human readable string representing a time period.
114
+ def update(every, timezone: nil)
115
+ time = Scheduler.parse_in(every, :quiet) || Scheduler.parse_cron(every, :quiet, timezone: timezone)
116
+ raise ArgumentError.new("couldn't parse \"#{o}\"") if time.nil?
117
+
118
+ @every = time
119
+ reschedule
120
+ end
121
+
122
+ # removes the event from the schedule
123
+ def pause
124
+ @paused = true
125
+ @scheduler.unschedule(self)
126
+ end
127
+
128
+ # reschedules the event to the next time period
129
+ # can be used to reset a repeating timer
130
+ def resume
131
+ @paused = false
132
+ @last_scheduled = @reactor.now
133
+ reschedule
134
+ end
135
+
136
+ # Runs the event and reschedules
137
+ def trigger
138
+ super()
139
+ @reactor.next_tick do
140
+ # Do this next tick to avoid needless scheduling
141
+ # if the event is stopped in the callback
142
+ reschedule
143
+ end
144
+ end
145
+
146
+
147
+ protected
148
+
149
+
150
+ def next_time
151
+ @last_scheduled = @reactor.now
152
+ if @every.is_a? Integer
153
+ @next_scheduled = @last_scheduled + @every
154
+ else
155
+ # must be a cron
156
+ @next_scheduled = (@every.next.to_f * 1000).to_i - @scheduler.time_diff
157
+ end
158
+ end
159
+
160
+ def reschedule
161
+ unless @paused
162
+ next_time
163
+ @scheduler.reschedule(self)
164
+ end
165
+ end
166
+
167
+ def info
168
+ "repeat:#{@every.inspect}"
169
+ end
170
+ end
171
+
172
+
173
+ class Scheduler
174
+ attr_reader :reactor
175
+ attr_reader :time_diff
176
+ attr_reader :next
177
+
178
+
179
+ def initialize(reactor)
180
+ @reactor = reactor
181
+ @schedules = Set.new
182
+ @scheduled = []
183
+ @next = nil # Next schedule time
184
+ @timer = nil # Reference to the timer
185
+
186
+ # Not really required when used correctly
187
+ @critical = Mutex.new
188
+
189
+ # Every hour we should re-calibrate this (just in case)
190
+ calibrate_time
191
+
192
+ @calibrate = @reactor.timer do
193
+ calibrate_time
194
+ @calibrate.start(3600000)
195
+ end
196
+ @calibrate.start(3600000)
197
+ @calibrate.unref
198
+ end
199
+
200
+
201
+ # As the libuv time is taken from an arbitrary point in time we
202
+ # need to roughly synchronize between it and ruby's Time.now
203
+ def calibrate_time
204
+ @reactor.update_time
205
+ @time_diff = (Time.now.to_f * 1000).to_i - @reactor.now
206
+ end
207
+
208
+ # Create a repeating event that occurs each time period
209
+ #
210
+ # @param time [String] a human readable string representing the time period. 3w2d4h1m2s for example.
211
+ # @param callback [Proc] a block or method to execute when the event triggers
212
+ # @return [::MTUV::Repeat]
213
+ def every(time)
214
+ ms = Scheduler.parse_in(time)
215
+ event = Repeat.new(self, ms)
216
+ event.progress &Proc.new if block_given?
217
+ schedule(event)
218
+ event
219
+ end
220
+
221
+ # Create a one off event that occurs after the time period
222
+ #
223
+ # @param time [String] a human readable string representing the time period. 3w2d4h1m2s for example.
224
+ # @param callback [Proc] a block or method to execute when the event triggers
225
+ # @return [::MTUV::OneShot]
226
+ def in(time)
227
+ ms = @reactor.now + Scheduler.parse_in(time)
228
+ event = OneShot.new(self, ms)
229
+ event.progress &Proc.new if block_given?
230
+ schedule(event)
231
+ event
232
+ end
233
+
234
+ # Create a one off event that occurs at a particular date and time
235
+ #
236
+ # @param time [String, Time] a representation of a date and time that can be parsed
237
+ # @param callback [Proc] a block or method to execute when the event triggers
238
+ # @return [::MTUV::OneShot]
239
+ def at(time)
240
+ ms = Scheduler.parse_at(time) - @time_diff
241
+ event = OneShot.new(self, ms)
242
+ event.progress &Proc.new if block_given?
243
+ schedule(event)
244
+ event
245
+ end
246
+
247
+ # Create a repeating event that uses a CRON line to determine the trigger time
248
+ #
249
+ # @param schedule [String] a standard CRON job line.
250
+ # @param callback [Proc] a block or method to execute when the event triggers
251
+ # @return [::MTUV::Repeat]
252
+ def cron(schedule, timezone: nil)
253
+ ms = Scheduler.parse_cron(schedule, timezone: timezone)
254
+ event = Repeat.new(self, ms)
255
+ event.progress &Proc.new if block_given?
256
+ schedule(event)
257
+ event
258
+ end
259
+
260
+ # Schedules an event for execution
261
+ #
262
+ # @param event [ScheduledEvent]
263
+ def reschedule(event)
264
+ # Check promise is not resolved
265
+ return if event.resolved?
266
+
267
+ @critical.synchronize {
268
+ # Remove the event from the scheduled list and ensure it is in the schedules set
269
+ if @schedules.include?(event)
270
+ remove(event)
271
+ else
272
+ @schedules << event
273
+ end
274
+
275
+ # optimal algorithm for inserting into an already sorted list
276
+ Bisect.insort(@scheduled, event)
277
+
278
+ # Update the timer
279
+ check_timer
280
+ }
281
+ end
282
+
283
+ # Removes an event from the schedule
284
+ #
285
+ # @param event [ScheduledEvent]
286
+ def unschedule(event)
287
+ @critical.synchronize {
288
+ # Only call delete and update the timer when required
289
+ if @schedules.include?(event)
290
+ @schedules.delete(event)
291
+ remove(event)
292
+ check_timer
293
+ end
294
+ }
295
+ end
296
+
297
+
298
+ private
299
+
300
+
301
+ # Remove an element from the array
302
+ def remove(obj)
303
+ position = nil
304
+
305
+ @scheduled.each_index do |i|
306
+ # object level comparison
307
+ if obj.equal? @scheduled[i]
308
+ position = i
309
+ break
310
+ end
311
+ end
312
+
313
+ @scheduled.slice!(position) unless position.nil?
314
+ end
315
+
316
+ # First time schedule we want to bind to the promise
317
+ def schedule(event)
318
+ reschedule(event)
319
+
320
+ event.finally do
321
+ unschedule event
322
+ end
323
+ end
324
+
325
+ # Ensures the current timer, if any, is still
326
+ # accurate by checking the head of the schedule
327
+ def check_timer
328
+ @reactor.update_time
329
+
330
+ existing = @next
331
+ schedule = @scheduled.first
332
+ @next = schedule.nil? ? nil : schedule.next_scheduled
333
+
334
+ if existing != @next
335
+ # lazy load the timer
336
+ if @timer.nil?
337
+ new_timer
338
+ else
339
+ @timer.stop
340
+ end
341
+
342
+ if not @next.nil?
343
+ in_time = @next - @reactor.now
344
+
345
+ # Ensure there are never negative start times
346
+ if in_time > 3
347
+ @timer.start(in_time)
348
+ else
349
+ # Effectively next tick
350
+ @timer.start(0)
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ # Is called when the libuv timer fires
357
+ def on_timer
358
+ @critical.synchronize {
359
+ schedule = @scheduled.shift
360
+ @schedules.delete(schedule)
361
+ schedule.trigger
362
+
363
+ # execute schedules that are within 3ms of this event
364
+ # Basic timer coalescing..
365
+ now = @reactor.now + 3
366
+ while @scheduled.first && @scheduled.first.next_scheduled <= now
367
+ schedule = @scheduled.shift
368
+ @schedules.delete(schedule)
369
+ schedule.trigger
370
+ end
371
+ check_timer
372
+ }
373
+ end
374
+
375
+ # Provide some assurances on timer failure
376
+ def new_timer
377
+ @timer = @reactor.timer { on_timer }
378
+ @timer.finally do
379
+ new_timer
380
+ unless @next.nil?
381
+ @timer.start(@next)
382
+ end
383
+ end
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddress' # IP Address parser
4
+
5
+ module MTUV
6
+ class TcpServer < ::MTLibuv::TCP
7
+ def initialize(reactor, server, port, klass, *args)
8
+ super(reactor)
9
+
10
+ @klass = klass
11
+ @args = args
12
+
13
+ if server == port && port.is_a?(Integer)
14
+ # We are opening a socket descriptor
15
+ open(server)
16
+ else
17
+ # Perform basic checks before attempting to bind address
18
+ server = '127.0.0.1' if server == 'localhost'
19
+ if IPAddress.valid? server
20
+ @server = server
21
+ bind(server, port) { |client| new_connection(client) }
22
+ listen(1024)
23
+ else
24
+ raise ArgumentError, "Invalid server address #{server}"
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ private
31
+
32
+
33
+ def new_connection(client)
34
+ # prevent buffering
35
+ client.enable_nodelay
36
+
37
+ # create the connection class
38
+ c = @klass.new(client)
39
+ c.post_init *@args
40
+
41
+ # start read after post init and call connected
42
+ client.start_read
43
+ c.on_connect(client)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTUV
4
+ VERSION = '2.4.7'
5
+ end
data/lib/mt-uv-rays.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mt-libuv'
4
+
5
+ module MTUV
6
+ autoload :Ping, 'mt-uv-rays/ping'
7
+
8
+ # In-memory event scheduling
9
+ autoload :Scheduler, 'mt-uv-rays/scheduler'
10
+
11
+ # Intelligent stream buffering
12
+ autoload :BufferedTokenizer, 'mt-uv-rays/buffered_tokenizer'
13
+ autoload :AbstractTokenizer, 'mt-uv-rays/abstract_tokenizer'
14
+
15
+ # TCP Connections
16
+ autoload :TcpServer, 'mt-uv-rays/tcp_server'
17
+ autoload :Connection, 'mt-uv-rays/connection'
18
+ autoload :TcpConnection, 'mt-uv-rays/connection'
19
+ autoload :InboundConnection, 'mt-uv-rays/connection'
20
+ autoload :OutboundConnection, 'mt-uv-rays/connection'
21
+ autoload :DatagramConnection, 'mt-uv-rays/connection'
22
+
23
+ # HTTP related methods
24
+ autoload :HttpEndpoint, 'mt-uv-rays/http_endpoint'
25
+
26
+
27
+ # @private
28
+ def self.klass_from_handler(klass, handler = nil, *args)
29
+ klass = if handler and handler.is_a?(Class)
30
+ raise ArgumentError, "must provide module or subclass of #{klass.name}" unless klass >= handler
31
+ handler
32
+ elsif handler
33
+ begin
34
+ handler::UR_CONNECTION_CLASS
35
+ rescue NameError
36
+ handler::const_set(:UR_CONNECTION_CLASS, Class.new(klass) {include handler})
37
+ end
38
+ else
39
+ klass
40
+ end
41
+
42
+ arity = klass.instance_method(:post_init).arity
43
+ expected = arity >= 0 ? arity : -(arity + 1)
44
+ if (arity >= 0 and args.size != expected) or (arity < 0 and args.size < expected)
45
+ raise ArgumentError, "wrong number of arguments for #{klass}#post_init (#{args.size} for #{expected})"
46
+ end
47
+
48
+ klass
49
+ end
50
+
51
+
52
+ def self.connect(server, port, handler, *args)
53
+ klass = klass_from_handler(OutboundConnection, handler, *args)
54
+
55
+ c = klass.new server, port
56
+ c.post_init *args
57
+ c
58
+ end
59
+
60
+ def self.start_server(server, port, handler, *args)
61
+ thread = reactor # Get the reactor running on this thread
62
+ raise ThreadError, "There is no Libuv reactor running on the current thread" if thread.nil?
63
+
64
+ klass = klass_from_handler(InboundConnection, handler, *args)
65
+ MTUV::TcpServer.new thread, server, port, klass, *args
66
+ end
67
+
68
+ def self.attach_server(sock, handler, *args)
69
+ thread = reactor # Get the reactor running on this thread
70
+ raise ThreadError, "There is no Libuv reactor running on the current thread" if thread.nil?
71
+
72
+ klass = klass_from_handler(InboundConnection, handler, *args)
73
+ sd = sock.respond_to?(:fileno) ? sock.fileno : sock
74
+
75
+ MTUV::TcpServer.new thread, sd, sd, klass, *args
76
+ end
77
+
78
+ def self.open_datagram_socket(handler, server = nil, port = nil, *args)
79
+ klass = klass_from_handler(DatagramConnection, handler, *args)
80
+
81
+ c = klass.new server, port
82
+ c.post_init *args
83
+ c
84
+ end
85
+ end
86
+
87
+ module MTLibuv
88
+ class Reactor
89
+ def scheduler
90
+ @scheduler ||= ::MTUV::Scheduler.new(@reactor)
91
+ @scheduler
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,38 @@
1
+ require File.expand_path("../lib/mt-uv-rays/version", __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "mt-uv-rays"
5
+ gem.version = MTUV::VERSION
6
+ gem.license = 'MIT'
7
+ gem.authors = ["Giallombardo Nathan"]
8
+ gem.email = ["nathan.giallombardo@mapotempo.com"]
9
+ gem.homepage = "https://github.com/mapotempo/mt-uv-rays"
10
+ gem.summary = "Abstractions for working with Libuv"
11
+ gem.description = "Opinionated abstractions for Libuv"
12
+
13
+ gem.required_ruby_version = '>= 2.0.0'
14
+ gem.require_paths = ["lib"]
15
+
16
+ gem.add_runtime_dependency 'mt-libuv', '~> 4.1', '>= 4.1.02' # Evented IO
17
+ gem.add_runtime_dependency 'bisect', '~> 0.1' # Sorted insertion
18
+ gem.add_runtime_dependency 'tzinfo', '~> 1.2' # Ruby timezones info
19
+ gem.add_runtime_dependency 'cookiejar', '~> 0.3' # HTTP cookies
20
+ gem.add_runtime_dependency 'ipaddress', '~> 0.8' # IP address validation
21
+ gem.add_runtime_dependency 'parse-cron', '~> 0.1' # CRON calculations
22
+ gem.add_runtime_dependency 'addressable', '~> 2.4' # URI parser
23
+ gem.add_runtime_dependency 'http-parser', '~> 1.2' # HTTP tokeniser
24
+ gem.add_runtime_dependency 'activesupport', '>= 4', '< 6'
25
+
26
+ # HTTP authentication helpers
27
+ gem.add_runtime_dependency 'rubyntlm', '~> 0.6'
28
+ gem.add_runtime_dependency 'net-http-digest_auth', '~> 1.4'
29
+
30
+ gem.add_development_dependency 'rspec', '~> 3.5'
31
+ gem.add_development_dependency 'rake', '~> 11.2'
32
+ gem.add_development_dependency 'yard', '~> 0.9'
33
+ gem.add_development_dependency 'httpi', '~> 2.4.2'
34
+
35
+ gem.files = Dir["{lib}/**/*"] + %w(Rakefile mt-uv-rays.gemspec README.md LICENSE)
36
+ gem.test_files = Dir["spec/**/*"]
37
+ gem.extra_rdoc_files = ["README.md"]
38
+ end
@@ -0,0 +1,129 @@
1
+ require 'mt-uv-rays'
2
+
3
+ describe MTUV::AbstractTokenizer do
4
+ before :each do
5
+ @buffer = MTUV::AbstractTokenizer.new({
6
+ indicator: "Start",
7
+ callback: lambda { |data|
8
+ return 4 if data.length > 3
9
+ return false
10
+ },
11
+ size_limit: 10,
12
+ encoding: "ASCII-8BIT"
13
+ })
14
+ end
15
+
16
+ it "should not return anything when a complete message is not available" do
17
+ msg1 = "test"
18
+
19
+ result = @buffer.extract(msg1)
20
+ expect(result).to eq([])
21
+ end
22
+
23
+ it "should not return anything when an empty message is present" do
24
+ msg1 = "test"
25
+
26
+ result = @buffer.extract(msg1)
27
+ expect(result).to eq([])
28
+ end
29
+
30
+ it "should tokenize messages where the data is a complete message" do
31
+ msg1 = "Start1234"
32
+
33
+ result = @buffer.extract(msg1)
34
+ expect(result).to eq(['1234'])
35
+ end
36
+
37
+ it "should return multiple complete messages" do
38
+ msg1 = "Start1234Start123456"
39
+
40
+ result = @buffer.extract(msg1)
41
+ expect(result).to eq(['1234', '1234'])
42
+ expect(@buffer.flush).to eq('56') # as we've indicated a message length of 4
43
+ end
44
+
45
+ it "should not return junk data" do
46
+ msg1 = "Start123456Start123456"
47
+
48
+ result = @buffer.extract(msg1)
49
+ expect(result).to eq(['1234', '1234'])
50
+ expect(@buffer.flush).to eq('56') # as we've indicated a message length of 4
51
+ end
52
+
53
+ it "should tokenize messages where the indicator is split" do
54
+ msg1 = "123Star"
55
+ msg2 = "twhoaStart1234"
56
+
57
+ result = @buffer.extract(msg1)
58
+ expect(result).to eq([])
59
+ result = @buffer.extract(msg2)
60
+ expect(result).to eq(['whoa', '1234'])
61
+
62
+ msg1 = "123Star"
63
+ msg2 = "twhoaSt"
64
+ msg3 = "art1234"
65
+
66
+ result = @buffer.extract(msg1)
67
+ expect(result).to eq([])
68
+ result = @buffer.extract(msg2)
69
+ expect(result).to eq(['whoa'])
70
+ result = @buffer.extract(msg3)
71
+ expect(result).to eq(['1234'])
72
+ end
73
+
74
+ it "should empty the buffer if the limit is exceeded" do
75
+ result = @buffer.extract('1234567890G')
76
+ expect(result).to eq([])
77
+
78
+ # We keep enough to match a possible partial indicator
79
+ expect(@buffer.flush).to eq('890G')
80
+ end
81
+
82
+ it "should work with regular expressions" do
83
+ @buffer = MTUV::AbstractTokenizer.new({
84
+ indicator: /Start/i,
85
+ callback: lambda { |data|
86
+ return 4 if data.length > 3
87
+ return false
88
+ }
89
+ })
90
+
91
+ result = @buffer.extract('1234567starta')
92
+ expect(result).to eq([])
93
+ result = @buffer.extract('bcd')
94
+ expect(result).to eq(['abcd'])
95
+ end
96
+
97
+ it "should work where the indicator is part of the message" do
98
+ # i.e. We are looking for \x02\x06\xLEN\x00\x00\x02\x06\x00\xEND
99
+ # Where the indicator may appear as part of the message body
100
+
101
+ msg1 = "StartStartStart123456"
102
+
103
+ result = @buffer.extract(msg1)
104
+ expect(result).to eq(['Star', '1234'])
105
+ expect(@buffer.flush).to eq('56') # as we've indicated a message length of 4
106
+ end
107
+
108
+ it "should work with messages that don't have an indicator" do
109
+ # i.e. there is a header that defines message length
110
+ msg1 = "\x0612345\x081234567\x031"
111
+
112
+ @buffer = MTUV::AbstractTokenizer.new({
113
+ callback: lambda { |data|
114
+ len = data.bytes[0]
115
+ if data.length >= len
116
+ len
117
+ else
118
+ false
119
+ end
120
+ },
121
+ size_limit: 10,
122
+ encoding: "ASCII-8BIT"
123
+ })
124
+
125
+ result = @buffer.extract(msg1)
126
+ expect(result).to eq(["\x0612345", "\x081234567"])
127
+ expect(@buffer.flush).to eq("\x031") # as we've indicated a message length of 3
128
+ end
129
+ end