event_state 0.0.1 → 0.1.0
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.
- 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:
|