event_state 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +2 -2
- data/lib/event_state/machine.rb +79 -4
- data/lib/event_state/state.rb +13 -5
- data/lib/event_state/version.rb +2 -2
- data/test/event_state/event_state_test.rb +114 -13
- metadata +10 -9
data/README.rdoc
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
* http://github.com/jdleesmiller/event_state
|
4
4
|
|
5
|
+
{<img src="https://secure.travis-ci.org/jdleesmiller/event_state.png"/>}[http://travis-ci.org/jdleesmiller/event_state]
|
6
|
+
|
5
7
|
== SYNOPSIS
|
6
8
|
|
7
9
|
A small embedded DSL for implementing stateful protocols in EventMachine using
|
@@ -98,8 +100,6 @@ Output:
|
|
98
100
|
|
99
101
|
== INSTALLATION
|
100
102
|
|
101
|
-
Not yet released; will eventually be installable with:
|
102
|
-
|
103
103
|
gem install event_state
|
104
104
|
|
105
105
|
== RELATED PROJECTS
|
data/lib/event_state/machine.rb
CHANGED
@@ -208,7 +208,9 @@ module EventState
|
|
208
208
|
#
|
209
209
|
# @return [nil]
|
210
210
|
#
|
211
|
-
def on_send *
|
211
|
+
def on_send *args
|
212
|
+
next_state_name = args.pop
|
213
|
+
message_names = args
|
212
214
|
raise "on_send must be called from a state block" unless @state
|
213
215
|
message_names.flatten.each do |name|
|
214
216
|
@state.on_sends[name] = next_state_name
|
@@ -241,13 +243,42 @@ module EventState
|
|
241
243
|
#
|
242
244
|
# @return [nil]
|
243
245
|
#
|
244
|
-
def on_recv *
|
246
|
+
def on_recv *args
|
247
|
+
next_state_name = args.pop
|
248
|
+
message_names = args
|
245
249
|
raise "on_recv must be called from a state block" unless @state
|
246
250
|
message_names.flatten.each do |name|
|
247
251
|
@state.on_recvs[name] = next_state_name
|
248
252
|
end
|
249
253
|
end
|
250
254
|
|
255
|
+
#
|
256
|
+
# Set maximum time (in seconds) that the machine should spend in this
|
257
|
+
# state. By default, there is no limit. If you set a timeout without
|
258
|
+
# passing a block, the default action is to call +close_connection+.
|
259
|
+
#
|
260
|
+
# Note that the timeout block will not be executed if unbind is called in
|
261
|
+
# this state. However, if the timeout block calls close_connection, the
|
262
|
+
# state's on_unbind handler will be called.
|
263
|
+
#
|
264
|
+
# Note that the timeout applies to all machines. If you want to set a
|
265
|
+
# timeout 'at run time' that applies to the machine only while it is in a
|
266
|
+
# particular state, see {#add_state_timer}.
|
267
|
+
#
|
268
|
+
# Internally, this uses EventMachine's +add_timer+ method. It is
|
269
|
+
# independent of EventMachine's +comm_inactivity_timeout+.
|
270
|
+
#
|
271
|
+
# @yield [] called once timeout elapses
|
272
|
+
#
|
273
|
+
# @return [nil]
|
274
|
+
#
|
275
|
+
def timeout timeout, &block
|
276
|
+
raise "on_recv must be called from a state block" unless @state
|
277
|
+
@state.timeout = timeout
|
278
|
+
@state.on_timeout = block if block_given?
|
279
|
+
nil
|
280
|
+
end
|
281
|
+
|
251
282
|
#
|
252
283
|
# @return [Hash<Symbol, State>] map from state names (ruby symbols) to
|
253
284
|
# {State}s
|
@@ -338,6 +369,11 @@ module EventState
|
|
338
369
|
end
|
339
370
|
end
|
340
371
|
|
372
|
+
#
|
373
|
+
# @return [State] the machine's current state
|
374
|
+
#
|
375
|
+
attr_reader :state
|
376
|
+
|
341
377
|
#
|
342
378
|
# Called by +EventMachine+ when a new connection has been established. This
|
343
379
|
# calls the +on_enter+ handler for the machine's start state with a +nil+
|
@@ -350,13 +386,16 @@ module EventState
|
|
350
386
|
#
|
351
387
|
def post_init
|
352
388
|
@state = self.class.start_state
|
389
|
+
@state_timer_sigs = []
|
390
|
+
add_state_timer @state.timeout, &@state.on_timeout if @state.timeout
|
353
391
|
@state.call_on_enter self, nil, nil
|
354
392
|
nil
|
355
393
|
end
|
356
394
|
|
357
395
|
#
|
358
396
|
# Called by +EventMachine+ when a connection is closed. This calls the
|
359
|
-
# {on_unbind} handler for the current state
|
397
|
+
# {on_unbind} handler for the current state and then cancels all state
|
398
|
+
# timers.
|
360
399
|
#
|
361
400
|
# @return [nil]
|
362
401
|
#
|
@@ -364,7 +403,7 @@ module EventState
|
|
364
403
|
#puts "#{self.class} UNBIND"
|
365
404
|
handler = @state.on_unbind
|
366
405
|
self.instance_exec(&handler) if handler
|
367
|
-
|
406
|
+
cancel_state_timers
|
368
407
|
end
|
369
408
|
|
370
409
|
#
|
@@ -452,16 +491,52 @@ module EventState
|
|
452
491
|
nil
|
453
492
|
end
|
454
493
|
|
494
|
+
#
|
495
|
+
# Add a timer that will be fired only while the machine is in the current
|
496
|
+
# state; the timer is cancelled when the machine leaves the current state
|
497
|
+
# (either by sending or receiving a message, or when unbind is called).
|
498
|
+
#
|
499
|
+
# Internally, this uses EventMachine's +add_timer+ method.
|
500
|
+
#
|
501
|
+
# @param [Numeric] timeout in seconds
|
502
|
+
#
|
503
|
+
# @yield [] handler called after timeout
|
504
|
+
#
|
505
|
+
# @return [Integer] EventMachine timer signature
|
506
|
+
#
|
507
|
+
def add_state_timer timeout, &handler
|
508
|
+
sig = EM.add_timer(timeout) do
|
509
|
+
self.instance_exec(&handler)
|
510
|
+
end
|
511
|
+
@state_timer_sigs << sig
|
512
|
+
sig
|
513
|
+
end
|
514
|
+
|
455
515
|
private
|
456
516
|
|
457
517
|
#
|
458
518
|
# Update @state and call appropriate on_exit and on_enter handlers.
|
459
519
|
#
|
460
520
|
def transition message_name, message, next_state_name
|
521
|
+
cancel_state_timers
|
461
522
|
@state.call_on_exit self, message_name, message
|
462
523
|
@state = self.class.states[next_state_name]
|
524
|
+
add_state_timer @state.timeout, &@state.on_timeout if @state.timeout
|
463
525
|
@state.call_on_enter self, message_name, message
|
464
526
|
end
|
527
|
+
|
528
|
+
#
|
529
|
+
# Cancel all EM timers added by {#add_state_timer}.
|
530
|
+
#
|
531
|
+
# @return [nil]
|
532
|
+
#
|
533
|
+
def cancel_state_timers
|
534
|
+
@state_timer_sigs.each do |sig|
|
535
|
+
EM.cancel_timer(sig)
|
536
|
+
end
|
537
|
+
@state_timer_sigs.clear
|
538
|
+
nil
|
539
|
+
end
|
465
540
|
end
|
466
541
|
end
|
467
542
|
|
data/lib/event_state/state.rb
CHANGED
@@ -25,16 +25,24 @@ module EventState
|
|
25
25
|
# @attr [Hash<Symbol, Symbol>] on_recvs map from message names to successor
|
26
26
|
# state names
|
27
27
|
#
|
28
|
+
# @attr [Numeric, nil] timeout limit on the number of seconds to spend in this
|
29
|
+
# state; the +on_timeout+ block is called after this elapses
|
30
|
+
#
|
31
|
+
# @attr [Proc] on_timeout called when the timeout elapses; by default, it
|
32
|
+
# calls +close_connection+
|
33
|
+
#
|
28
34
|
State = Struct.new(:name,
|
29
35
|
:on_enters, :default_on_enter,
|
30
36
|
:on_exits, :default_on_exit,
|
31
|
-
:on_unbind, :on_sends, :on_recvs
|
37
|
+
:on_unbind, :on_sends, :on_recvs,
|
38
|
+
:timeout, :on_timeout) do
|
32
39
|
def initialize(*)
|
33
40
|
super
|
34
|
-
self.on_enters
|
35
|
-
self.on_exits
|
36
|
-
self.on_sends
|
37
|
-
self.on_recvs
|
41
|
+
self.on_enters ||= {}
|
42
|
+
self.on_exits ||= {}
|
43
|
+
self.on_sends ||= {}
|
44
|
+
self.on_recvs ||= {}
|
45
|
+
self.on_timeout ||= lambda { close_connection }
|
38
46
|
end
|
39
47
|
|
40
48
|
#
|
data/lib/event_state/version.rb
CHANGED
@@ -2,9 +2,9 @@ module EventState
|
|
2
2
|
# Major version number.
|
3
3
|
VERSION_MAJOR = 0
|
4
4
|
# Minor version number.
|
5
|
-
VERSION_MINOR =
|
5
|
+
VERSION_MINOR = 1
|
6
6
|
# Patch number.
|
7
|
-
VERSION_PATCH =
|
7
|
+
VERSION_PATCH = 0
|
8
8
|
# Version string.
|
9
9
|
VERSION = [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH].join('.')
|
10
10
|
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
#require 'simplecov'
|
2
|
+
#SimpleCov.start
|
3
|
+
|
1
4
|
require 'event_state'
|
2
5
|
require 'test/unit'
|
3
6
|
|
@@ -5,6 +8,7 @@ require 'test/unit'
|
|
5
8
|
require 'event_state/ex_echo'
|
6
9
|
require 'event_state/ex_readme'
|
7
10
|
require 'event_state/ex_secret'
|
11
|
+
require 'event_state/ex_job'
|
8
12
|
|
9
13
|
# give more helpful errors
|
10
14
|
Thread.abort_on_exception = true
|
@@ -15,6 +19,9 @@ class TestEventState < Test::Unit::TestCase
|
|
15
19
|
DEFAULT_HOST = 'localhost'
|
16
20
|
DEFAULT_PORT = 14159
|
17
21
|
|
22
|
+
#
|
23
|
+
# Run server and client in the same EventMachine reactor. Returns the client.
|
24
|
+
#
|
18
25
|
def run_server_and_client server_class, client_class, opts={}, &block
|
19
26
|
host = opts[:host] || DEFAULT_HOST
|
20
27
|
port = opts[:port] || DEFAULT_PORT
|
@@ -34,11 +41,45 @@ class TestEventState < Test::Unit::TestCase
|
|
34
41
|
client
|
35
42
|
end
|
36
43
|
|
44
|
+
#
|
45
|
+
# Spawn the given server in a new process (fork) and yield once it's up and
|
46
|
+
# running.
|
47
|
+
#
|
48
|
+
# This works by spawning a child process and starting an EventMachine reactor
|
49
|
+
# in the child process. You should start a new one in the given block, if you
|
50
|
+
# want to connect a client.
|
51
|
+
#
|
52
|
+
def with_forked_server server_class, server_args=[], opts={}, &block
|
53
|
+
host = opts[:host] || DEFAULT_HOST
|
54
|
+
port = opts[:port] || DEFAULT_PORT
|
55
|
+
|
56
|
+
# use a pipe to signal the parent that the child server has started
|
57
|
+
p_r, p_w = IO.pipe
|
58
|
+
child_pid = fork do
|
59
|
+
p_r.close
|
60
|
+
EventMachine.run do
|
61
|
+
EventMachine.start_server(host, port, server_class, *server_args)
|
62
|
+
p_w.puts
|
63
|
+
p_w.close
|
64
|
+
end
|
65
|
+
end
|
66
|
+
p_w.close
|
67
|
+
p_r.gets # wait for child process to start server
|
68
|
+
p_r.close
|
69
|
+
|
70
|
+
begin
|
71
|
+
yield host, port
|
72
|
+
ensure
|
73
|
+
Process.kill 'TERM', child_pid
|
74
|
+
Process.wait
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
37
78
|
def run_echo_test client_class
|
38
79
|
server_log = []
|
39
80
|
recorder = run_server_and_client(LoggingEchoServer, client_class,
|
40
|
-
server_args
|
41
|
-
client_args
|
81
|
+
:server_args => [server_log],
|
82
|
+
:client_args => [%w(foo bar baz), []]).recorder
|
42
83
|
|
43
84
|
assert_equal [
|
44
85
|
"entering listening state", # on_enter called on the start state
|
@@ -59,14 +100,14 @@ class TestEventState < Test::Unit::TestCase
|
|
59
100
|
def test_echo_basic
|
60
101
|
assert_equal %w(foo bar baz),
|
61
102
|
run_server_and_client(EchoServer, EchoClient,
|
62
|
-
client_args
|
103
|
+
:client_args => [%w(foo bar baz), []]).recorder
|
63
104
|
end
|
64
105
|
|
65
106
|
def test_delayed_echo
|
66
107
|
assert_equal %w(foo bar baz),
|
67
108
|
run_server_and_client(DelayedEchoServer, EchoClient,
|
68
|
-
server_args
|
69
|
-
client_args
|
109
|
+
:server_args => [0.5],
|
110
|
+
:client_args => [%w(foo bar baz), []]).recorder
|
70
111
|
end
|
71
112
|
|
72
113
|
def test_echo_with_object_protocol_client
|
@@ -83,7 +124,9 @@ class TestEventState < Test::Unit::TestCase
|
|
83
124
|
|
84
125
|
def test_print_state_machine_dot
|
85
126
|
dot = EchoClient.print_state_machine_dot(:graph_options => 'rankdir=LR;')
|
86
|
-
|
127
|
+
|
128
|
+
# order isn't determined, but we'll get one of the following two
|
129
|
+
match_1 = dot.string == <<DOT
|
87
130
|
digraph "EventState::EchoClient" {
|
88
131
|
rankdir=LR;
|
89
132
|
speaking [peripheries=2];
|
@@ -91,6 +134,15 @@ digraph "EventState::EchoClient" {
|
|
91
134
|
listening -> speaking [color=blue,label="String"];
|
92
135
|
}
|
93
136
|
DOT
|
137
|
+
match_2 = dot.string == <<DOT
|
138
|
+
digraph "EventState::EchoClient" {
|
139
|
+
rankdir=LR;
|
140
|
+
speaking [peripheries=2];
|
141
|
+
listening -> speaking [color=blue,label="String"];
|
142
|
+
speaking -> listening [color=red,label="String"];
|
143
|
+
}
|
144
|
+
DOT
|
145
|
+
assert match_1 || match_2
|
94
146
|
end
|
95
147
|
|
96
148
|
class TestDSLBasic < EventState::Machine; end
|
@@ -113,13 +165,13 @@ DOT
|
|
113
165
|
end
|
114
166
|
|
115
167
|
assert_equal [
|
116
|
-
[:
|
117
|
-
[:
|
168
|
+
[:bar, [:recv, :good_bye], :foo],
|
169
|
+
[:foo, [:recv, :hello], :bar]], trans.sort_by {|x| x.first.to_s}
|
118
170
|
end
|
119
171
|
|
120
172
|
class TestDSLNoNestedProtocols < EventState::Machine; end
|
121
173
|
|
122
|
-
def
|
174
|
+
def test_dsl_no_nested_protocols
|
123
175
|
#
|
124
176
|
# nested protocol blocks are illegal
|
125
177
|
#
|
@@ -252,8 +304,8 @@ DOT
|
|
252
304
|
server_log = []
|
253
305
|
client_log = []
|
254
306
|
run_server_and_client(TestUnbindServer, TestDelayClient,
|
255
|
-
server_args
|
256
|
-
client_args
|
307
|
+
:server_args => [server_log, 1],
|
308
|
+
:client_args => [client_log, [2,2]])
|
257
309
|
assert_equal [
|
258
310
|
"entered foo",
|
259
311
|
"unbound in foo"], server_log
|
@@ -267,8 +319,8 @@ DOT
|
|
267
319
|
server_log = []
|
268
320
|
client_log = []
|
269
321
|
run_server_and_client(TestUnbindServer, TestDelayClient,
|
270
|
-
server_args
|
271
|
-
client_args
|
322
|
+
:server_args => [server_log, 1],
|
323
|
+
:client_args => [client_log, [0.5,2]])
|
272
324
|
assert_equal [
|
273
325
|
"entered foo",
|
274
326
|
"entered bar",
|
@@ -355,5 +407,54 @@ DOT
|
|
355
407
|
assert_equal Fixnum, error.message_name
|
356
408
|
assert_equal 42, error.data
|
357
409
|
end
|
410
|
+
|
411
|
+
def test_job_server_timeouts
|
412
|
+
client_logs = [[],[],[],[]]
|
413
|
+
with_forked_server JobServer do |host, port|
|
414
|
+
EM.run do
|
415
|
+
EM.add_timer 0.1 do
|
416
|
+
EventMachine.connect(host, port, JobClient, 0.1, 1.0, client_logs[0])
|
417
|
+
end
|
418
|
+
EM.add_timer 0.5 do
|
419
|
+
EventMachine.connect(host, port, JobClient, 0.1, 1.0, client_logs[1])
|
420
|
+
end
|
421
|
+
EM.add_timer 1.5 do
|
422
|
+
EventMachine.connect(host, port, JobClient, 0.1, 2.5, client_logs[2])
|
423
|
+
end
|
424
|
+
EM.add_timer 4.5 do
|
425
|
+
EventMachine.connect(host, port, JobClient, 1.5, 0.5, client_logs[3])
|
426
|
+
end
|
427
|
+
EM.add_timer 7 do
|
428
|
+
EM.stop
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# first client gets its job processed
|
434
|
+
assert_equal [
|
435
|
+
'starting',
|
436
|
+
'entering sending state',
|
437
|
+
'sending job',
|
438
|
+
'closed: work: 1.0'], client_logs[0]
|
439
|
+
|
440
|
+
# second client tries while job is still being processed
|
441
|
+
assert_equal [
|
442
|
+
'starting',
|
443
|
+
'busy'], client_logs[1]
|
444
|
+
|
445
|
+
# third client's job time gets sent but times out
|
446
|
+
assert_equal [
|
447
|
+
'starting',
|
448
|
+
'entering sending state',
|
449
|
+
'sending job',
|
450
|
+
'timed out in waiting state',
|
451
|
+
'unbind in waiting state'], client_logs[2]
|
452
|
+
|
453
|
+
# fourth client waits too long to send the job; server gives up
|
454
|
+
assert_equal [
|
455
|
+
'starting',
|
456
|
+
'entering sending state',
|
457
|
+
'unbind in sending state'], client_logs[3]
|
458
|
+
end
|
358
459
|
end
|
359
460
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: event_state
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-01-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: eventmachine
|
16
|
-
requirement: &
|
16
|
+
requirement: &85328870 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.12.10
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *85328870
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: gemma
|
27
|
-
requirement: &
|
27
|
+
requirement: &85328600 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: 2.0.0
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *85328600
|
36
36
|
description: A small embedded DSL for implementing stateful protocols in EventMachine
|
37
37
|
using finite state machines.
|
38
38
|
email:
|
@@ -56,7 +56,7 @@ rdoc_options:
|
|
56
56
|
- --main
|
57
57
|
- README.rdoc
|
58
58
|
- --title
|
59
|
-
- event_state-0.0
|
59
|
+
- event_state-0.1.0 Documentation
|
60
60
|
require_paths:
|
61
61
|
- lib
|
62
62
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -67,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
67
|
version: '0'
|
68
68
|
segments:
|
69
69
|
- 0
|
70
|
-
hash:
|
70
|
+
hash: 112961203
|
71
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
76
|
version: '0'
|
77
77
|
segments:
|
78
78
|
- 0
|
79
|
-
hash:
|
79
|
+
hash: 112961203
|
80
80
|
requirements: []
|
81
81
|
rubyforge_project: event_state
|
82
82
|
rubygems_version: 1.8.10
|
@@ -85,3 +85,4 @@ specification_version: 3
|
|
85
85
|
summary: StateMachines for EventMachines.
|
86
86
|
test_files:
|
87
87
|
- test/event_state/event_state_test.rb
|
88
|
+
has_rdoc:
|