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 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
@@ -208,7 +208,9 @@ module EventState
208
208
  #
209
209
  # @return [nil]
210
210
  #
211
- def on_send *message_names, next_state_name
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 *message_names, next_state_name
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
- nil
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
 
@@ -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) do
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
  #
@@ -2,9 +2,9 @@ module EventState
2
2
  # Major version number.
3
3
  VERSION_MAJOR = 0
4
4
  # Minor version number.
5
- VERSION_MINOR = 0
5
+ VERSION_MINOR = 1
6
6
  # Patch number.
7
- VERSION_PATCH = 1
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: [server_log],
41
- client_args: [%w(foo bar baz), []]).recorder
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: [%w(foo bar baz), []]).recorder
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: [0.5],
69
- client_args: [%w(foo bar baz), []]).recorder
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
- assert_equal <<DOT, dot.string
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
- [:foo, [:recv, :hello], :bar],
117
- [:bar, [:recv, :good_bye], :foo]], trans
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 test_dsl_no_nested_states
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: [server_log, 1],
256
- client_args: [client_log, [2,2]])
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: [server_log, 1],
271
- client_args: [client_log, [0.5,2]])
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.1
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: 2011-11-30 00:00:00.000000000Z
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: &80005190 !ruby/object:Gem::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: *80005190
24
+ version_requirements: *85328870
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: gemma
27
- requirement: &80004940 !ruby/object:Gem::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: *80004940
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.1 Documentation
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: 304276877
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: 304276877
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: