uv-rays 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module UvRays
2
+ VERSION = '0.0.1'
3
+ 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