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.
@@ -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