serf 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +21 -0
  2. data/.travis.yml +7 -0
  3. data/Gemfile +20 -26
  4. data/Guardfile +16 -0
  5. data/NOTICE.txt +1 -1
  6. data/README.md +223 -207
  7. data/Rakefile +3 -18
  8. data/lib/serf/builder.rb +31 -136
  9. data/lib/serf/errors/policy_failure.rb +10 -0
  10. data/lib/serf/middleware/error_handler.rb +53 -0
  11. data/lib/serf/middleware/parcel_freezer.rb +36 -0
  12. data/lib/serf/middleware/parcel_masher.rb +39 -0
  13. data/lib/serf/middleware/policy_checker.rb +31 -0
  14. data/lib/serf/middleware/uuid_tagger.rb +13 -11
  15. data/lib/serf/parcel_builder.rb +30 -0
  16. data/lib/serf/serfer.rb +27 -66
  17. data/lib/serf/util/error_handling.rb +13 -36
  18. data/lib/serf/util/protected_call.rb +2 -2
  19. data/lib/serf/util/uuidable.rb +14 -38
  20. data/lib/serf/version.rb +1 -1
  21. data/schemas/{caught_exception_event.json → serf/events/caught_error.json} +4 -7
  22. data/serf.gemspec +22 -101
  23. data/spec/serf/builder_spec.rb +44 -0
  24. data/spec/serf/errors/policy_failure_spec.rb +11 -0
  25. data/spec/serf/middleware/error_handler_spec.rb +48 -0
  26. data/spec/serf/middleware/parcel_freezer_spec.rb +20 -0
  27. data/spec/serf/middleware/parcel_masher_spec.rb +30 -0
  28. data/spec/serf/middleware/policy_checker_spec.rb +70 -0
  29. data/spec/serf/middleware/uuid_tagger_spec.rb +32 -0
  30. data/spec/serf/parcel_builder_spec.rb +46 -0
  31. data/spec/serf/serfer_spec.rb +61 -0
  32. data/spec/serf/util/error_handling_spec.rb +35 -0
  33. data/spec/serf/util/null_object_spec.rb +26 -0
  34. data/spec/serf/util/options_extraction_spec.rb +62 -0
  35. data/spec/serf/util/protected_call_spec.rb +33 -0
  36. data/spec/serf/util/uuidable_spec.rb +56 -0
  37. data/spec/serf_spec.rb +1 -4
  38. data/spec/spec_helper.rb +3 -0
  39. data/spec/support/error_handling_wrapper.rb +5 -0
  40. data/spec/support/factories.rb +32 -0
  41. data/spec/support/failing_policy.rb +9 -0
  42. data/spec/support/json_schema_tester.rb +14 -0
  43. data/spec/support/options_extraction_wrapper.rb +10 -0
  44. data/spec/support/passing_policy.rb +7 -0
  45. data/spec/support/protected_call_wrapper.rb +5 -0
  46. metadata +81 -131
  47. data/.document +0 -5
  48. data/.rspec +0 -1
  49. data/Gemfile.lock +0 -58
  50. data/docs/thread_pools.txt +0 -16
  51. data/lib/serf/command.rb +0 -79
  52. data/lib/serf/error.rb +0 -11
  53. data/lib/serf/errors/not_found.rb +0 -8
  54. data/lib/serf/middleware/girl_friday_async.rb +0 -39
  55. data/lib/serf/middleware/masherize.rb +0 -25
  56. data/lib/serf/routing/regexp_matcher.rb +0 -35
  57. data/lib/serf/routing/route.rb +0 -35
  58. data/lib/serf/routing/route_set.rb +0 -64
  59. data/schemas/message_accepted_event.json +0 -14
data/lib/serf/builder.rb CHANGED
@@ -1,166 +1,61 @@
1
- require 'serf/routing/route'
2
- require 'serf/routing/route_set'
1
+ require 'serf/middleware/error_handler'
2
+ require 'serf/middleware/parcel_freezer'
3
+ require 'serf/middleware/parcel_masher'
4
+ require 'serf/middleware/policy_checker'
5
+ require 'serf/middleware/uuid_tagger'
3
6
  require 'serf/serfer'
4
- require 'serf/util/null_object'
5
7
  require 'serf/util/options_extraction'
6
8
 
7
9
  module Serf
8
10
 
9
- ##
10
- # A Serf Builder that processes the SerfUp DSL to build a rack-like
11
- # app to handlers that process received messages. This builder is
12
- # implemented based on code from Rack::Builder.
13
- #
14
- # builder = Serf::Builder.parse_file 'examples/config.su'
15
- # builder.to_app
16
- #
17
- # or
18
- #
19
- # builder = Serf::Builder.new do
20
- # ... A SerfUp Config block here.
21
- # end
22
- # builder.to_app
23
- #
24
11
  class Builder
25
12
  include Serf::Util::OptionsExtraction
26
13
 
27
- attr_reader :serfer_factory
28
- attr_reader :route_set_factory
29
- attr_reader :route_factory
30
-
31
- def self.parse_file(config)
32
- cfgfile = ::File.read(config)
33
- builder = eval "Serf::Builder.new {\n" + cfgfile + "\n}",
34
- TOPLEVEL_BINDING, config
35
- return builder
36
- end
37
-
38
- def self.app(*args, &block)
39
- new(*args, &block).to_app
40
- end
41
-
42
14
  def initialize(*args, &block)
43
15
  extract_options! args
44
16
 
45
- # Our factories
46
- @serfer_factory = opts :serfer_factory, Serf::Serfer
47
- @route_set_factory = opts :route_set_factory, Serf::Routing::RouteSet
48
- @route_factory = opts :route_factory, Serf::Routing::Route
49
-
50
- # List of middleware to be executed (non-built form)
17
+ @run = opts :interactor
51
18
  @use = []
19
+ @policy_chain = opts :policy_chain, []
52
20
 
53
- # A list of "mounted", non-built, command handlers with their
54
- # matcher and policies.
55
- @runs = []
56
-
57
- # List of default policies to be run (non-built form)
58
- @default_policies = []
59
-
60
- # The current matcher
61
- @matcher = nil
62
-
63
- # Current policies to be run (PRE-built)
64
- @policies = []
65
-
66
- # configure based on a given block.
67
- instance_eval(&block) if block_given?
21
+ if block_given?
22
+ instance_eval(&block)
23
+ else
24
+ use_defaults
25
+ end
68
26
  end
69
27
 
70
28
  ##
71
- # Append a policy to default policy chain. The default
72
- # policy chain is used by any route that does not define
73
- # at least one of its own policies.
29
+ # Set a default chain of the following:
30
+ #
31
+ # use Serf::Middleware::ParcelMasher
32
+ # use Serf::Middleware::UuidTagger
33
+ # use Serf::Middleware::ParcelFreezer
34
+ # use Serf::Middleware::ErrorHandler
35
+ # use Serf::Middleware::PolicyChecker, @policy_chain
36
+ # use Serf::Serfer
74
37
  #
75
- # @param policy the policy factory to append
76
- # @param *args the arguments to pass to the factory
77
- # @param &block the block to pass to the factory
78
- def default_policy(policy, *args, &block)
79
- @default_policies << proc { policy.build(*args, &block) }
38
+ def use_defaults
39
+ use Serf::Middleware::ParcelMasher
40
+ use Serf::Middleware::UuidTagger
41
+ use Serf::Middleware::ParcelFreezer
42
+ use Serf::Middleware::ErrorHandler
43
+ use Serf::Middleware::PolicyChecker, policy_chain: @policy_chain
44
+ use Serf::Serfer
80
45
  end
81
46
 
82
- ##
83
- # Append a rack-like middleware
84
- #
85
- # @param the middleware class
86
- # @param *args the arguments to pass to middleware.new
87
- # @param &block the block to pass to middleware.new
88
47
  def use(middleware, *args, &block)
89
48
  @use << proc { |app| middleware.new(app, *args, &block) }
90
49
  end
91
50
 
92
- ##
93
- # Append a policy to the current match's policy chain.
94
- #
95
- # @param policy the policy factory to append
96
- # @param *args the arguments to pass to the factory
97
- # @param &block the block to pass to the factory
98
- def policy(policy, *args, &block)
99
- @policies << proc { policy.build(*args, &block) }
51
+ def run(interactor)
52
+ @run = interactor
100
53
  end
101
54
 
102
- def response_channel(channel); @response_channel = channel; end
103
- def error_channel(channel); @error_channel = channel; end
104
- def logger(logger); @logger = logger; end
105
-
106
- ##
107
- # DSL Method to change our current context to use the given matcher.
108
- #
109
- def match(matcher)
110
- @matcher = matcher
111
- @policies = []
112
- end
113
-
114
- ##
115
- # @param command_factory the factory to invoke (in #to_app)
116
- # @param *args the rest of the args to pass to command_factory#build method
117
- # @param &block the block to pass to command_factory#build method
118
- def run(command_factory, *args, &block)
119
- raise 'No matcher defined yet' unless @matcher
120
- # Create a local duplicate of the matcher and policies "snapshotted"
121
- # at the time this method is called... so that snapshot is consistent
122
- # for when the proc is called.
123
- matcher = @matcher.dup
124
- policies = @policies.dup
125
-
126
- # This proc will be called in to_app when we actually go ahead and
127
- # instantiate all the objects. By this point, route_set and
128
- # default_policies passed to this proc will be ready, built.
129
- @runs << proc { |route_set, default_policies|
130
- route_set.add(
131
- matcher,
132
- route_factory.build(
133
- command: command_factory.build(*args, &block),
134
- policies: (policies.size > 0 ?
135
- policies.map{ |p| p.call } :
136
- default_policies)))
137
- }
138
- end
139
-
140
- ##
141
- # Create our app.
142
- #
143
55
  def to_app
144
- # Create the route_set to resolve routes
145
- route_set = route_set_factory.build
146
- # Build the default policies to be used if routes did not specify any.
147
- default_policies = @default_policies.map{ |p| p.call }
148
- # Add each route to the route_set
149
- for run in @runs
150
- run.call route_set, default_policies
151
- end
152
- # Create our serfer class
153
- app = serfer_factory.build(
154
- route_set: route_set,
155
- response_channel: (@response_channel || Serf::Util::NullObject.new),
156
- error_channel: (@error_channel || Serf::Util::NullObject.new),
157
- logger: (@logger || Serf::Util::NullObject.new))
158
-
159
- # We're going to inject middleware here.
160
- app = @use.reverse.inject(app) { |a,e| e[a] } if @use.size > 0
161
-
162
- return app
56
+ @use.reverse.inject(@run) { |a,e| e[a] }
163
57
  end
164
58
 
165
59
  end
60
+
166
61
  end
@@ -0,0 +1,10 @@
1
+ module Serf
2
+ module Errors
3
+
4
+ ##
5
+ # Common base error to raise for any policy failure.
6
+ class PolicyFailure < RuntimeError
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,53 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/parcel_builder'
4
+ require 'serf/util/error_handling'
5
+ require 'serf/util/uuidable'
6
+
7
+ module Serf
8
+ module Middleware
9
+
10
+ ##
11
+ # Middleware to catch raised exceptions and return an error parcel
12
+ # instead.
13
+ #
14
+ class ErrorHandler
15
+ include Serf::Util::ErrorHandling
16
+ include Serf::Util::OptionsExtraction
17
+
18
+ attr_reader :app
19
+ attr_reader :parcel_builder
20
+ attr_reader :uuidable
21
+
22
+ ##
23
+ # @param app the app
24
+ #
25
+ def initialize(app, *args)
26
+ extract_options! args
27
+ @app = app
28
+
29
+ # Tunable knobs
30
+ @parcel_builder = opts(:parcel_builder) { Serf::ParcelBuilder.new }
31
+ @uuidable = opts(:uuidable) { Serf::Util::Uuidable.new }
32
+ end
33
+
34
+ def call(parcel)
35
+ # Attempt to execute the app, catching errors
36
+ response_parcel, error_message = with_error_handling do
37
+ app.call parcel
38
+ end
39
+
40
+ # Return on success
41
+ return response_parcel if response_parcel
42
+
43
+ # We got an error message instead, so build out the headers
44
+ # and return the parcel.
45
+ error_headers = uuidable.create_uuids parcel[:headers]
46
+ error_headers[:kind] = 'serf/events/caught_error'
47
+ return parcel_builder.build error_headers, error_message
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ require 'ice_nine'
2
+
3
+ require 'serf/util/options_extraction'
4
+
5
+ module Serf
6
+ module Middleware
7
+
8
+ ##
9
+ # Middleware to add uuids to the headers of the parcel hash.
10
+ #
11
+ class ParcelFreezer
12
+ include Serf::Util::OptionsExtraction
13
+
14
+ attr_reader :app
15
+ attr_reader :freezer
16
+
17
+ ##
18
+ # @param app the app
19
+ #
20
+ def initialize(app, *args)
21
+ extract_options! args
22
+ @app = app
23
+ @freezer = opts :freezer, IceNine
24
+ end
25
+
26
+ ##
27
+ # Chains the call, but deep freezes the parcel.
28
+ def call(parcel)
29
+ freezer.deep_freeze parcel
30
+ app.call parcel
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/util/options_extraction'
4
+
5
+ module Serf
6
+ module Middleware
7
+
8
+ ##
9
+ # Middleware to add uuids to the headers of the parcel hash.
10
+ #
11
+ class ParcelMasher
12
+ include Serf::Util::OptionsExtraction
13
+
14
+ attr_reader :app
15
+ attr_reader :masher_class
16
+
17
+ ##
18
+ # @param app the app
19
+ #
20
+ def initialize(app, *args)
21
+ extract_options! args
22
+ @app = app
23
+ @masher_class = opts :masher_class, Hashie::Mash
24
+ end
25
+
26
+ ##
27
+ # Coerces the parcel into a Hashie::Mash, makes sure that
28
+ # the headers and message are set, and then passes it along the chain.
29
+ def call(parcel)
30
+ mashed_parcel = masher_class.new parcel
31
+ mashed_parcel[:headers] ||= {}
32
+ mashed_parcel[:message] ||= {}
33
+ app.call mashed_parcel
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ require 'serf/util/options_extraction'
2
+
3
+ module Serf
4
+ module Middleware
5
+
6
+ class PolicyChecker
7
+ include Serf::Util::OptionsExtraction
8
+
9
+ attr_reader :app
10
+ attr_reader :policy_chain
11
+
12
+ def initialize(app, *args)
13
+ extract_options! args
14
+ @app = app
15
+ @policy_chain = opts :policy_chain, []
16
+ end
17
+
18
+ ##
19
+ # Iterates the policy chain and does a check for each policy.
20
+ # Assumes that policies will raise errors on any policy failure.
21
+ def call(parcel)
22
+ policy_chain.each do |policy|
23
+ policy.check! parcel
24
+ end
25
+ app.call parcel
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -1,36 +1,38 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/util/options_extraction'
1
4
  require 'serf/util/uuidable'
2
5
 
3
6
  module Serf
4
7
  module Middleware
5
8
 
6
9
  ##
7
- # Middleware to add a request uuid to both the message and context
8
- # of the env hash. But it won't overwrite the uuid field
9
- # if the incoming request already has it.
10
+ # Middleware to add uuids to the headers of the parcel hash.
10
11
  #
11
12
  class UuidTagger
12
13
  include Serf::Util::OptionsExtraction
13
14
 
15
+ attr_reader :app
14
16
  attr_reader :uuidable
15
17
 
16
18
  ##
17
19
  # @param app the app
18
- # @options opts [String] :field the ENV field to set with a UUID.
19
20
  #
20
21
  def initialize(app, *args)
21
22
  extract_options! args
22
23
  @app = app
23
- @uuidable = opts :uuidable, Serf::Util::Uuidable
24
+ @uuidable = opts(:uuidable) { Serf::Util::Uuidable.new }
24
25
  end
25
26
 
26
- def call(env)
27
- message = env[:message]
28
- message[:uuid] = uuidable.create_coded_uuid if message && !message[:uuid]
27
+ def call(parcel)
28
+ # Makes sure our parcel has headers
29
+ parcel[:headers] ||= {}
29
30
 
30
- context = env[:context]
31
- context[:uuid] = uuidable.create_coded_uuid if context && !context[:uuid]
31
+ # Tag headers with a UUID unless it already has one
32
+ parcel[:headers][:uuid] ||= uuidable.create_coded_uuid
32
33
 
33
- @app.call env
34
+ # Pass on the given parcel with newly annotated headers
35
+ app.call parcel
34
36
  end
35
37
 
36
38
  end
@@ -0,0 +1,30 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/util/options_extraction'
4
+
5
+ module Serf
6
+
7
+ ##
8
+ # Builds Parcels as Hashie::Mash objects with headers and messages.
9
+ #
10
+ class ParcelBuilder
11
+ include Serf::Util::OptionsExtraction
12
+
13
+ attr_reader :mash_class
14
+
15
+ def initialize(*args)
16
+ extract_options! args
17
+
18
+ @mash_class = opts :mash_class, Hashie::Mash
19
+ end
20
+
21
+ def build(headers=nil, message=nil)
22
+ # We want to make sure that our headers and message are Mashes.
23
+ headers = mash_class.new(headers) unless headers.kind_of? mash_class
24
+ message = mash_class.new(message) unless message.kind_of? mash_class
25
+ mash_class.new headers: headers, message: message
26
+ end
27
+
28
+ end
29
+
30
+ end
data/lib/serf/serfer.rb CHANGED
@@ -1,87 +1,48 @@
1
1
  require 'hashie'
2
2
 
3
- require 'serf/error'
4
- require 'serf/errors/not_found'
5
- require 'serf/util/error_handling'
6
- require 'serf/util/null_object'
3
+ require 'serf/parcel_builder'
4
+ require 'serf/util/options_extraction'
5
+ require 'serf/util/uuidable'
7
6
 
8
7
  module Serf
9
8
 
10
9
  ##
11
- # Class to drive the command handler execution, error handling, etc
12
- # of received messages.
10
+ # Class to drive the Interactor execution.
11
+ #
13
12
  class Serfer
14
- include Serf::Util::ErrorHandling
13
+ include Serf::Util::OptionsExtraction
15
14
 
16
- attr_reader :route_set
17
- attr_reader :response_channel
18
- attr_reader :error_channel
19
- attr_reader :logger
15
+ attr_reader :interactor
16
+ attr_reader :parcel_builder
17
+ attr_reader :uuidable
20
18
 
21
- def initialize(*args)
19
+ def initialize(interactor, *args)
22
20
  extract_options! args
23
21
 
24
- @route_set = opts! :route_set
25
- @response_channel = opts(:response_channel) { Serf::Util::NullObject }
26
- @error_channel = opts(:error_channel) { Serf::Util::NullObject }
27
- @logger = opts(:logger) { Serf::Util::NullObject }
22
+ # How to and when to handle requests
23
+ @interactor = interactor
24
+
25
+ # Tunable knobs
26
+ @parcel_builder = opts(:parcel_builder) { Serf::ParcelBuilder.new }
27
+ @uuidable = opts(:uuidable) { Serf::Util::Uuidable.new }
28
28
  end
29
29
 
30
30
  ##
31
- # Rack-like call to run a set of handlers for a message
31
+ # Rack-like call to run the Interactor's use-case.
32
32
  #
33
- def call(env)
34
- env = Hashie::Mash.new env unless env.is_a? Hashie::Mash
35
-
36
- # We normalize by making the request a Hashie Mash
37
- message = Hashie::Mash.new env.message
38
- context = Hashie::Mash.new env.context
39
-
40
- # Resolve the routes that we want to run
41
- routes = route_set.resolve message, context
42
-
43
- # We raise an error if no routes were found.
44
- raise Serf::Errors::NotFound unless routes.size > 0
33
+ def call(parcel)
34
+ headers = parcel[:headers]
35
+ message = parcel[:message]
45
36
 
46
- # For each route, we're going to execute
47
- results = routes.map { |route|
48
- # 1. Check request+context with the policies (RAISE)
49
- # 2. Execute command (RETURNS Hash)
50
- ok, res = with_error_handling(
51
- message: message,
52
- options: context) do
53
- route.check_policies! message, context
54
- route.execute! message, context
55
- end
56
- # Return the run_results as result of this block.
57
- res
58
- }.flatten.select { |r| r }
59
- push_results results, context
60
- return results
61
- rescue => e
62
- e.extend(Serf::Error)
63
- raise e
64
- end
65
-
66
- def self.build(*args, &block)
67
- new *args, &block
68
- end
37
+ # 1. Execute interactor
38
+ response_message, response_kind = interactor.call message
69
39
 
70
- private
40
+ # 2. Create the response headers
41
+ response_headers = uuidable.create_uuids headers
42
+ response_headers[:kind] = response_kind
71
43
 
72
- ##
73
- # Loop over the results and push them to the response channel.
74
- # Any error in pushing individual messages will result in
75
- # a log event and an error channel event.
76
- def push_results(results, context)
77
- results.each do |result|
78
- with_error_handling(result) do
79
- response_channel.push(
80
- message: result,
81
- context: context)
82
- end
83
- end
84
- return nil
44
+ # 3. Return the response headers and message as a parcel
45
+ return parcel_builder.build response_headers, response_message
85
46
  end
86
47
 
87
48
  end
@@ -1,7 +1,3 @@
1
- require 'active_support/core_ext/string/inflections'
2
-
3
- require 'serf/util/null_object'
4
- require 'serf/util/options_extraction'
5
1
  require 'serf/util/protected_call'
6
2
 
7
3
  module Serf
@@ -9,53 +5,34 @@ module Util
9
5
 
10
6
  ##
11
7
  # Helper module to rescues exceptions from executing blocks of
12
- # code, and then logs+pushes the error event.
8
+ # code, and then converts the exception to an "Error Message".
13
9
  #
14
- # Including classes may have the following instance variables
15
- # to override the default values:
16
- # * @logger - ::Serf::Util::NullObject.new
17
- # * @error_channel - ::Serf::Util::NullObject.new
18
10
  module ErrorHandling
19
- include Serf::Util::OptionsExtraction
20
11
  include Serf::Util::ProtectedCall
21
12
 
22
13
  ##
23
14
  # A block wrapper to handle errors when executing a block.
24
15
  #
25
- def with_error_handling(context=nil, *args, &block)
26
- ok, results = pcall *args, &block
27
- return ok, (ok ? results : handle_error(results, context))
16
+ def with_error_handling(*args, &block)
17
+ results, err = pcall *args, &block
18
+ return results, handle_error(err)
28
19
  end
29
20
 
30
21
  ##
31
22
  # Including classes may override this method to do alternate error
32
- # handling. By default, this method will create a new caught exception
33
- # event and publish it to the error channel. This method will also
34
- # log the exception itself to the logger.
23
+ # handling. By default, this method will create a new error event message.
35
24
  #
36
- def handle_error(e, context=nil)
37
- logger = opts(:logger){ ::Serf::Util::NullObject.new }
38
- error_channel = opts(:error_channel) { ::Serf::Util::NullObject.new }
39
- error_event = {
40
- kind: 'serf/messages/caught_exception_event',
41
- context: context,
42
- error: e.class.to_s.underscore,
25
+ def handle_error(e)
26
+ # no error was passed, so do nothing.
27
+ return nil unless e
28
+
29
+ # Return a simple error event message
30
+ return {
31
+ error: e.class.to_s,
43
32
  message: e.message,
33
+ process_env: ENV.to_hash,
44
34
  backtrace: e.backtrace.join("\n")
45
35
  }
46
-
47
- # log the error to our logger
48
- logger.error e
49
-
50
- # log the error event to our error channel.
51
- begin
52
- error_channel.push error_event
53
- rescue => e1
54
- logger.error e1
55
- end
56
-
57
- # We're done, so just return this error.
58
- return error_event
59
36
  end
60
37
 
61
38
  end
@@ -24,9 +24,9 @@ module Util
24
24
  # @return boolean success and the block's (or caught exception) results.
25
25
  #
26
26
  def pcall(*args)
27
- return true, yield(*args)
27
+ return yield(*args), nil
28
28
  rescue => e
29
- return false, e
29
+ return nil, e
30
30
  end
31
31
  alias_method :protected_call, :pcall
32
32