uv-rays 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +22 -0
- data/lib/uv-rays/abstract_tokenizer.rb +100 -0
- data/lib/uv-rays/buffered_tokenizer.rb +97 -0
- data/lib/uv-rays/connection.rb +175 -0
- data/lib/uv-rays/http/encoding.rb +119 -0
- data/lib/uv-rays/http/request.rb +150 -0
- data/lib/uv-rays/http/response.rb +119 -0
- data/lib/uv-rays/http_endpoint.rb +253 -0
- data/lib/uv-rays/scheduler/cron.rb +386 -0
- data/lib/uv-rays/scheduler/time.rb +275 -0
- data/lib/uv-rays/scheduler.rb +319 -0
- data/lib/uv-rays/tcp_server.rb +48 -0
- data/lib/uv-rays/version.rb +3 -0
- data/lib/uv-rays.rb +96 -0
- data/spec/abstract_tokenizer_spec.rb +87 -0
- data/spec/buffered_tokenizer_spec.rb +253 -0
- data/spec/connection_spec.rb +137 -0
- data/spec/http_endpoint_spec.rb +279 -0
- data/spec/scheduler_cron_spec.rb +429 -0
- data/spec/scheduler_spec.rb +90 -0
- data/spec/scheduler_time_spec.rb +132 -0
- data/uv-rays.gemspec +31 -0
- metadata +217 -0
@@ -0,0 +1,319 @@
|
|
1
|
+
|
2
|
+
module UvRays
|
3
|
+
|
4
|
+
class ScheduledEvent < ::Libuv::Q::DeferredPromise
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
attr_reader :created
|
8
|
+
attr_reader :last_scheduled
|
9
|
+
attr_reader :next_scheduled
|
10
|
+
attr_reader :trigger_count
|
11
|
+
|
12
|
+
def initialize(scheduler)
|
13
|
+
# Create a dummy deferrable
|
14
|
+
loop = scheduler.loop
|
15
|
+
defer = loop.defer
|
16
|
+
|
17
|
+
# Setup common event variables
|
18
|
+
@scheduler = scheduler
|
19
|
+
@created = loop.now
|
20
|
+
@last_scheduled = @created
|
21
|
+
@trigger_count = 0
|
22
|
+
|
23
|
+
# init the promise
|
24
|
+
super(loop, defer)
|
25
|
+
end
|
26
|
+
|
27
|
+
# required for comparable
|
28
|
+
def <=>(anOther)
|
29
|
+
@next_scheduled <=> anOther.next_scheduled
|
30
|
+
end
|
31
|
+
|
32
|
+
# reject the promise
|
33
|
+
def cancel
|
34
|
+
@defer.reject(:cancelled)
|
35
|
+
end
|
36
|
+
|
37
|
+
# notify listeners of the event
|
38
|
+
def trigger
|
39
|
+
@trigger_count += 1
|
40
|
+
@defer.notify(@loop.now, self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class OneShot < ScheduledEvent
|
45
|
+
def initialize(scheduler, at)
|
46
|
+
super(scheduler)
|
47
|
+
|
48
|
+
@next_scheduled = at
|
49
|
+
end
|
50
|
+
|
51
|
+
# Updates the scheduled time
|
52
|
+
def update(time)
|
53
|
+
@last_scheduled = @loop.now
|
54
|
+
|
55
|
+
|
56
|
+
parsed_time = parse_in(time, :quiet)
|
57
|
+
if parsed_time.nil?
|
58
|
+
# Parse at will throw an error if time is invalid
|
59
|
+
parsed_time = parse_at(time) - @scheduler.time_diff
|
60
|
+
else
|
61
|
+
parsed_time += @last_scheduled
|
62
|
+
end
|
63
|
+
|
64
|
+
@next_scheduled = parsed_time
|
65
|
+
@scheduler.reschedule(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Runs the event and cancels the schedule
|
69
|
+
def trigger
|
70
|
+
super()
|
71
|
+
@defer.resolve(:triggered)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Repeat < ScheduledEvent
|
76
|
+
def initialize(scheduler, every)
|
77
|
+
super(scheduler)
|
78
|
+
|
79
|
+
@every = every
|
80
|
+
next_time
|
81
|
+
end
|
82
|
+
|
83
|
+
# Update the time period of the repeating event
|
84
|
+
#
|
85
|
+
# @param schedule [String] a standard CRON job line or a human readable string representing a time period.
|
86
|
+
def update(every)
|
87
|
+
time = parse_in(every, :quiet) || parse_cron(every, :quiet)
|
88
|
+
raise ArgumentError.new("couldn't parse \"#{o}\"") if time.nil?
|
89
|
+
|
90
|
+
@every = time
|
91
|
+
reschedule
|
92
|
+
end
|
93
|
+
|
94
|
+
# removes the event from the schedule
|
95
|
+
def pause
|
96
|
+
@paused = true
|
97
|
+
@scheduler.unschedule(self)
|
98
|
+
end
|
99
|
+
|
100
|
+
# reschedules the event to the next time period
|
101
|
+
# can be used to reset a repeating timer
|
102
|
+
def resume
|
103
|
+
@paused = false
|
104
|
+
reschedule
|
105
|
+
end
|
106
|
+
|
107
|
+
# Runs the event and reschedules
|
108
|
+
def trigger
|
109
|
+
super()
|
110
|
+
@loop.next_tick do
|
111
|
+
# Do this next tick to avoid needless scheduling
|
112
|
+
# if the event is stopped in the callback
|
113
|
+
reschedule
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
|
121
|
+
def next_time
|
122
|
+
@last_scheduled = @loop.now
|
123
|
+
if @every.is_a? Fixnum
|
124
|
+
@next_scheduled = @last_scheduled + @every
|
125
|
+
else
|
126
|
+
# must be a cron
|
127
|
+
@next_scheduled = (@every.next_time.to_f * 1000).to_i - @scheduler.time_diff
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def reschedule
|
132
|
+
if not @paused
|
133
|
+
next_time
|
134
|
+
@scheduler.reschedule(self)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
class Scheduler
|
141
|
+
attr_reader :loop
|
142
|
+
attr_reader :time_diff
|
143
|
+
|
144
|
+
|
145
|
+
def initialize(loop)
|
146
|
+
@loop = loop
|
147
|
+
@schedules = Set.new
|
148
|
+
@scheduled = []
|
149
|
+
@next = nil # Next schedule time
|
150
|
+
@timer = nil # Reference to the timer
|
151
|
+
@timer_callback = method(:on_timer)
|
152
|
+
|
153
|
+
# as the libuv time is taken from an arbitrary point in time we
|
154
|
+
# need to roughly synchronize between it and ruby's Time.now
|
155
|
+
@loop.update_time
|
156
|
+
@time_diff = (Time.now.to_f * 1000).to_i - @loop.now
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
# Create a repeating event that occurs each time period
|
161
|
+
#
|
162
|
+
# @param time [String] a human readable string representing the time period. 3w2d4h1m2s for example.
|
163
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
164
|
+
# @return [::UvRays::Repeat]
|
165
|
+
def every(time, callback = nil, &block)
|
166
|
+
callback ||= block
|
167
|
+
ms = Scheduler.parse_in(time)
|
168
|
+
event = Repeat.new(self, ms)
|
169
|
+
|
170
|
+
if callback.respond_to? :call
|
171
|
+
event.progress callback
|
172
|
+
end
|
173
|
+
schedule(event)
|
174
|
+
event
|
175
|
+
end
|
176
|
+
|
177
|
+
# Create a one off event that occurs after the time period
|
178
|
+
#
|
179
|
+
# @param time [String] a human readable string representing the time period. 3w2d4h1m2s for example.
|
180
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
181
|
+
# @return [::UvRays::OneShot]
|
182
|
+
def in(time, callback = nil, &block)
|
183
|
+
callback ||= block
|
184
|
+
ms = @loop.now + Scheduler.parse_in(time)
|
185
|
+
event = OneShot.new(self, ms)
|
186
|
+
|
187
|
+
if callback.respond_to? :call
|
188
|
+
event.progress callback
|
189
|
+
end
|
190
|
+
schedule(event)
|
191
|
+
event
|
192
|
+
end
|
193
|
+
|
194
|
+
# Create a one off event that occurs at a particular date and time
|
195
|
+
#
|
196
|
+
# @param time [String, Time] a representation of a date and time that can be parsed
|
197
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
198
|
+
# @return [::UvRays::OneShot]
|
199
|
+
def at(time, callback = nil, &block)
|
200
|
+
callback ||= block
|
201
|
+
ms = Scheduler.parse_at(time) - @time_diff
|
202
|
+
event = OneShot.new(self, ms)
|
203
|
+
|
204
|
+
if callback.respond_to? :call
|
205
|
+
event.progress callback
|
206
|
+
end
|
207
|
+
schedule(event)
|
208
|
+
event
|
209
|
+
end
|
210
|
+
|
211
|
+
# Create a repeating event that uses a CRON line to determine the trigger time
|
212
|
+
#
|
213
|
+
# @param schedule [String] a standard CRON job line.
|
214
|
+
# @param callback [Proc] a block or method to execute when the event triggers
|
215
|
+
# @return [::UvRays::Repeat]
|
216
|
+
def cron(schedule, callback = nil, &block)
|
217
|
+
callback ||= block
|
218
|
+
ms = Scheduler.parse_cron(time)
|
219
|
+
event = Repeat.new(self, ms)
|
220
|
+
|
221
|
+
if callback.respond_to? :call
|
222
|
+
event.progress callback
|
223
|
+
end
|
224
|
+
schedule(event)
|
225
|
+
event
|
226
|
+
end
|
227
|
+
|
228
|
+
# Schedules an event for execution
|
229
|
+
#
|
230
|
+
# @param event [ScheduledEvent]
|
231
|
+
def reschedule(event)
|
232
|
+
# Check promise is not resolved
|
233
|
+
return if event.resolved?
|
234
|
+
|
235
|
+
# Remove the event from the scheduled list and ensure it is in the schedules set
|
236
|
+
if @schedules.include?(event)
|
237
|
+
@scheduled.delete(event)
|
238
|
+
else
|
239
|
+
@schedules << event
|
240
|
+
end
|
241
|
+
|
242
|
+
# optimal algorithm for inserting into an already sorted list
|
243
|
+
Bisect.insort(@scheduled, event)
|
244
|
+
|
245
|
+
# Update the timer
|
246
|
+
check_timer
|
247
|
+
end
|
248
|
+
|
249
|
+
# Removes an event from the schedule
|
250
|
+
#
|
251
|
+
# @param event [ScheduledEvent]
|
252
|
+
def unschedule(event)
|
253
|
+
# Only call delete and update the timer when required
|
254
|
+
if @schedules.include?(event)
|
255
|
+
@schedules.delete(event)
|
256
|
+
@scheduled.delete(event)
|
257
|
+
check_timer
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
|
265
|
+
# First time schedule we want to bind to the promise
|
266
|
+
def schedule(event)
|
267
|
+
reschedule(event)
|
268
|
+
|
269
|
+
event.finally do
|
270
|
+
unschedule event
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Ensures the current timer, if any, is still
|
275
|
+
# accurate by checking the head of the schedule
|
276
|
+
def check_timer
|
277
|
+
existing = @next
|
278
|
+
schedule = @scheduled.first
|
279
|
+
@next = schedule.nil? ? nil : schedule.next_scheduled
|
280
|
+
|
281
|
+
if existing != @next
|
282
|
+
# lazy load the timer
|
283
|
+
if @timer.nil?
|
284
|
+
@timer = @loop.timer @timer_callback
|
285
|
+
else
|
286
|
+
@timer.stop
|
287
|
+
end
|
288
|
+
|
289
|
+
if not @next.nil?
|
290
|
+
@timer.start(@next - @loop.now)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Is called when the libuv timer fires
|
296
|
+
def on_timer
|
297
|
+
schedule = @scheduled.shift
|
298
|
+
schedule.trigger
|
299
|
+
|
300
|
+
# execute schedules that are within 30ms of this event
|
301
|
+
# Basic timer coalescing..
|
302
|
+
now = @loop.now + 30
|
303
|
+
while @scheduled.first && @scheduled.first.next_scheduled <= now
|
304
|
+
schedule = @scheduled.shift
|
305
|
+
schedule.trigger
|
306
|
+
end
|
307
|
+
check_timer
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
module Libuv
|
313
|
+
class Loop
|
314
|
+
def scheduler
|
315
|
+
@scheduler ||= UvRays::Scheduler.new(@loop)
|
316
|
+
@scheduler
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
module UvRays
|
3
|
+
class TcpServer < ::Libuv::TCP
|
4
|
+
def initialize(loop, server, port, klass, *args)
|
5
|
+
super(loop)
|
6
|
+
|
7
|
+
@klass = klass
|
8
|
+
@args = args
|
9
|
+
@accept_method = method(:client_accepted)
|
10
|
+
|
11
|
+
if server == port && port.is_a?(Fixnum)
|
12
|
+
# We are opening a socket descriptor
|
13
|
+
open(server)
|
14
|
+
else
|
15
|
+
# Perform basic checks before attempting to bind address
|
16
|
+
server = '127.0.0.1' if server == 'localhost'
|
17
|
+
if IPAddress.valid? server
|
18
|
+
@server = server
|
19
|
+
bind(server, port, method(:new_connection))
|
20
|
+
listen(1024)
|
21
|
+
else
|
22
|
+
raise ArgumentError, "Invalid server address #{server}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
|
31
|
+
def new_connection(server)
|
32
|
+
server.accept @accept_method
|
33
|
+
end
|
34
|
+
|
35
|
+
def client_accepted(client)
|
36
|
+
# prevent buffering
|
37
|
+
client.enable_nodelay
|
38
|
+
|
39
|
+
# create the connection class
|
40
|
+
c = @klass.new(client)
|
41
|
+
c.post_init *@args
|
42
|
+
|
43
|
+
# start read after post init and call connected
|
44
|
+
client.start_read
|
45
|
+
c.on_connect(client)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/uv-rays.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'libuv'
|
2
|
+
|
3
|
+
|
4
|
+
# In-memory event scheduling
|
5
|
+
require 'set' # ruby std lib
|
6
|
+
require 'bisect' # insert into a sorted array
|
7
|
+
require 'tzinfo' # timezone information
|
8
|
+
require 'uv-rays/scheduler/cron'
|
9
|
+
require 'uv-rays/scheduler/time'
|
10
|
+
require 'uv-rays/scheduler'
|
11
|
+
|
12
|
+
# Intelligent stream buffering
|
13
|
+
require 'uv-rays/buffered_tokenizer'
|
14
|
+
require 'uv-rays/abstract_tokenizer'
|
15
|
+
|
16
|
+
# TCP Connections
|
17
|
+
require 'ipaddress' # IP Address parser
|
18
|
+
require 'uv-rays/tcp_server'
|
19
|
+
require 'uv-rays/connection'
|
20
|
+
|
21
|
+
# HTTP related methods
|
22
|
+
require 'cookiejar' # Manages cookies
|
23
|
+
require 'http-parser' # Parses HTTP request / responses
|
24
|
+
require 'addressable/uri' # URI parser
|
25
|
+
require 'uv-rays/http/encoding'
|
26
|
+
require 'uv-rays/http/request'
|
27
|
+
require 'uv-rays/http/response'
|
28
|
+
require 'uv-rays/http_endpoint'
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
module UvRays
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def self.klass_from_handler(klass, handler = nil, *args)
|
36
|
+
klass = if handler and handler.is_a?(Class)
|
37
|
+
raise ArgumentError, "must provide module or subclass of #{klass.name}" unless klass >= handler
|
38
|
+
handler
|
39
|
+
elsif handler
|
40
|
+
begin
|
41
|
+
handler::UR_CONNECTION_CLASS
|
42
|
+
rescue NameError
|
43
|
+
handler::const_set(:UR_CONNECTION_CLASS, Class.new(klass) {include handler})
|
44
|
+
end
|
45
|
+
else
|
46
|
+
klass
|
47
|
+
end
|
48
|
+
|
49
|
+
arity = klass.instance_method(:post_init).arity
|
50
|
+
expected = arity >= 0 ? arity : -(arity + 1)
|
51
|
+
if (arity >= 0 and args.size != expected) or (arity < 0 and args.size < expected)
|
52
|
+
raise ArgumentError, "wrong number of arguments for #{klass}#post_init (#{args.size} for #{expected})"
|
53
|
+
end
|
54
|
+
|
55
|
+
klass
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def self.connect(server, port, handler, *args)
|
60
|
+
klass = klass_from_handler(OutboundConnection, handler, *args)
|
61
|
+
|
62
|
+
c = klass.new server, port
|
63
|
+
c.post_init *args
|
64
|
+
c
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.start_server(server, port, handler, *args)
|
68
|
+
loop = Libuv::Loop.current # Get the current event loop
|
69
|
+
raise ThreadError, "There is no Libuv Loop running on the current thread" if loop.nil?
|
70
|
+
|
71
|
+
klass = klass_from_handler(InboundConnection, handler, *args)
|
72
|
+
UvRays::TcpServer.new loop, server, port, klass, *args
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.attach_server(sock, handler, *args)
|
76
|
+
loop = Libuv::Loop.current # Get the current event loop
|
77
|
+
raise ThreadError, "There is no Libuv Loop running on the current thread" if loop.nil?
|
78
|
+
|
79
|
+
klass = klass_from_handler(InboundConnection, handler, *args)
|
80
|
+
sd = sock.respond_to?(:fileno) ? sock.fileno : sock
|
81
|
+
|
82
|
+
UvRays::TcpServer.new loop, sd, sd, klass, *args
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.open_datagram_socket(handler, server = nil, port = nil, *args)
|
86
|
+
klass = klass_from_handler(DatagramConnection, handler, *args)
|
87
|
+
|
88
|
+
c = klass.new server, port
|
89
|
+
c.post_init *args
|
90
|
+
c
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Alias for {UvRays}
|
95
|
+
UV = UvRays
|
96
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'uv-rays'
|
2
|
+
|
3
|
+
describe UvRays::AbstractTokenizer do
|
4
|
+
before :each do
|
5
|
+
@buffer = UvRays::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
|
+
})
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should not return anything when a complete message is not available" do
|
16
|
+
msg1 = "test"
|
17
|
+
|
18
|
+
result = @buffer.extract(msg1)
|
19
|
+
result.should == []
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not return anything when an empty message is present" do
|
23
|
+
msg1 = "test"
|
24
|
+
|
25
|
+
result = @buffer.extract(msg1)
|
26
|
+
result.should == []
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should tokenize messages where the data is a complete message" do
|
30
|
+
msg1 = "Start1234"
|
31
|
+
|
32
|
+
result = @buffer.extract(msg1)
|
33
|
+
result.should == ['1234']
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should return multiple complete messages" do
|
37
|
+
msg1 = "Start1234Start123456"
|
38
|
+
|
39
|
+
result = @buffer.extract(msg1)
|
40
|
+
result.should == ['1234', '1234']
|
41
|
+
@buffer.flush.should == '56' # as we've indicated a message length of 4
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should tokenize messages where the indicator is split" do
|
45
|
+
msg1 = "123Star"
|
46
|
+
msg2 = "twhoaStart1234"
|
47
|
+
|
48
|
+
result = @buffer.extract(msg1)
|
49
|
+
result.should == []
|
50
|
+
result = @buffer.extract(msg2)
|
51
|
+
result.should == ['whoa', '1234']
|
52
|
+
|
53
|
+
msg1 = "123Star"
|
54
|
+
msg2 = "twhoaSt"
|
55
|
+
msg3 = "art1234"
|
56
|
+
|
57
|
+
result = @buffer.extract(msg1)
|
58
|
+
result.should == []
|
59
|
+
result = @buffer.extract(msg2)
|
60
|
+
result.should == ['whoa']
|
61
|
+
result = @buffer.extract(msg3)
|
62
|
+
result.should == ['1234']
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should empty the buffer if the limit is exceeded" do
|
66
|
+
result = @buffer.extract('1234567890G')
|
67
|
+
result.should == []
|
68
|
+
|
69
|
+
# We keep enough to match a possible partial indicator
|
70
|
+
@buffer.flush.should == '890G'
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should work with regular expressions" do
|
74
|
+
@buffer = UvRays::AbstractTokenizer.new({
|
75
|
+
indicator: /Start/i,
|
76
|
+
callback: lambda { |data|
|
77
|
+
return 4 if data.length > 3
|
78
|
+
return false
|
79
|
+
}
|
80
|
+
})
|
81
|
+
|
82
|
+
result = @buffer.extract('1234567starta')
|
83
|
+
result.should == []
|
84
|
+
result = @buffer.extract('bcd')
|
85
|
+
result.should == ['abcd']
|
86
|
+
end
|
87
|
+
end
|