uv-rays 0.0.1
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/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
|