em-pg-client 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md CHANGED
@@ -1,3 +1,8 @@
1
+ 0.3.4
2
+
3
+ - spec: fix em_client_on_connect and em_connection_pool tests
4
+ - asynchronous wait_for_notify and wait_for_notify_defer with specs
5
+
1
6
  0.3.3
2
7
 
3
8
  - rake: console for debugging
data/README.md CHANGED
@@ -72,15 +72,16 @@ Features
72
72
  processing and transactions.
73
73
  * [Sequel Adapter](https://github.com/fl00r/em-pg-sequel) by Peter Yanovich.
74
74
  * Works on windows (requires ruby 2.0) ([issue #7][Issue 7]).
75
- * __New__ - supports asynchronous query data processing in single row mode
75
+ * Supports asynchronous query data processing in single row mode
76
76
  ([issue #12][Issue 12]). See {file:BENCHMARKS.md BENCHMARKING}.
77
+ * __New__ - asynchronous implementation of wait_for_notify
77
78
 
78
79
  Requirements
79
80
  ------------
80
81
 
81
82
  * ruby >= 1.9.2 (tested: 2.1.0, 2.0.0-p353, 1.9.3-p374, 1.9.2-p320)
82
83
  * https://bitbucket.org/ged/ruby-pg >= 0.17.0
83
- * [PostgreSQL](http://www.postgresql.org/ftp/source/) RDBMS >= 8.3
84
+ * [PostgreSQL](http://www.postgresql.org/ftp/source/) RDBMS >= 8.4
84
85
  * http://rubyeventmachine.com >= 1.0.0
85
86
  * [EM-Synchrony](https://github.com/igrigorik/em-synchrony)
86
87
  (optional - not needed for any of the client functionality,
@@ -96,7 +97,7 @@ Install
96
97
  #### Gemfile
97
98
 
98
99
  ```ruby
99
- gem "em-pg-client", "~> 0.3.3"
100
+ gem "em-pg-client", "~> 0.3.4"
100
101
  ```
101
102
 
102
103
  #### Github
@@ -261,6 +262,52 @@ Additionally the `on_autoreconnect` callback may be set on the connection.
261
262
  It's being invoked after successfull connection restart, just before the
262
263
  pending command is sent again to the server.
263
264
 
265
+ ### Server-sent notifications - async style
266
+
267
+ Not surprisingly, there are two possible ways to wait for notifications,
268
+ one with a deferrable:
269
+
270
+ ```ruby
271
+ pg = PG::EM::Client.new
272
+ EM.run do
273
+ pg.wait_for_notify_defer(7).callback do |notify|
274
+ if notify
275
+ puts "Someone spoke to us on channel: #{notify[:relname]} from #{notify[:be_pid]}"
276
+ else
277
+ puts "Too late, 7 seconds passed"
278
+ end
279
+ end.errback do |ex|
280
+ puts "Connection to deep space lost..."
281
+ end
282
+ pg.query_defer("LISTEN deep_space") do
283
+ pg.query_defer("NOTIFY deep_space") do
284
+ puts "Reaching out... to the other worlds"
285
+ end
286
+ end
287
+ end
288
+ ```
289
+
290
+ and the other, using fibers:
291
+
292
+ ```ruby
293
+ EM.synchrony do
294
+ pg = PG::EM::Client.new
295
+ EM::Synchrony.next_tick do
296
+ pg.query('LISTEN "some channel"')
297
+ pg.query('SELECT pg_notify($1::text,$2::text)', ['some channel', 'with some message'])
298
+ end
299
+ pg.wait_for_notify(10) do |channel, pid, payload|
300
+ puts "I've got notification on #{channel} #{payload}."
301
+ end.tap do |name|
302
+ puts "Whatever, I've been waiting too long already" if name.nil?
303
+ end
304
+ end
305
+ ```
306
+
307
+ As you might have noticed, one does not simply wait for notifications,
308
+ but one can also run some queries on the same connection at the same time,
309
+ if one wishes so.
310
+
264
311
  ### Connection Pool
265
312
 
266
313
  Forever alone? Not anymore! There is a dedicated {PG::EM::ConnectionPool}
@@ -360,16 +407,14 @@ The other reason was to get rid of the ugly em / em-synchrony duality.
360
407
  Bugs/Limitations
361
408
  ----------------
362
409
 
363
- * no async support for: COPY commands (`get_copy_data`, `put_copy_data`),
364
- `wait_for_notify`
410
+ * no async support for COPY commands (`get_copy_data`, `put_copy_data`)
365
411
  * actually no ActiveRecord support (you are welcome to contribute).
366
412
 
367
413
  TODO:
368
414
  -----
369
415
 
370
416
  * more convenient streaming API
371
- * implement EM adapted version of `get_copy_data`, `put_copy_data`,
372
- `wait_for_notify` and `transaction`
417
+ * implement EM adapted version of `get_copy_data`, `put_copy_data`
373
418
  * ORM (ActiveRecord and maybe Datamapper) support as separate projects
374
419
 
375
420
  More Info
@@ -398,13 +443,10 @@ The greetz go to:
398
443
  * Andrew Rudenko [prepor](https://github.com/prepor) for the implicit idea
399
444
  of the re-usable watcher from his [em-pg](https://github.com/prepor/em-pg).
400
445
 
401
- [![Bitdeli Badge][BB img]][Bitdeli Badge]
402
-
403
446
  [Gem Version]: https://rubygems.org/gems/em-pg-client
404
447
  [Dependency Status]: https://gemnasium.com/royaltm/ruby-em-pg-client
405
448
  [Coverage Status]: https://coveralls.io/r/royaltm/ruby-em-pg-client
406
449
  [Build Status]: https://travis-ci.org/royaltm/ruby-em-pg-client
407
- [Bitdeli Badge]: https://bitdeli.com/free
408
450
  [Issue 7]: https://github.com/royaltm/ruby-em-pg-client/issues/7
409
451
  [Issue 12]: https://github.com/royaltm/ruby-em-pg-client/issues/12
410
452
  [GV img]: https://badge.fury.io/rb/em-pg-client.png
@@ -1,5 +1,5 @@
1
1
  module PG
2
2
  module EM
3
- VERSION = '0.3.3'
3
+ VERSION = '0.3.4'
4
4
  end
5
5
  end
@@ -352,7 +352,7 @@ module PG
352
352
  #
353
353
  # @see http://deveiate.org/code/pg/PG/Connection.html#method-c-new PG::Connection.new
354
354
  def self.connect_defer(*args, &blk)
355
- df = PG::EM::FeaturedDeferrable.new(&blk)
355
+ df = FeaturedDeferrable.new(&blk)
356
356
  async_args = parse_async_options(args)
357
357
  conn = df.protect { connect_start(*args) }
358
358
  if conn
@@ -523,70 +523,77 @@ module PG
523
523
 
524
524
  # @!visibility private
525
525
  # Perform auto re-connect. Used internally.
526
- def async_autoreconnect!(deferrable, error, &send_proc)
526
+ def async_autoreconnect!(deferrable, error, send_proc = nil, &on_connection_bad)
527
527
  # reconnect only if connection is bad and flag is set
528
- if self.status == CONNECTION_BAD && async_autoreconnect
529
- # check if transaction was active
530
- was_in_transaction = case @last_transaction_status
531
- when PQTRANS_IDLE, PQTRANS_UNKNOWN
532
- false
533
- else
534
- true
535
- end
536
- # reset asynchronously
537
- reset_df = reset_defer
538
- # just fail on reset failure
539
- reset_df.errback { |ex| deferrable.fail ex }
540
- # reset succeeds
541
- reset_df.callback do
542
- # handle on_autoreconnect
543
- if on_autoreconnect
544
- # wrap in a fiber, so on_autoreconnect code may yield from it
545
- Fiber.new do
546
- # call on_autoreconnect handler and fail if it raises an error
547
- returned_df = begin
548
- on_autoreconnect.call(self, error)
549
- rescue => ex
550
- ex
551
- end
552
- if returned_df.respond_to?(:callback) && returned_df.respond_to?(:errback)
553
- # the handler returned a deferrable
554
- returned_df.callback do
555
- if was_in_transaction || !send_proc
556
- # there was a transaction in progress, fail anyway
557
- deferrable.fail error
558
- else
559
- # try to call failed query command again
560
- deferrable.protect(&send_proc)
528
+ if self.status == CONNECTION_BAD
529
+
530
+ yield if block_given?
531
+
532
+ if async_autoreconnect
533
+ # check if transaction was active
534
+ was_in_transaction = case @last_transaction_status
535
+ when PQTRANS_IDLE, PQTRANS_UNKNOWN
536
+ false
537
+ else
538
+ true
539
+ end
540
+ # reset asynchronously
541
+ reset_df = reset_defer
542
+ # just fail on reset failure
543
+ reset_df.errback { |ex| deferrable.fail ex }
544
+ # reset succeeds
545
+ reset_df.callback do
546
+ # handle on_autoreconnect
547
+ if on_autoreconnect
548
+ # wrap in a fiber, so on_autoreconnect code may yield from it
549
+ Fiber.new do
550
+ # call on_autoreconnect handler and fail if it raises an error
551
+ returned_df = begin
552
+ on_autoreconnect.call(self, error)
553
+ rescue => ex
554
+ ex
555
+ end
556
+ if returned_df.respond_to?(:callback) && returned_df.respond_to?(:errback)
557
+ # the handler returned a deferrable
558
+ returned_df.callback do
559
+ if was_in_transaction || !send_proc
560
+ # fail anyway, there was a transaction in progress or in single result mode
561
+ deferrable.fail error
562
+ else
563
+ # try to call failed query command again
564
+ deferrable.protect(&send_proc)
565
+ end
561
566
  end
567
+ # fail when handler's deferrable fails
568
+ returned_df.errback { |ex| deferrable.fail ex }
569
+ elsif returned_df.is_a?(Exception)
570
+ # tha handler returned an exception object, so fail with it
571
+ deferrable.fail returned_df
572
+ elsif returned_df == false || !send_proc || (was_in_transaction && returned_df != true)
573
+ # tha handler returned false or in single result mode
574
+ # or there was an active transaction and handler didn't return true
575
+ deferrable.fail error
576
+ else
577
+ # try to call failed query command again
578
+ deferrable.protect(&send_proc)
562
579
  end
563
- # fail when handler's deferrable fails
564
- returned_df.errback { |ex| deferrable.fail ex }
565
- elsif returned_df.is_a?(Exception)
566
- # tha handler returned an exception object, so fail with it
567
- deferrable.fail returned_df
568
- elsif returned_df == false || !send_proc || (was_in_transaction && returned_df != true)
569
- # tha handler returned false or raised an exception
570
- # or there was an active transaction and handler didn't return true
571
- deferrable.fail error
572
- else
573
- # try to call failed query command again
574
- deferrable.protect(&send_proc)
575
- end
576
- end.resume
577
- elsif was_in_transaction || !send_proc
578
- # there was a transaction in progress, fail anyway
579
- deferrable.fail error
580
- else
581
- # no on_autoreconnect handler, no transaction, then
582
- # try to call failed query command again
583
- deferrable.protect(&send_proc)
580
+ end.resume
581
+ elsif was_in_transaction || !send_proc
582
+ # there was a transaction in progress or in single result mode;
583
+ # fail anyway
584
+ deferrable.fail error
585
+ else
586
+ # no on_autoreconnect handler, no transaction
587
+ # try to call failed query command again
588
+ deferrable.protect(&send_proc)
589
+ end
584
590
  end
591
+ # connection is bad, reset in progress, all done
592
+ return
585
593
  end
586
- else
587
- # connection is good, or the async_autoreconnect is not set
588
- deferrable.fail error
589
594
  end
595
+ # connection is either good or bad, the async_autoreconnect is not set
596
+ deferrable.fail error
590
597
  end
591
598
 
592
599
  # @!macro deferrable_api
@@ -653,7 +660,7 @@ module PG
653
660
 
654
661
  class_eval <<-EOD, __FILE__, __LINE__
655
662
  def #{defer_name}(*args, &blk)
656
- df = FeaturedDeferrable.new(&blk)
663
+ df = FeaturedDeferrable.new
657
664
  send_proc = proc do
658
665
  #{send_name}(*args)
659
666
  setup_emio_watcher.watch_results(df, send_proc)
@@ -663,10 +670,11 @@ module PG
663
670
  @last_transaction_status = transaction_status
664
671
  send_proc.call
665
672
  rescue Error => e
666
- ::EM.next_tick { async_autoreconnect!(df, e, &send_proc) }
673
+ ::EM.next_tick { async_autoreconnect!(df, e, send_proc) }
667
674
  rescue Exception => e
668
675
  ::EM.next_tick { df.fail(e) }
669
676
  end
677
+ df.completion(&blk) if block_given?
670
678
  df
671
679
  end
672
680
  EOD
@@ -678,6 +686,45 @@ module PG
678
686
  alias_method :async_exec_defer, :exec_defer
679
687
  alias_method :exec_params_defer, :exec_defer
680
688
 
689
+ # Asynchronously waits for notification or until the optional
690
+ # +timeout+ is reached, whichever comes first. +timeout+ is
691
+ # measured in seconds and can be fractional.
692
+ # Returns immediately with a Deferrable.
693
+ #
694
+ # Pass the block to the returned deferrable's +callback+ to obtain notification
695
+ # hash. In case of connection error +errback+ hook is called with an error object.
696
+ # If the +timeout+ is reached +nil+ is passed to deferrable's +callback+.
697
+ # If the block is provided it's bound to both the +callback+ and +errback+ hooks
698
+ # of the returned deferrable.
699
+ # If another call is made to this method before the notification is received
700
+ # (or before reaching timeout) the previous deferrable's +errback+ will be called
701
+ # with +nil+ argument.
702
+ #
703
+ # @return [FeaturedDeferrable]
704
+ # @yieldparam notification [Hash|nil|Error] notification hash or a PG::Error instance on error
705
+ # or nil when timeout is reached or canceled.
706
+ #
707
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-notifies PG::Connection#notifies
708
+ def wait_for_notify_defer(timeout = nil, &blk)
709
+ df = FeaturedDeferrable.new
710
+ begin
711
+ check_async_command_aborted!
712
+ if status == CONNECTION_OK
713
+ setup_emio_watcher.watch_notify(df, timeout)
714
+ else
715
+ raise_error ConnectionBad
716
+ end
717
+ rescue Error => e
718
+ ::EM.next_tick { async_autoreconnect!(df, e) }
719
+ rescue Exception => e
720
+ ::EM.next_tick { df.fail(e) }
721
+ end
722
+ df.completion(&blk) if block_given?
723
+ df
724
+ end
725
+
726
+ alias_method :notifies_wait_defer, :wait_for_notify_defer
727
+
681
728
  # Asynchronously retrieves the next result from a call to
682
729
  # #send_query (or another asynchronous command) and immediately
683
730
  # returns with a Deferrable.
@@ -692,11 +739,10 @@ module PG
692
739
  # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-get_result PG::Connection#get_result
693
740
  #
694
741
  def get_result_defer(&blk)
742
+ df = FeaturedDeferrable.new
695
743
  begin
696
- df = FeaturedDeferrable.new(&blk)
697
744
  if status == CONNECTION_OK
698
745
  if is_busy
699
- check_async_command_aborted!
700
746
  setup_emio_watcher.watch_results(df, nil, true)
701
747
  else
702
748
  df.succeed blocking_get_result
@@ -709,6 +755,7 @@ module PG
709
755
  rescue Exception => e
710
756
  ::EM.next_tick { df.fail(e) }
711
757
  end
758
+ df.completion(&blk) if block_given?
712
759
  df
713
760
  end
714
761
 
@@ -726,10 +773,9 @@ module PG
726
773
  # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-get_last_result PG::Connection#get_last_result
727
774
  #
728
775
  def get_last_result_defer(&blk)
776
+ df = FeaturedDeferrable.new
729
777
  begin
730
- df = FeaturedDeferrable.new(&blk)
731
778
  if status == CONNECTION_OK
732
- check_async_command_aborted!
733
779
  setup_emio_watcher.watch_results(df)
734
780
  else
735
781
  df.succeed
@@ -739,11 +785,13 @@ module PG
739
785
  rescue Exception => e
740
786
  ::EM.next_tick { df.fail(e) }
741
787
  end
788
+ df.completion(&blk) if block_given?
742
789
  df
743
790
  end
744
791
 
745
792
  # @!endgroup
746
793
 
794
+ alias_method :blocking_wait_for_notify, :wait_for_notify
747
795
  alias_method :blocking_get_result, :get_result
748
796
 
749
797
  def raise_error(klass=Error, message=error_message)
@@ -892,6 +940,44 @@ module PG
892
940
  alias_method :async_query, :exec
893
941
  alias_method :async_exec, :exec
894
942
 
943
+ # Blocks while waiting for notification(s), or until the optional
944
+ # +timeout+ is reached, whichever comes first.
945
+ # Returns +nil+ if +timeout+ is reached, the name of the +NOTIFY+
946
+ # event otherwise.
947
+ #
948
+ # If EventMachine reactor is running and the current fiber isn't the
949
+ # root fiber this method performs command asynchronously yielding
950
+ # current fiber. Other fibers can process while the current one is
951
+ # waiting for notifications.
952
+ #
953
+ # Otherwise performs a blocking call to a parent method.
954
+ # @return [String|nil]
955
+ # @yieldparam name [String] the name of the +NOTIFY+ event
956
+ # @yieldparam pid [Number] the generating pid
957
+ # @yieldparam payload [String] the optional payload
958
+ # @raise [PG::Error]
959
+ #
960
+ # @see http://deveiate.org/code/pg/PG/Connection.html#method-i-wait_for_notify PG::Connection#wait_for_notify
961
+ def wait_for_notify(timeout = nil)
962
+ if ::EM.reactor_running? && !(f = Fiber.current).equal?(ROOT_FIBER)
963
+ unless notify_hash = notifies
964
+ if (notify_hash = fiber_sync wait_for_notify_defer(timeout), f).is_a?(::Exception)
965
+ raise notify_hash
966
+ end
967
+ end
968
+ if notify_hash
969
+ if block_given?
970
+ yield notify_hash.values_at(:relname, :be_pid, :extra)
971
+ end
972
+ notify_hash[:relname]
973
+ end
974
+ else
975
+ super
976
+ end
977
+ end
978
+
979
+ alias_method :notifies_wait, :wait_for_notify
980
+
895
981
  # Retrieves the next result from a call to #send_query (or another
896
982
  # asynchronous command). If no more results are available returns
897
983
  # +nil+ and the block (if given) is never called.
@@ -11,6 +11,11 @@ module PG
11
11
  def initialize(client)
12
12
  @client = client
13
13
  @is_connected = true
14
+ @one_result_mode = false
15
+ @deferrable = nil
16
+ @notify_deferrable = nil
17
+ @timer = nil
18
+ @notify_timer = nil
14
19
  end
15
20
 
16
21
  def watching?
@@ -21,28 +26,49 @@ module PG
21
26
  @one_result_mode
22
27
  end
23
28
 
24
- def watch_results(deferrable, send_proc=nil, one_result_mode=false)
29
+ def watch_results(deferrable, send_proc = nil, one_result_mode = false)
25
30
  @one_result_mode = one_result_mode
26
31
  @last_result = nil
27
32
  @deferrable = deferrable
28
33
  @send_proc = send_proc
29
34
  cancel_timer
30
- self.notify_readable = true
35
+ self.notify_readable = true unless notify_readable?
31
36
  if (timeout = @client.query_timeout) > 0
32
- @notify_timestamp = Time.now
37
+ @readable_timestamp = Time.now
33
38
  setup_timer timeout
34
39
  end
35
40
  fetch_results
36
41
  end
37
42
 
43
+ def watch_notify(deferrable, timeout = nil)
44
+ notify_df = @notify_deferrable
45
+ @notify_deferrable = deferrable
46
+ cancel_notify_timer
47
+ self.notify_readable = true unless notify_readable?
48
+ if timeout
49
+ @notify_timer = ::EM::Timer.new(timeout) do
50
+ @notify_timer = nil
51
+ succeed_notify
52
+ end
53
+ end
54
+ notify_df.fail nil if notify_df
55
+ check_notify
56
+ end
57
+
38
58
  def setup_timer(timeout, adjustment = 0)
39
59
  @timer = ::EM::Timer.new(timeout - adjustment) do
40
- if (last_interval = Time.now - @notify_timestamp) >= timeout
60
+ if (last_interval = Time.now - @readable_timestamp) >= timeout
41
61
  @timer = nil
62
+ cancel_notify_timer
42
63
  self.notify_readable = false
43
64
  @client.async_command_aborted = true
44
- @deferrable.protect do
65
+ @send_proc = nil
66
+ begin
45
67
  @client.raise_error ConnectionBad, "query timeout expired (async)"
68
+ rescue Exception => e
69
+ fail_result e
70
+ # notify should also fail: query timeout is like connection error
71
+ fail_notify e
46
72
  end
47
73
  else
48
74
  setup_timer timeout, last_interval
@@ -50,6 +76,13 @@ module PG
50
76
  end
51
77
  end
52
78
 
79
+ def cancel_notify_timer
80
+ if @notify_timer
81
+ @notify_timer.cancel
82
+ @notify_timer = nil
83
+ end
84
+ end
85
+
53
86
  def cancel_timer
54
87
  if @timer
55
88
  @timer.cancel
@@ -60,9 +93,17 @@ module PG
60
93
  def notify_readable
61
94
  @client.consume_input
62
95
  rescue Exception => e
63
- handle_error(e)
96
+ handle_error e
64
97
  else
65
- fetch_results
98
+ fetch_results if @deferrable
99
+ check_notify if @notify_deferrable
100
+ end
101
+
102
+ def check_notify
103
+ if notify_hash = @client.notifies
104
+ cancel_notify_timer
105
+ succeed_notify notify_hash
106
+ end
66
107
  end
67
108
 
68
109
  # Carefully extract results without
@@ -84,40 +125,75 @@ module PG
84
125
  @last_result = single_result
85
126
  end
86
127
  rescue Exception => e
87
- handle_error(e)
128
+ handle_error e
88
129
  else
89
130
  if result == false
90
- @notify_timestamp = Time.now if @timer
131
+ @readable_timestamp = Time.now if @timer
91
132
  else
92
- self.notify_readable = false
93
133
  cancel_timer
94
- @send_proc = nil
95
- @deferrable.succeed(result)
134
+ self.notify_readable = false unless @notify_deferrable
135
+ df = @deferrable
136
+ @deferrable = @send_proc = nil
137
+ df.succeed result
96
138
  end
97
139
  end
98
140
 
99
141
  def unbind
100
142
  @is_connected = false
101
- @deferrable.protect do
102
- cancel_timer
143
+ cancel_timer
144
+ cancel_notify_timer
145
+ if @deferrable || @notify_deferrable
103
146
  @client.raise_error ConnectionBad, "connection reset"
104
- end if @deferrable
147
+ end
148
+ rescue Exception => e
149
+ fail_result e
150
+ fail_notify e
105
151
  end
106
152
 
107
153
  private
108
154
 
155
+ def fail_result(e)
156
+ df = @deferrable
157
+ @deferrable = nil
158
+ df.fail e if df
159
+ end
160
+
161
+ def succeed_notify(notify_hash = nil)
162
+ self.notify_readable = false unless @deferrable
163
+ df = @notify_deferrable
164
+ @notify_deferrable = nil
165
+ df.succeed notify_hash
166
+ end
167
+
168
+ def fail_notify(e)
169
+ df = @notify_deferrable
170
+ @notify_deferrable = nil
171
+ df.fail e if df
172
+ end
173
+
109
174
  def handle_error(e)
110
- self.notify_readable = false
111
175
  cancel_timer
112
176
  send_proc = @send_proc
113
177
  @send_proc = nil
114
- df = @deferrable
178
+ df = @deferrable || FeaturedDeferrable.new
115
179
  # prevent unbind error on auto re-connect
116
- @deferrable = false
180
+ @deferrable = nil
181
+ notify_df = @notify_deferrable
182
+ self.notify_readable = false unless notify_df
117
183
  if e.is_a?(PG::Error)
118
- @client.async_autoreconnect!(df, e, &send_proc)
184
+ @client.async_autoreconnect!(df, e, send_proc) do
185
+ # there was a connection error so stop any remaining activity
186
+ if notify_df
187
+ @notify_deferrable = nil
188
+ cancel_notify_timer
189
+ self.notify_readable = false
190
+ # fail notify_df after deferrable completes
191
+ # handler might setup listen again then immediately
192
+ df.completion { notify_df.fail e }
193
+ end
194
+ end
119
195
  else
120
- df.fail(e)
196
+ df.fail e
121
197
  end
122
198
  end
123
199