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 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: