state_flow 0.2.0 → 0.2.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.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.1
@@ -20,6 +20,7 @@ module StateFlow
20
20
  autoload :ActionEvent, 'state_flow/action_event'
21
21
  autoload :ExceptionHandler, 'state_flow/exception_handler'
22
22
  autoload :ExceptionHandlerClient, 'state_flow/exception_handler_client'
23
+ autoload :RecoverableException, 'state_flow/recoverable_exception'
23
24
 
24
25
  autoload :Log, 'state_flow/log'
25
26
 
@@ -27,6 +28,7 @@ module StateFlow
27
28
 
28
29
  def self.included(mod)
29
30
  mod.module_eval do
31
+ include(Base::ClientInstanceMethods)
30
32
  extend(Base::ClientClassMethods)
31
33
  end
32
34
  end
@@ -19,10 +19,9 @@ module StateFlow
19
19
  exception_handling(context) do
20
20
  result = context.record_send(method_name, *method_args)
21
21
  event = event_for_action_result(result)
22
- if event
23
- event.process(context)
24
- elsif action
25
- action.process(context)
22
+ event.process(context) if event
23
+ unless event
24
+ action.process(context) if action
26
25
  end
27
26
  update_to_destination(context)
28
27
  end
@@ -4,6 +4,13 @@ module StateFlow
4
4
 
5
5
  class Base
6
6
 
7
+ module ClientInstanceMethods
8
+ def state_flow_contexts
9
+ @state_flow_contexts ||= {}
10
+ end
11
+ end
12
+
13
+
7
14
  module ClientClassMethods
8
15
  def state_flow_for(selectable_attr)
9
16
  return nil unless @state_flows
@@ -20,8 +27,9 @@ module StateFlow
20
27
  @state_flows << flow
21
28
  module_eval(<<-EOS, __FILE__, __LINE__)
22
29
  def process_#{selectable_attr}(context_or_options = nil)
23
- flow = self.class.state_flow_for(:#{selectable_attr})
30
+ flow = self.class.state_flow_for(:#{flow.attr_name})
24
31
  context = flow.prepare_context(self, context_or_options)
32
+ state_flow_contexts[:#{selectable_attr}] = context
25
33
  context.process(flow)
26
34
  end
27
35
  EOS
@@ -97,7 +105,9 @@ module StateFlow
97
105
 
98
106
  def process(context)
99
107
  current_key = context.current_attr_key
108
+ raise ArgumentError, "current_key not found for: #{context.inspect}" unless current_key
100
109
  state = concrete_states[current_key]
110
+ raise ArgumentError, "status not found for: #{current_key.inspect}" unless state
101
111
  state.process(context)
102
112
  context
103
113
  end
@@ -13,7 +13,7 @@ module StateFlow
13
13
  end
14
14
 
15
15
  def process(flow_or_named_event = flow)
16
- flow.klass.transaction do
16
+ transaction_with_recovering do
17
17
  flow_or_named_event.process(self)
18
18
  save_record_if_need
19
19
  end
@@ -21,8 +21,10 @@ module StateFlow
21
21
  last_current_key = current_attr_key
22
22
  while true
23
23
  @mark_proceeding = false
24
- flow.process(self)
25
- save_record_if_need if @mark_proceeding
24
+ transaction_with_recovering do
25
+ flow.process(self)
26
+ save_record_if_need if @mark_proceeding
27
+ end
26
28
  break unless @mark_proceeding
27
29
  break if last_current_key == current_attr_key
28
30
  last_current_key = current_attr_key
@@ -30,12 +32,39 @@ module StateFlow
30
32
  end
31
33
  self
32
34
  end
35
+
36
+ private
37
+ def transaction_with_recovering(&block)
38
+ begin
39
+ flow.klass.transaction(&block)
40
+ rescue ::StateFlow::RecoverableException => exception
41
+ handler = exception.recover_handler
42
+ log_with_stack_trace(:info, "RECOVERING START", :backtrace => true, :exception => exception, :recover_handler => handler)
43
+ start_force_recovering
44
+ begin
45
+ transaction_with_recovering do
46
+ handler.process(self)
47
+ save_record_if_need
48
+ end
49
+ log_with_stack_trace(:info, "RECOVERED", :backtrace => false, :exception => exception, :recover_handler => handler)
50
+ rescue Exception => exception
51
+ exceptions << exception
52
+ trace(exception)
53
+ log_with_stack_trace(:warn, "RECOVERING FAILURE!!!!!", :backtrace => true, :exception => exception, :recover_handler => handler)
54
+ raise
55
+ end
56
+ end
57
+ end
58
+
33
59
 
60
+ public
61
+
34
62
  def mark_proceeding
35
63
  @mark_proceeding = true
36
64
  end
37
65
 
38
66
  def trace(object)
67
+ ActiveRecord::Base.logger.debug(object.inspect)
39
68
  stack_trace << object
40
69
  end
41
70
 
@@ -43,22 +72,100 @@ module StateFlow
43
72
  @stack_trace ||= []
44
73
  end
45
74
 
75
+ def stack_trace_inspects
76
+ stack_trace.map{|st| st.inspect}
77
+ end
78
+
79
+ def start_force_recovering ; @force_recovering = true ; end
80
+ def finish_force_recovering; @force_recovering = false; end
81
+ def force_recovering?; @force_recovering; end
82
+
83
+ def with_force_recovering(target = nil, *recovering_method_and_args)
84
+ return yield unless force_recovering?
85
+ begin
86
+ return yield
87
+ rescue Exception => exception
88
+ log_with_stack_trace(:warn, "IGNORE EXCEPTION IN RECOVERING",
89
+ :exception => exception, :backtrace => true)
90
+ if recovering_method_and_args.empty?
91
+ return
92
+ else
93
+ return target.send(*recovering_method_and_args)
94
+ end
95
+ end
96
+ end
97
+
98
+
99
+ def log_with_stack_trace(level, *messages)
100
+ options = messages.extract_options!
101
+ exception = options.delete(:exception)
102
+ backtrace = options.delete(:backtrace)
103
+ result = "#{messages.shift}"
104
+ result << "\n exception: #{exception.inspect}" if exception
105
+ messages.each do |msg|
106
+ result << "\n #{msg}"
107
+ end
108
+ options.each do |key, value|
109
+ result << "\n #{key.inspect}: #{value.inspect}"
110
+ end
111
+ if exception && backtrace
112
+ result << "\n exception.backtrace:\n " << exception.backtrace.join("\n ")
113
+ end
114
+ result << "\n context.stack_trace:\n " << stack_trace_inspects.reverse.join("\n ")
115
+ ActiveRecord::Base.logger.send(level, result)
116
+ end
117
+
118
+ class Manipulation
119
+ attr_reader :target, :method, :args, :block
120
+ attr_reader :trace, :result
121
+ attr_reader :context
122
+ def initialize(context, target, method, *args, &block)
123
+ @context = context
124
+ @target, @method, @args, @block = target, method, args, block
125
+ @trace = caller(3)
126
+ end
127
+
128
+ def execute
129
+ begin
130
+ return @result = @target.send(@method, *@args, &@block)
131
+ rescue Exception
132
+ raise unless @context.force_recovering?
133
+ end
134
+ end
135
+
136
+ def inspect
137
+ args_part = @args.inspect.gsub(/^\[|\]$/, '')
138
+ if !args_part.nil? && !args_part.empty?
139
+ args_part = "(#{args_part})"
140
+ end
141
+ "#{@target.class.name}##{@method}#{args_part}#{@block ? " with block" : nil}"
142
+ end
143
+ end
46
144
 
47
145
  def save_record_if_need
48
146
  return unless options[:save]
49
- record.send(options[:save])
147
+ manipulation = Manipulation.new(self, record, options[:save])
148
+ trace(manipulation)
149
+ manipulation.execute
50
150
  end
51
151
 
52
152
  def record_send(*args, &block)
53
- record.send(*args, &block)
153
+ manipulation = Manipulation.new(self, record, *args, &block)
154
+ trace(manipulation)
155
+ manipulation.execute
54
156
  end
55
157
 
56
158
  def record_reload_if_possible
57
- record.reload unless record.new_record?
159
+ return if record.new_record?
160
+ manipulation = Manipulation.new(self, record, :reload)
161
+ trace(manipulation)
162
+ manipulation.execute
58
163
  end
59
164
 
60
165
  def transaction_rollback
61
- record.class.connection.rollback_db_transaction
166
+ manipulation = Manipulation.new(self, record.class.connection, :rollback_db_transaction)
167
+ trace(manipulation)
168
+ manipulation.execute
62
169
  end
63
170
 
64
171
  def exceptions
@@ -30,6 +30,11 @@ module StateFlow
30
30
  context.record_send("#{flow.attr_key_name}=", destination)
31
31
  end
32
32
 
33
+ def retry_in_recovering(context)
34
+ update_to_destination(context)
35
+ end
36
+
37
+
33
38
  class << self
34
39
  def uninspected_var(*vars)
35
40
  @@uninspected_var_hash ||= {}
@@ -41,8 +46,8 @@ module StateFlow
41
46
  @uninspected_vars ||= @@uninspected_var_hash.nil? ? %w(@flow @origin) :
42
47
  self.ancestors.map{|klass| @@uninspected_var_hash[klass] || []}.flatten
43
48
  end
44
- end
45
- self.uninspected_var :flow, :origin, :events, :guards, :action
49
+ end
50
+ self.uninspected_var :flow, :origin, :events, :guards, :action
46
51
 
47
52
  def inspect
48
53
  vars = (instance_variables - self.class.uninspected_vars).map do |name|
@@ -52,8 +57,6 @@ module StateFlow
52
57
  vars.unshift("%s:%#x" % [self.class.name, self.object_id])
53
58
  "#<#{vars.join(' ')}>"
54
59
  end
55
-
56
-
57
60
 
58
61
  end
59
62
 
@@ -65,23 +65,6 @@ module StateFlow
65
65
  def event_for_action_result(result)
66
66
  events.detect{|ev| ev.match?(result)}
67
67
  end
68
-
69
- def exception_handling(context)
70
- begin
71
- yield
72
- rescue Exception => exception
73
- context.exceptions << exception
74
- context.trace(exception)
75
- handlers = events.select{|ev| ev.is_a?(ExceptionHandler)}
76
- handlers.each do |handler|
77
- next unless handler.match?(exception)
78
- handler.process(context)
79
- break
80
- end
81
- raise exception unless context.recovered?(exception)
82
- end
83
- end
84
-
85
68
  end
86
69
 
87
70
 
@@ -4,7 +4,7 @@ module StateFlow
4
4
 
5
5
  class ExceptionHandler < Event
6
6
  attr_reader :exceptions
7
- attr_reader :recovering, :rolling_back, :logging_error
7
+ attr_reader :recovering, :rolling_back, :logging_error, :raise_error_in_handling
8
8
 
9
9
  def initialize(origin, *exceptions, &block)
10
10
  options = exceptions.extract_options!
@@ -13,6 +13,8 @@ module StateFlow
13
13
  @recovering = options[:recovering] || false
14
14
  @rolling_back = options[:rolling_back] || options[:rollback] || false
15
15
  @logging_error = options[:logging]
16
+ # 例外をハンドリングしている最中にエラーが出た場合にraiseするかどうか。デフォルトnil
17
+ @raise_error_in_handling = options[:raise_error_in_handling]
16
18
  end
17
19
 
18
20
  def match?(exception)
@@ -22,8 +24,8 @@ module StateFlow
22
24
 
23
25
  def process(context)
24
26
  context.recovered_exceptions << context.exceptions.last if recovering
25
- context.transaction_rollback if rolling_back
26
- context.record_reload_if_possible
27
+ context.record_reload_if_possible # rollbackよりもreloadが先じゃないとネストしたtransactionでおかしい場合がある?
28
+ # context.transaction_rollback if rolling_back
27
29
  super
28
30
  end
29
31
 
@@ -3,19 +3,34 @@ require 'state_flow'
3
3
  module StateFlow
4
4
 
5
5
  module ExceptionHandlerClient
6
- def exception_handling(context)
6
+ # 例外ハンドラの配列を返します。
7
+ # StateFlow::Stateはこれを上書きして親のハンドラも含めて返します。
8
+ def exception_handlers
9
+ events.select{|ev| ev.is_a?(ExceptionHandler)}
10
+ end
11
+
12
+ def exception_handling(context, &block)
13
+ if context.force_recovering?
14
+ return context.with_force_recovering(self, :retry_in_recovering, context, &block)
15
+ end
16
+
17
+ handlers = exception_handlers
18
+ return yield if handlers.empty?
19
+ ActiveRecord::Base.logger.debug("---- exception_handling BEGIN by #{self.inspect}")
7
20
  begin
8
- yield
21
+ return yield
22
+ ActiveRecord::Base.logger.debug("---- exception_handling END by #{self.inspect}")
9
23
  rescue Exception => exception
10
24
  context.exceptions << exception
11
25
  context.trace(exception)
12
- handlers = events.select{|ev| ev.is_a?(ExceptionHandler)}
13
- handlers.each do |handler|
14
- next unless handler.match?(exception)
15
- handler.process(context)
16
- break
26
+ if recover_handler = handlers.detect{|handler| handler.match?(exception)}
27
+ raise ::StateFlow::RecoverableException.new(recover_handler, exception)
28
+ else
29
+ context.log_with_stack_trace(:error, "NOT RECOVERED",
30
+ :exception => exception, :backtrace => true,
31
+ :exception_handlers => handlers, :recover_handler => recover_handler)
32
+ raise exception
17
33
  end
18
- raise exception unless context.recovered?(exception)
19
34
  end
20
35
  end
21
36
 
@@ -10,10 +10,9 @@ module StateFlow
10
10
  end
11
11
 
12
12
  def process(context)
13
- block = state.ancestors_exception_handled_proc(context) do
13
+ state.exception_handling(context) do
14
14
  super
15
15
  end
16
- block.call
17
16
  end
18
17
 
19
18
  class << self
@@ -38,6 +37,7 @@ module StateFlow
38
37
  events.each do |event|
39
38
  if event.state.include?(self.send(flow.attr_key_name))
40
39
  context = flow.prepare_context(self, args.first)
40
+ self.state_flow_contexts[flow.attr_name] = context
41
41
  result = context.process(event)
42
42
  break
43
43
  end
@@ -0,0 +1,12 @@
1
+ module StateFlow
2
+ class RecoverableException < Exception
3
+ attr_reader :recover_handler, :original
4
+ def initialize(recover_handler, original = nil)
5
+ @recover_handler = recover_handler
6
+ @original = original
7
+ super("#{original ? original.inspect + ' ' : nil}RECOVERABLE by " << recover_handler.inspect)
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -59,17 +59,18 @@ module StateFlow
59
59
  public
60
60
  def process(context)
61
61
  context.trace(self)
62
- block = ancestors_exception_handled_proc(context) do
62
+ exception_handling(context) do
63
63
  guard = guard_for(context)
64
64
  return guard.process(context) if guard
65
65
  return action.process(context) if action
66
66
  end
67
- block.call
68
67
  end
69
68
 
70
- def ancestors_exception_handled_proc(context, &block)
71
- result = Proc.new{ exception_handling(context, &block) }
72
- parent ? parent.ancestors_exception_handled_proc(context, &result) : result
69
+ # override ExceptionHandlerClient#exception_handlers
70
+ def exception_handlers
71
+ result = events.select{|ev| ev.is_a?(ExceptionHandler)}
72
+ result.concat(parent.exception_handlers) if parent
73
+ result
73
74
  end
74
75
 
75
76
  def name_path(separator = '>')
@@ -81,6 +81,44 @@ describe Order do
81
81
  Order.count(:conditions => {:status_cd => Order.status_id_by_key(:internal_error)}).should == 1
82
82
  StateFlow::Log.count.should == 0
83
83
  end
84
+
85
+ it "cancel_request but raised error and another error during recovering" do
86
+ @order.should_receive(:send_mail_cancel_requested).and_raise(IOError)
87
+ @order.should_receive(:send_mail_error).and_raise(IOError)
88
+ context = @order.cancel_request(:keep_process => false)
89
+ @order.status_key.should == :internal_error
90
+ # saveされてます。
91
+ Order.count.should == 1
92
+ Order.count(:conditions => {:status_cd => Order.status_id_by_key(:internal_error)}).should == 1
93
+ StateFlow::Log.count.should == 0
94
+ end
95
+
96
+ it "cancel_request but raised error and another error during recovering #2" do
97
+ @order.should_receive(:send_mail_cancel_requested).and_raise(IOError)
98
+ @order.should_receive(:write_exception_to_log).and_raise(IOError)
99
+ context = @order.cancel_request(:keep_process => false)
100
+ @order.status_key.should == :internal_error
101
+ # saveされてます。
102
+ Order.count.should == 1
103
+ Order.count(:conditions => {:status_cd => Order.status_id_by_key(:internal_error)}).should == 1
104
+ StateFlow::Log.count.should == 0
105
+ end
106
+
107
+ it "can access runtime error message in action method" do
108
+ expected_msg = "実行時に取得しにくいエラー"
109
+ @order.should_receive(:send_mail_cancel_requested).and_raise(IOError.new(expected_msg))
110
+ context = @order.cancel_request(:keep_process => false)
111
+
112
+ # context.stack_trace.should == []
113
+
114
+ @order.status_key.should == :internal_error
115
+ @order.instance_variable_get(:@last_error_message).should == expected_msg
116
+
117
+ # saveされてます。
118
+ Order.count.should == 1
119
+ Order.count(:conditions => {:status_cd => Order.status_id_by_key(:internal_error)}).should == 1
120
+ StateFlow::Log.count.should == 0
121
+ end
84
122
  end
85
123
 
86
124
  describe "credit_card" do
@@ -252,11 +290,41 @@ describe Order do
252
290
  Order.count(:conditions => {:status_cd => Order.status_id_by_key(:settlement_error)}).should == 1
253
291
  end
254
292
 
255
- it "settle failed by IOError" do
256
- @order.should_receive(:settle).and_raise(IOError)
293
+ class TestRuntimeError1 < RuntimeError
294
+ end
295
+
296
+ it "settle failed by RuntimeError" do
297
+ @order.should_receive(:settle).and_raise(TestRuntimeError1)
257
298
  @order.should_receive(:release_stock)
258
299
  @order.should_receive(:delete_point)
259
- @order.process_status_cd
300
+ context = @order.process_status_cd
301
+ # context.stack_trace.should == []
302
+ context.stack_trace[ 0].inspect.should == "Order#status_key"
303
+ context.stack_trace[ 1].should be_a(StateFlow::State)
304
+ context.stack_trace[ 1].name_path.should == "valid>auto_cancelable>online_settling"
305
+ context.stack_trace[ 2].inspect.should == "Order#credit_card?"
306
+ context.stack_trace[ 3].should be_a(StateFlow::Action)
307
+ context.stack_trace[ 3].method_name.should == :settle
308
+ context.stack_trace[ 4].inspect.should == "Order#settle"
309
+ context.stack_trace[ 5].should be_a(TestRuntimeError1)
310
+ context.stack_trace[ 6].inspect.should == "Order#reload"
311
+ # context.stack_trace[ 6].inspect.should == "#{Order.connection.class.name}#rollback_db_transaction"
312
+ context.stack_trace[ 7].should be_a(StateFlow::ExceptionHandler)
313
+ context.stack_trace[ 7].exceptions.should == [Exception]
314
+ context.stack_trace[ 8].should be_a(StateFlow::Action)
315
+ context.stack_trace[ 8].method_name.should == :release_stock
316
+ context.stack_trace[ 9].inspect.should == "Order#release_stock"
317
+ context.stack_trace[10].should be_a(StateFlow::Action)
318
+ context.stack_trace[10].method_name.should == :delete_point
319
+ context.stack_trace[11].inspect.should == "Order#delete_point"
320
+ context.stack_trace[12].inspect.should == "Order#status_key=(:settlement_error)"
321
+ context.stack_trace[13].inspect.should == "Order#save!"
322
+ context.stack_trace[14].inspect.should == "Order#status_key"
323
+ context.stack_trace[15].inspect.should == "Order#status_key"
324
+ context.stack_trace[16].should be_a(StateFlow::State)
325
+ context.stack_trace[16].name_path.should == "error>settlement_error"
326
+ context.stack_trace.length.should == 17
327
+
260
328
  @order.status_key.should == :settlement_error
261
329
  Order.count.should == 1
262
330
  Order.count(:conditions => {:status_cd => Order.status_id_by_key(:settlement_error)}).should == 1
@@ -27,7 +27,7 @@ class Order < ActiveRecord::Base
27
27
  # recoverの順番は重要。Exceptionを先に書くと全ての例外は:internal_errorになってしまいます。
28
28
  recover(:'Net::HTTPHeaderSyntaxError').to(:external_error)
29
29
  recover(:'Net::ProtocolError').to(:external_error)
30
- recover(:'Exception').to(:internal_error)
30
+ recover(:'Exception').action(:write_exception_to_log).action(:send_mail_error).to(:internal_error)
31
31
 
32
32
  group(:auto_cancelable) do # 自動キャンセル可
33
33
 
@@ -126,7 +126,6 @@ class Order < ActiveRecord::Base
126
126
 
127
127
  group(:error) do # 異常系
128
128
  from(:settlement_error) do
129
- action(:send_mail_settlement_error)
130
129
  # to(:settlement_error) # 遷移しない
131
130
  end
132
131
 
@@ -168,4 +167,21 @@ class Order < ActiveRecord::Base
168
167
  def send_mail_deliver_notification; end
169
168
  def send_mail_settlement_error; end
170
169
 
170
+ def send_mail_error; end
171
+
172
+ # recover(:'Exception') で処理した例外を実行時に参照します。
173
+ def write_exception_to_log
174
+ logger.debug "*" * 100
175
+ logger.debug "write_exception_to_log"
176
+
177
+ logger.debug state_flow_contexts.inspect
178
+
179
+ if context = state_flow_contexts[:status_cd]
180
+ logger.debug context.inspect
181
+ logger.debug context.exceptions.inspect
182
+ @last_error_message = context.exceptions.last.to_s
183
+ end
184
+
185
+ end
186
+
171
187
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takeshi Akima
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-04 00:00:00 +09:00
12
+ date: 2009-11-05 00:00:00 +09:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -46,6 +46,7 @@ files:
46
46
  - lib/state_flow/log.rb
47
47
  - lib/state_flow/named_event.rb
48
48
  - lib/state_flow/named_guard.rb
49
+ - lib/state_flow/recoverable_exception.rb
49
50
  - lib/state_flow/state.rb
50
51
  - spec/.gitignore
51
52
  - spec/database.yml