resque-bus 0.3.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG.md +15 -0
  5. data/Gemfile +2 -3
  6. data/README.mdown +50 -64
  7. data/Rakefile +0 -1
  8. data/lib/resque-bus.rb +21 -283
  9. data/lib/resque_bus/adapter.rb +66 -0
  10. data/lib/resque_bus/compatibility/deprecated.rb +38 -0
  11. data/lib/resque_bus/compatibility/driver.rb +10 -0
  12. data/lib/resque_bus/compatibility/heartbeat.rb +10 -0
  13. data/lib/resque_bus/compatibility/publisher.rb +13 -0
  14. data/lib/resque_bus/compatibility/rider.rb +32 -0
  15. data/lib/resque_bus/compatibility/subscriber.rb +8 -0
  16. data/lib/resque_bus/compatibility/task_manager.rb +8 -0
  17. data/lib/resque_bus/server.rb +6 -5
  18. data/lib/resque_bus/server/views/bus.erb +2 -2
  19. data/lib/resque_bus/tasks.rb +46 -46
  20. data/lib/resque_bus/version.rb +2 -4
  21. data/resque-bus.gemspec +5 -12
  22. data/spec/adapter/compatibility_spec.rb +97 -0
  23. data/spec/adapter/integration_spec.rb +111 -0
  24. data/spec/adapter/publish_at_spec.rb +50 -0
  25. data/spec/adapter/retry_spec.rb +47 -0
  26. data/spec/adapter/support.rb +23 -0
  27. data/spec/adapter_spec.rb +14 -0
  28. data/spec/application_spec.rb +62 -62
  29. data/spec/config_spec.rb +83 -0
  30. data/spec/dispatch_spec.rb +6 -6
  31. data/spec/driver_spec.rb +62 -53
  32. data/spec/heartbeat_spec.rb +4 -4
  33. data/spec/integration_spec.rb +2 -2
  34. data/spec/matcher_spec.rb +29 -29
  35. data/spec/publish_spec.rb +62 -38
  36. data/spec/publisher_spec.rb +7 -0
  37. data/spec/rider_spec.rb +14 -66
  38. data/spec/spec_helper.rb +25 -28
  39. data/spec/subscriber_spec.rb +194 -176
  40. data/spec/subscription_list_spec.rb +1 -1
  41. data/spec/subscription_spec.rb +1 -1
  42. data/spec/worker_spec.rb +32 -0
  43. metadata +75 -91
  44. data/lib/resque_bus/application.rb +0 -115
  45. data/lib/resque_bus/dispatch.rb +0 -61
  46. data/lib/resque_bus/driver.rb +0 -30
  47. data/lib/resque_bus/heartbeat.rb +0 -106
  48. data/lib/resque_bus/local.rb +0 -34
  49. data/lib/resque_bus/matcher.rb +0 -81
  50. data/lib/resque_bus/publisher.rb +0 -12
  51. data/lib/resque_bus/rider.rb +0 -54
  52. data/lib/resque_bus/subscriber.rb +0 -63
  53. data/lib/resque_bus/subscription.rb +0 -55
  54. data/lib/resque_bus/subscription_list.rb +0 -53
  55. data/lib/resque_bus/task_manager.rb +0 -52
  56. data/lib/resque_bus/util.rb +0 -42
  57. data/lib/tasks/resquebus.rake +0 -2
  58. data/spec/publish_at_spec.rb +0 -74
  59. data/spec/redis_spec.rb +0 -13
@@ -0,0 +1,66 @@
1
+ module QueueBus
2
+ module Adapters
3
+ class Resque < QueueBus::Adapters::Base
4
+ def enabled!
5
+ # know we are using it
6
+ require 'resque'
7
+ require 'resque/scheduler'
8
+ require 'resque-retry'
9
+
10
+ QueueBus::Worker.extend(::Resque::Plugins::ExponentialBackoff)
11
+ QueueBus::Worker.extend(::QueueBus::Adapters::Resque::RetryHandlers)
12
+ end
13
+
14
+ def redis(&block)
15
+ block.call(::Resque.redis)
16
+ end
17
+
18
+ def enqueue(queue_name, klass, json)
19
+ ::Resque.enqueue_to(queue_name, klass, json)
20
+ end
21
+
22
+ def enqueue_at(epoch_seconds, queue_name, klass, json)
23
+ ::Resque.enqueue_at_with_queue(queue_name, epoch_seconds, klass, json)
24
+ end
25
+
26
+ def setup_heartbeat!(queue_name)
27
+ # turn on the heartbeat
28
+ # should be down after loading scheduler yml if you do that
29
+ # otherwise, anytime
30
+ name = 'resquebus_heartbeat'
31
+ schedule = { 'class' => '::QueueBus::Worker',
32
+ 'args'=>[::QueueBus::Util.encode({'bus_class_proxy' => '::QueueBus::Heartbeat'})],
33
+ 'cron' => '* * * * *', # every minute
34
+ 'queue' => queue_name,
35
+ 'description' => 'I publish a heartbeat_minutes event every minute'
36
+ }
37
+ if ::Resque::Scheduler.dynamic
38
+ ::Resque.set_schedule(name, schedule)
39
+ end
40
+ ::Resque.schedule[name] = schedule
41
+ end
42
+
43
+ private
44
+
45
+ module RetryHandlers
46
+ # @failure_hooks_already_ran on https://github.com/defunkt/resque/tree/1-x-stable
47
+ # to prevent running twice
48
+ def queue
49
+ @my_queue
50
+ end
51
+
52
+ def on_failure_aaa(exception, *args)
53
+ # note: sorted alphabetically
54
+ # queue needs to be set for rety to work (know what queue in Requeue.class_to_queue)
55
+ hash = ::QueueBus::Util.decode(args[0])
56
+ @my_queue = hash["bus_rider_queue"]
57
+ end
58
+
59
+ def on_failure_zzz(exception, *args)
60
+ # note: sorted alphabetically
61
+ @my_queue = nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,38 @@
1
+ module ResqueBus
2
+ module Deprecated
3
+ def show_deprecations=val
4
+ @show_deprecations = val
5
+ end
6
+
7
+ def show_deprecations?
8
+ return @show_deprecations if defined?(@show_deprecations)
9
+ return true if !ENV['QUEUES'] && !ENV['QUEUE'] # not in background, probably test
10
+ return true if ENV['VVERBOSE'] || ENV['LOGGING'] || ENV['VERBOSE']
11
+ false
12
+ end
13
+
14
+ def note_deprecation(message)
15
+ @noted_deprecations ||= {}
16
+ if @noted_deprecations[message]
17
+ @noted_deprecations[message] += 1
18
+ else
19
+ warn(message) if show_deprecations?
20
+ @noted_deprecations[message] = 1
21
+ end
22
+ end
23
+
24
+ def redis
25
+ ResqueBus.note_deprecation "[DEPRECATION] ResqueBus direct usage is deprecated. Use `QueueBus.redis` instead. Note that it also requires block usage now."
26
+ ::Resque.redis
27
+ end
28
+
29
+ def redis=val
30
+ ResqueBus.note_deprecation "[DEPRECATION] ResqueBus can no longer set redis directly. It will use Resque's instance of redis."
31
+ end
32
+
33
+ def method_missing(method_name, *args, &block)
34
+ ResqueBus.note_deprecation "[DEPRECATION] ResqueBus direct usage is deprecated. Use `QueueBus.#{method_name}` instead."
35
+ ::QueueBus.send(method_name, *args, &block)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,10 @@
1
+ module ResqueBus
2
+ class Driver
3
+ class << self
4
+ def perform(attributes={})
5
+ ResqueBus.note_deprecation "[MIGRATION] Note: new events will be using QueueBus::Driver"
6
+ ::QueueBus::Driver.perform(attributes)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module ResqueBus
2
+ class Heartbeat
3
+ class << self
4
+ def perform(attributes={})
5
+ ResqueBus.note_deprecation "[MIGRATION] Note: new events will be using QueueBus::Heartbeat"
6
+ ::QueueBus::Heartbeat.perform(attributes)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ module ResqueBus
2
+ # publishes on a delay
3
+ class Publisher
4
+ class << self
5
+ def perform(event_type, attributes = {})
6
+ attributes["bus_event_type"] = event_type # now using one hash only
7
+ ResqueBus.note_deprecation "[MIGRATION] Note: new events will be using QueueBus::Publisher"
8
+ ::QueueBus::Publisher.perform(attributes)
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ require 'resque-retry'
2
+
3
+ module ResqueBus
4
+ class Rider
5
+ extend Resque::Plugins::ExponentialBackoff
6
+
7
+ class << self
8
+ def perform(attributes = {})
9
+ ResqueBus.note_deprecation "[MIGRATION] Note: new events will be using QueueBus::Rider"
10
+ ::QueueBus::Rider.perform(attributes)
11
+ end
12
+
13
+ # @failure_hooks_already_ran on https://github.com/defunkt/resque/tree/1-x-stable
14
+ # to prevent running twice
15
+ def queue
16
+ @my_queue
17
+ end
18
+
19
+ def on_failure_aaa(exception, *args)
20
+ # note: sorted alphabetically
21
+ # queue needs to be set for rety to work (know what queue in Requeue.class_to_queue)
22
+ @my_queue = args[0]["bus_rider_queue"]
23
+ end
24
+
25
+ def on_failure_zzz(exception, *args)
26
+ # note: sorted alphabetically
27
+ @my_queue = nil
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ module ResqueBus
2
+ module Subscriber
3
+ def self.included(base)
4
+ ResqueBus.note_deprecation "[DEPRECATION] ResqueBus::Subscriber is deprecated. Use QueueBus::Subscriber instead."
5
+ base.send(:include, ::QueueBus::Subscriber)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module ResqueBus
2
+ class TaskManager < ::QueueBus::TaskManager
3
+ def initialize(logging)
4
+ ResqueBus.note_deprecation "[DEPRECATION] ResqueBus::TaskManager is deprecated. Use QueueBus::TaskManager instead."
5
+ super(logging)
6
+ end
7
+ end
8
+ end
@@ -2,7 +2,8 @@ require 'resque-bus'
2
2
  require 'resque/server'
3
3
  require 'erb'
4
4
 
5
- # Extend Resque::Server to add tabs.
5
+ # MIGRATE TODO: move to resque gem
6
+ # Extend ::Resque::Server to add tabs.
6
7
  module ResqueBus
7
8
  module Server
8
9
 
@@ -15,7 +16,7 @@ module ResqueBus
15
16
 
16
17
 
17
18
  post '/bus/unsubscribe' do
18
- app = Application.new(params[:name]).unsubscribe
19
+ app = ::QueueBus::Application.new(params[:name]).unsubscribe
19
20
  redirect u('bus')
20
21
  end
21
22
 
@@ -24,7 +25,7 @@ module ResqueBus
24
25
  end
25
26
  end
26
27
 
27
- Resque::Server.tabs << 'Bus'
28
- Resque::Server.class_eval do
29
- include ResqueBus::Server
28
+ ::Resque::Server.tabs << 'Bus'
29
+ ::Resque::Server.class_eval do
30
+ include ::ResqueBus::Server
30
31
  end
@@ -18,7 +18,7 @@ else
18
18
  event_hash = {}
19
19
 
20
20
  # collect each differently
21
- ResqueBus::Application.all.each do |app|
21
+ ::QueueBus::Application.all.each do |app|
22
22
  app_key = app.app_key
23
23
 
24
24
  app_hash[app_key] ||= []
@@ -61,7 +61,7 @@ else
61
61
  else
62
62
  one, two, queue, filters = val
63
63
  out = "<td>#{h(one)}</td><td>#{h(two)}</td><td><a href='#{u("queues/#{queue}")}'>#{h(queue)}</a></td>"
64
- out << "<td>#{h(Resque.encode(filters).gsub(/\"bus_special_value_(\w+)\"/){ "(#{$1})" }).gsub(" ", "&nbsp;").gsub('&quot;,&quot;', '&quot;, &quot;')}</td>"
64
+ out << "<td>#{h(::QueueBus::Util.encode(filters).gsub(/\"bus_special_value_(\w+)\"/){ "(#{$1})" }).gsub(" ", "&nbsp;").gsub('&quot;,&quot;', '&quot;, &quot;')}</td>"
65
65
  end
66
66
 
67
67
  if first
@@ -1,15 +1,35 @@
1
- # require 'resquebus/tasks'
2
- # will give you the resquebus tasks
3
-
1
+ # require 'resque_bus/tasks'
2
+ # will give you these tasks
4
3
 
4
+ require "queue_bus/tasks"
5
5
  require "resque/tasks"
6
+
6
7
  namespace :resquebus do
8
+ # deprecated
9
+ task :setup => ["queuebus:setup"] do
10
+ ResqueBus.note_deprecation "[DEPRECATION] rake resquebus:setup is deprecated. Use rake queuebus:setup instead."
11
+ end
12
+
13
+ task :driver => ["queuebus:driver"] do
14
+ ResqueBus.note_deprecation "[DEPRECATION] rake resquebus:driver is deprecated. Use rake queuebus:driver instead."
15
+ end
16
+
17
+ task :subscribe => ["queuebus:subscribe"] do
18
+ ResqueBus.note_deprecation "[DEPRECATION] rake resquebus:subscribe is deprecated. Use rake queuebus:subscribe instead."
19
+ end
20
+
21
+ task :unsubsribe => ["queuebus:unsubsribe"] do
22
+ ResqueBus.note_deprecation "[DEPRECATION] rake resquebus:driver is deprecated. Use rake queuebus:unsubsribe instead."
23
+ end
24
+ end
25
+
26
+ namespace :queuebus do
7
27
 
8
28
  desc "Setup will configure a resque task to run before resque:work"
9
29
  task :setup => [ :preload ] do
10
-
30
+
11
31
  if ENV['QUEUES'].nil?
12
- manager = ::ResqueBus::TaskManager.new(true)
32
+ manager = ::QueueBus::TaskManager.new(true)
13
33
  queues = manager.queue_names
14
34
  ENV['QUEUES'] = queues.join(",")
15
35
  else
@@ -23,24 +43,9 @@ namespace :resquebus do
23
43
  end
24
44
  end
25
45
 
26
- desc "Subscribes this application to ResqueBus events"
27
- task :subscribe => [ :preload ] do
28
- manager = ::ResqueBus::TaskManager.new(true)
29
- count = manager.subscribe!
30
- raise "No subscriptions created" if count == 0
31
- end
32
-
33
- desc "Unsubscribes this application from ResqueBus events"
34
- task :unsubscribe => [ :preload ] do
35
- require 'resque-bus'
36
- manager = ::ResqueBus::TaskManager.new(true)
37
- count = manager.unsubscribe!
38
- puts "No subscriptions unsubscribed" if count == 0
39
- end
40
-
41
- desc "Sets the queue to work the driver Use: `rake resquebus:driver resque:work`"
46
+ desc "Sets the queue to work the driver Use: `rake queuebus:driver resque:work`"
42
47
  task :driver => [ :preload ] do
43
- ENV['QUEUES'] = "resquebus_incoming"
48
+ ENV['QUEUES'] = ::QueueBus.incoming_queue
44
49
  end
45
50
 
46
51
  # Preload app files if this is Rails
@@ -50,32 +55,27 @@ namespace :resquebus do
50
55
  require "resque/failure/redis"
51
56
  require "resque/failure/multiple_with_retry_suppression"
52
57
 
53
- # change the namespace to be the ones used by ResqueBus
54
- # save the old one for handling later
55
- ResqueBus.original_redis = Resque.redis
56
- Resque.redis = ResqueBus.redis
57
-
58
58
  Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
59
59
  Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
60
-
60
+
61
61
  Rake::Task["resque:setup"].invoke # loads the environment and such if defined
62
62
  end
63
-
64
-
63
+
64
+
65
65
  # examples to test out the system
66
66
  namespace :example do
67
67
  desc "Publishes events to example applications"
68
- task :publish => [ "resquebus:preload", "resquebus:setup" ] do
68
+ task :publish => [ "queuebus:preload", "queuebus:setup" ] do
69
69
  which = ["one", "two", "three", "other"][rand(4)]
70
- ResqueBus.publish("event_#{which}", { "rand" => rand(99999)})
71
- ResqueBus.publish("event_all", { "rand" => rand(99999)})
72
- ResqueBus.publish("none_subscribed", { "rand" => rand(99999)})
70
+ QueueBus.publish("event_#{which}", { "rand" => rand(99999)})
71
+ QueueBus.publish("event_all", { "rand" => rand(99999)})
72
+ QueueBus.publish("none_subscribed", { "rand" => rand(99999)})
73
73
  puts "published event_#{which}, event_all, none_subscribed"
74
74
  end
75
-
75
+
76
76
  desc "Sets up an example config"
77
- task :register => [ "resquebus:preload"] do
78
- ResqueBus.dispatch("example") do
77
+ task :register => [ "queuebus:preload"] do
78
+ QueueBus.dispatch("example") do
79
79
  subscribe "event_one" do
80
80
  puts "event1 happened"
81
81
  end
@@ -93,14 +93,14 @@ namespace :resquebus do
93
93
  end
94
94
  end
95
95
  end
96
-
97
- desc "Subscribes this application to ResqueBus example events"
98
- task :subscribe => [ :register, "resquebus:subscribe" ]
99
-
100
- desc "Start a ResqueBus example worker"
101
- task :work => [ :register, "resquebus:setup", "resque:work" ]
102
-
103
- desc "Start a ResqueBus example worker"
104
- task :driver => [ :register, "resquebus:driver", "resque:work" ]
96
+
97
+ desc "Subscribes this application to QueueBus example events"
98
+ task :subscribe => [ :register, "queuebus:subscribe" ]
99
+
100
+ desc "Start a QueueBus example worker"
101
+ task :work => [ :register, "queuebus:setup", "resque:work" ]
102
+
103
+ desc "Start a QueueBus example worker"
104
+ task :driver => [ :register, "queuebus:driver", "resque:work" ]
105
105
  end
106
106
  end
@@ -1,5 +1,3 @@
1
- module Resque
2
- module Bus
3
- VERSION = "0.3.2"
4
- end
1
+ module ResqueBus
2
+ VERSION = "0.7.0"
5
3
  end
data/resque-bus.gemspec CHANGED
@@ -4,30 +4,23 @@ require "resque_bus/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "resque-bus"
7
- s.version = Resque::Bus::VERSION
7
+ s.version = ResqueBus::VERSION
8
8
  s.authors = ["Brian Leonard"]
9
9
  s.email = ["brian@bleonard.com"]
10
- s.homepage = ""
10
+ s.homepage = "https://github.com/queue-bus/resque-bus"
11
11
  s.summary = %q{A simple event bus on top of Resque}
12
- s.description = %q{A simple event bus on top of Resque.
13
- Publish and subscribe to events as they occur through a queue.}
14
-
15
- s.rubyforge_project = "resque-bus"
12
+ s.description = %q{A simple event bus on top of Resque. Publish and subscribe to events as they occur through a queue.}
16
13
 
17
14
  s.files = `git ls-files`.split("\n")
18
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
17
  s.require_paths = ["lib"]
21
18
 
22
- # specify any dependencies here; for example:
23
- # s.add_development_dependency "rspec"
24
- # s.add_runtime_dependency "rest-client"
19
+ s.add_dependency('queue-bus', ['>= 0.7', '< 1'])
25
20
  s.add_dependency('resque', ['>= 1.10.0', '< 2.0'])
26
21
  s.add_dependency('resque-scheduler', '>= 2.0.1')
27
22
  s.add_dependency('resque-retry')
28
- s.add_dependency("redis-namespace")
29
- s.add_dependency("redis")
30
-
23
+
31
24
  s.add_development_dependency("rspec")
32
25
  s.add_development_dependency("timecop")
33
26
  s.add_development_dependency("json_pure")
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Compatibility with old resque-bus" do
4
+ before(:each) do
5
+ ResqueBus.show_deprecations = false # expected
6
+
7
+ QueueBus.dispatch("r1") do
8
+ subscribe "event_name" do |attributes|
9
+ QueueBus::Runner1.run(attributes)
10
+ end
11
+ end
12
+
13
+ QueueBus::TaskManager.new(false).subscribe!
14
+
15
+ @incoming = Resque::Worker.new(:resquebus_incoming)
16
+ @incoming.register_worker
17
+
18
+ @new_incoming = Resque::Worker.new(:bus_incoming)
19
+ @new_incoming.register_worker
20
+
21
+ @rider = Resque::Worker.new(:r1_default)
22
+ @rider.register_worker
23
+ end
24
+
25
+ describe "Publisher" do
26
+ it "should still publish as expected" do
27
+ val = QueueBus.redis { |redis| redis.lpop("queue:resquebus_incoming") }
28
+ val.should == nil
29
+
30
+ args = [ "event_name", {"bus_event_type"=>"event_name", "two"=>"here", "one"=>1, "id" => 12} ]
31
+ item = {:class => "ResqueBus::Publisher", :args => args}
32
+
33
+ QueueBus.redis { |redis| redis.sadd(:queues, "resquebus_incoming") }
34
+ QueueBus.redis { |redis| redis.rpush "queue:resquebus_incoming", Resque.encode(item) }
35
+
36
+ QueueBus::Runner1.value.should == 0
37
+
38
+ perform_next_job @incoming # publish
39
+
40
+ QueueBus::Runner1.value.should == 0
41
+
42
+ perform_next_job @new_incoming # drive
43
+
44
+ QueueBus::Runner1.value.should == 0
45
+
46
+ perform_next_job @rider # ride
47
+
48
+ QueueBus::Runner1.value.should == 1
49
+
50
+ end
51
+ end
52
+
53
+ describe "Rider" do
54
+ it "should still ride as expected" do
55
+ val = QueueBus.redis { |redis| redis.lpop("queue:r1_default") }
56
+ val.should == nil
57
+
58
+ args = [ {"bus_rider_app_key"=>"r1", "x" => "y", "bus_event_type" => "event_name",
59
+ "bus_rider_sub_key"=>"event_name", "bus_rider_queue" => "default",
60
+ "bus_rider_class_name"=>"::ResqueBus::Rider"}]
61
+ item = {:class => "ResqueBus::Rider", :args => args}
62
+
63
+ QueueBus.redis { |redis| redis.sadd(:queues, "r1_default") }
64
+ QueueBus.redis { |redis| redis.rpush "queue:r1_default", Resque.encode(item) }
65
+
66
+ QueueBus::Runner1.value.should == 0
67
+
68
+ perform_next_job @rider
69
+
70
+ QueueBus::Runner1.value.should == 1
71
+ end
72
+ end
73
+
74
+ describe "Driver" do
75
+ it "should still drive as expected" do
76
+ val = QueueBus.redis { |redis| redis.lpop("queue:resquebus_incoming") }
77
+ val.should == nil
78
+
79
+ args = [ {"bus_event_type" => "event_name", "two"=>"here", "one"=>1, "id" => 12} ]
80
+ item = {:class => "ResqueBus::Driver", :args => args}
81
+
82
+ QueueBus.redis { |redis| redis.sadd(:queues, "resquebus_incoming") }
83
+ QueueBus.redis { |redis| redis.rpush "queue:resquebus_incoming", Resque.encode(item) }
84
+
85
+ QueueBus::Runner1.value.should == 0
86
+
87
+ perform_next_job @incoming
88
+
89
+ QueueBus::Runner1.value.should == 0
90
+
91
+ perform_next_job @rider
92
+
93
+ QueueBus::Runner1.value.should == 1
94
+
95
+ end
96
+ end
97
+ end