sanger_warren 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +39 -0
- data/.rubocop.yml +11 -5
- data/.yardopts +3 -0
- data/CHANGELOG.md +34 -1
- data/Gemfile +6 -1
- data/Gemfile.lock +71 -39
- data/README.md +133 -43
- data/bin/console +3 -6
- data/bin/warren +6 -0
- data/lefthook.yml +53 -0
- data/lib/sanger_warren.rb +8 -0
- data/lib/warren.rb +49 -4
- data/lib/warren/app.rb +9 -0
- data/lib/warren/app/cli.rb +35 -0
- data/lib/warren/app/config.rb +110 -0
- data/lib/warren/app/consumer.rb +65 -0
- data/lib/warren/app/consumer_add.rb +131 -0
- data/lib/warren/app/consumer_start.rb +40 -0
- data/lib/warren/app/exchange_config.rb +151 -0
- data/lib/warren/app/templates/subscriber.tt +32 -0
- data/lib/warren/callback.rb +2 -7
- data/lib/warren/callback/broadcast_with_warren.rb +1 -1
- data/lib/warren/client.rb +111 -0
- data/lib/warren/config/consumers.rb +123 -0
- data/lib/warren/delay_exchange.rb +85 -0
- data/lib/warren/den.rb +93 -0
- data/lib/warren/exceptions.rb +15 -0
- data/lib/warren/fox.rb +165 -0
- data/lib/warren/framework_adaptor/rails_adaptor.rb +135 -0
- data/lib/warren/handler.rb +16 -0
- data/lib/warren/handler/base.rb +20 -0
- data/lib/warren/handler/broadcast.rb +54 -18
- data/lib/warren/handler/log.rb +50 -10
- data/lib/warren/handler/test.rb +101 -14
- data/lib/warren/helpers/state_machine.rb +55 -0
- data/lib/warren/log_tagger.rb +58 -0
- data/lib/warren/message.rb +7 -5
- data/lib/warren/message/full.rb +20 -0
- data/lib/warren/message/short.rb +49 -4
- data/lib/warren/message/simple.rb +15 -0
- data/lib/warren/railtie.rb +12 -0
- data/lib/warren/subscriber/base.rb +151 -0
- data/lib/warren/subscription.rb +78 -0
- data/lib/warren/version.rb +2 -1
- data/sanger-warren.gemspec +5 -4
- metadata +49 -6
- data/.travis.yml +0 -6
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Configures and wraps up delay exchange on a Bunny Channel/Queue
|
5
|
+
# A delay exchange routes immediately onto a queue with a ttl
|
6
|
+
# once messages on this queue expire they are dead-lettered back onto
|
7
|
+
# to original exchange
|
8
|
+
# Note: This does not currently support the rabbitmq-delayed-message-exchange
|
9
|
+
# plugin.
|
10
|
+
class DelayExchange
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :channel
|
14
|
+
|
15
|
+
#
|
16
|
+
# Create a new delay exchange. Handles queue creation, binding and attaching
|
17
|
+
# consumers to the queues
|
18
|
+
#
|
19
|
+
# @param channel [Warren::Handler::Broadcast::Channel] A channel on which to register queues
|
20
|
+
# @param config [Hash] queue configuration hash
|
21
|
+
#
|
22
|
+
def initialize(channel:, config:)
|
23
|
+
@channel = channel
|
24
|
+
@exchange_config = config&.fetch('exchange', nil)
|
25
|
+
@bindings = config&.fetch('bindings', [])
|
26
|
+
end
|
27
|
+
|
28
|
+
def_delegators :channel, :nack, :ack
|
29
|
+
|
30
|
+
# Ensures the queues and channels are set up to receive messages
|
31
|
+
# keys: additional routing_keys to bind
|
32
|
+
def activate!
|
33
|
+
establish_bindings!
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Post a message to the delay exchange.
|
38
|
+
#
|
39
|
+
# @param payload [String] The message payload
|
40
|
+
# @param routing_key [String] The routing key of the re-sent message
|
41
|
+
# @param headers [Hash] A hash of headers. Typically: { attempts: <Integer> }
|
42
|
+
# @option headers [Integer] :attempts The number of times the message has been processed
|
43
|
+
#
|
44
|
+
# @return [Void]
|
45
|
+
#
|
46
|
+
def publish(payload, routing_key:, headers: {})
|
47
|
+
raise StandardError, 'No delay queue configured' unless configured?
|
48
|
+
|
49
|
+
message = Warren::Message::Simple.new(routing_key, payload, headers)
|
50
|
+
channel.publish(message, exchange: exchange)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def configured?
|
56
|
+
@exchange_config&.key?('name')
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_binding(queue, options)
|
60
|
+
queue.bind(exchange, options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def exchange
|
64
|
+
@exchange ||= channel.exchange(*@exchange_config.values_at('name', 'options'))
|
65
|
+
end
|
66
|
+
|
67
|
+
def queue(config)
|
68
|
+
channel.queue(*config.values_at('name', 'options'))
|
69
|
+
end
|
70
|
+
|
71
|
+
def establish_bindings!
|
72
|
+
@bindings.each do |binding_config|
|
73
|
+
queue = queue(binding_config['queue'])
|
74
|
+
transformed_options = merge_routing_key_prefix(binding_config['options'])
|
75
|
+
add_binding(queue, transformed_options)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def merge_routing_key_prefix(options)
|
80
|
+
options.transform_values do |value|
|
81
|
+
format(value, routing_key_prefix: channel.routing_key_prefix)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/warren/den.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bunny'
|
4
|
+
require 'warren/fox'
|
5
|
+
require 'warren/subscription'
|
6
|
+
require 'warren/delay_exchange'
|
7
|
+
|
8
|
+
module Warren
|
9
|
+
# A Den is in charge of creating a Fox from a consumer configuration
|
10
|
+
# It handles the registration of dead-letter queues, and configuration of
|
11
|
+
# {Warren::Subscription subscriptions} and
|
12
|
+
# {Warren::DelayExchange delay exchanges}
|
13
|
+
class Den
|
14
|
+
# The number of simultaneous workers generated by default
|
15
|
+
DEFAULT_WORKER_COUNT = 3
|
16
|
+
|
17
|
+
#
|
18
|
+
# Create a {Warren::Fox} work pool.
|
19
|
+
# @param app_name [String] The name of the application. Corresponds to the
|
20
|
+
# subscriptions config in `config/warren.yml`
|
21
|
+
# @param config [Warren::Config::Consumers] A configuration object, loaded from `config/warren.yml` by default
|
22
|
+
# @param adaptor [#recovered?,#handle,#env] An adaptor to handle framework specifics
|
23
|
+
def initialize(app_name, config, adaptor:)
|
24
|
+
@app_name = app_name
|
25
|
+
@config = config
|
26
|
+
@fox = nil
|
27
|
+
@adaptor = adaptor
|
28
|
+
end
|
29
|
+
|
30
|
+
def fox
|
31
|
+
@fox ||= spawn_fox
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Ensures the dead_letter queues and exchanges are registered.
|
36
|
+
#
|
37
|
+
# @return [Void]
|
38
|
+
def register_dead_letter_queues
|
39
|
+
config = dead_letter_config
|
40
|
+
return unless config
|
41
|
+
|
42
|
+
Warren.handler.with_channel do |channel|
|
43
|
+
subscription = Warren::Subscription.new(channel: channel, config: config)
|
44
|
+
subscription.activate!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def consumer_config
|
51
|
+
@config.consumer(@app_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Spawn a new fox
|
56
|
+
#
|
57
|
+
# @return [Warren::Fox]
|
58
|
+
def spawn_fox
|
59
|
+
# We don't use with_channel as our consumer persists outside the block,
|
60
|
+
# and while we *can* share channels between consumers it results in them
|
61
|
+
# sharing the same worker pool. This process lets us control workers on
|
62
|
+
# a per-queue basis. Currently that just means one worker per consumer.
|
63
|
+
channel = Warren.handler.new_channel(worker_count: worker_count)
|
64
|
+
subscription = Warren::Subscription.new(channel: channel, config: queue_config)
|
65
|
+
delay = Warren::DelayExchange.new(channel: channel, config: delay_config)
|
66
|
+
Warren::Fox.new(name: @app_name,
|
67
|
+
subscription: subscription,
|
68
|
+
adaptor: @adaptor,
|
69
|
+
subscribed_class: subscribed_class,
|
70
|
+
delayed: delay)
|
71
|
+
end
|
72
|
+
|
73
|
+
def worker_count
|
74
|
+
consumer_config.fetch('worker_count', DEFAULT_WORKER_COUNT)
|
75
|
+
end
|
76
|
+
|
77
|
+
def queue_config
|
78
|
+
consumer_config.fetch('queue')
|
79
|
+
end
|
80
|
+
|
81
|
+
def dead_letter_config
|
82
|
+
consumer_config.fetch('dead_letters')
|
83
|
+
end
|
84
|
+
|
85
|
+
def delay_config
|
86
|
+
consumer_config.fetch('delay', nil)
|
87
|
+
end
|
88
|
+
|
89
|
+
def subscribed_class
|
90
|
+
Object.const_get(consumer_config.fetch('subscribed_class'))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Exceptions used by the warren gem
|
5
|
+
module Exceptions
|
6
|
+
# raise {Warren::Exceptions::TemporaryIssue} in a {Warren::Subscriber} to
|
7
|
+
# nack the message, requeuing it, and sending the consumers into sleep
|
8
|
+
# mode until the issue resolves itself.
|
9
|
+
TemporaryIssue = Class.new(StandardError)
|
10
|
+
|
11
|
+
# {Warren::Exceptions::Exceptions::MultipleAcknowledgements} is raised if a message
|
12
|
+
# is acknowledged, or rejected (nacked) multiple times.
|
13
|
+
MultipleAcknowledgements = Class.new(StandardError)
|
14
|
+
end
|
15
|
+
end
|
data/lib/warren/fox.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'bunny'
|
5
|
+
require 'warren'
|
6
|
+
require 'warren/helpers/state_machine'
|
7
|
+
require 'warren/subscriber/base'
|
8
|
+
require 'warren/log_tagger'
|
9
|
+
require 'warren/framework_adaptor/rails_adaptor'
|
10
|
+
|
11
|
+
module Warren
|
12
|
+
# A fox is a rabbitMQ consumer. It handles subscription to the queue
|
13
|
+
# and passing message on to the registered Subscriber
|
14
|
+
class Fox
|
15
|
+
# A little cute fox emoji to easily flag output from the consumers
|
16
|
+
FOX = '🦊'
|
17
|
+
|
18
|
+
extend Forwardable
|
19
|
+
extend Warren::Helpers::StateMachine
|
20
|
+
# Maximum wait time between database retries: 5 minutes
|
21
|
+
MAX_RECONNECT_DELAY = 60 * 5
|
22
|
+
|
23
|
+
attr_reader :state, :subscription, :consumer_tag, :delayed
|
24
|
+
|
25
|
+
#
|
26
|
+
# Creates a fox, a RabbitMQ consumer.
|
27
|
+
# Subscribes to the queues defined in `subscription`
|
28
|
+
# and passes messages on to the subscriber
|
29
|
+
#
|
30
|
+
# @param name [String] The name of the consumer
|
31
|
+
# @param subscription [Warren::Subscription] Describes the queue to subscribe to
|
32
|
+
# @param adaptor [#recovered?,#handle,#env] An adaptor to handle framework specifics
|
33
|
+
# @param subscribed_class [Warren::Subscriber::Base] The class to process received messages
|
34
|
+
# @param delayed [Warren::DelayExchange] The details handling delayed message broadcast
|
35
|
+
#
|
36
|
+
def initialize(name:, subscription:, adaptor:, subscribed_class:, delayed:)
|
37
|
+
@consumer_tag = "#{adaptor.env}_#{name}_#{Process.pid}"
|
38
|
+
@subscription = subscription
|
39
|
+
@delayed = delayed
|
40
|
+
@logger = Warren::LogTagger.new(logger: adaptor.logger, tag: "#{FOX} #{@consumer_tag}")
|
41
|
+
@adaptor = adaptor
|
42
|
+
@subscribed_class = subscribed_class
|
43
|
+
@state = :initialized
|
44
|
+
end
|
45
|
+
|
46
|
+
states :stopping, :stopped, :paused, :starting, :started, :running
|
47
|
+
def_delegators :@logger, :warn, :info, :error, :debug
|
48
|
+
|
49
|
+
#
|
50
|
+
# Starts up the fox, automatically registering the configured queues and bindings
|
51
|
+
# before subscribing to the queue.
|
52
|
+
#
|
53
|
+
# @return [Void]
|
54
|
+
#
|
55
|
+
def run!
|
56
|
+
starting!
|
57
|
+
subscription.activate! # Set up the queues
|
58
|
+
delayed.activate!
|
59
|
+
running! # Transition to running state
|
60
|
+
subscribe! # Subscribe to the queue
|
61
|
+
|
62
|
+
info { 'Started consumer' }
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Stop the consumer and unsubscribes from the queue. Blocks until fully unsubscribed.
|
67
|
+
#
|
68
|
+
# @return [Void]
|
69
|
+
#
|
70
|
+
def stop!
|
71
|
+
info { 'Stopping consumer' }
|
72
|
+
stopping!
|
73
|
+
unsubscribe!
|
74
|
+
info { 'Stopped consumer' }
|
75
|
+
stopped!
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Temporarily unsubscribes the consumer, and schedules an attempted recovery.
|
80
|
+
# Recovery is triggered by the {#attempt_recovery} method which gets called
|
81
|
+
# periodically by {Warren::Client}
|
82
|
+
#
|
83
|
+
# @return [Void]
|
84
|
+
#
|
85
|
+
def pause!
|
86
|
+
return unless running?
|
87
|
+
|
88
|
+
unsubscribe!
|
89
|
+
@recovery_attempts = 0
|
90
|
+
@recover_at = Time.now
|
91
|
+
paused!
|
92
|
+
end
|
93
|
+
|
94
|
+
# If the fox is paused, and a recovery attempt is scheduled, will prompt
|
95
|
+
# the framework adaptor to attempt to recover. (Such as reconnecting to the
|
96
|
+
# database). If this operation is successful will resubscribe to the queue,
|
97
|
+
# otherwise a further recovery attempt will be scheduled. Successive recovery
|
98
|
+
# attempts will be gradually further apart, up to the MAX_RECONNECT_DELAY
|
99
|
+
# of 5 minutes.
|
100
|
+
def attempt_recovery
|
101
|
+
return unless paused? && recovery_due?
|
102
|
+
|
103
|
+
warn { "Attempting recovery: #{@recovery_attempts}" }
|
104
|
+
if recovered?
|
105
|
+
running!
|
106
|
+
subscribe!
|
107
|
+
else
|
108
|
+
@recovery_attempts += 1
|
109
|
+
@recover_at = Time.now + delay_for_attempt
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Our consumer operates in another thread. It is non blocking.
|
116
|
+
def subscribe!
|
117
|
+
raise StandardError, 'Consumer already exists' unless @consumer.nil?
|
118
|
+
|
119
|
+
@consumer = @subscription.subscribe(@consumer_tag) do |delivery_info, properties, payload|
|
120
|
+
process(delivery_info, properties, payload)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Cancels the consumer and un-registers it
|
125
|
+
def unsubscribe!
|
126
|
+
info { 'Unsubscribing' }
|
127
|
+
@consumer&.cancel
|
128
|
+
@consumer = nil
|
129
|
+
info { 'Unsubscribed' }
|
130
|
+
end
|
131
|
+
|
132
|
+
def delay_for_attempt
|
133
|
+
[2**@recovery_attempts, MAX_RECONNECT_DELAY].min
|
134
|
+
end
|
135
|
+
|
136
|
+
def recovery_due?
|
137
|
+
Time.now > @recover_at
|
138
|
+
end
|
139
|
+
|
140
|
+
def recovered?
|
141
|
+
@adaptor.recovered?
|
142
|
+
end
|
143
|
+
|
144
|
+
def process(delivery_info, properties, payload)
|
145
|
+
log_message(payload) do
|
146
|
+
message = @subscribed_class.new(self, delivery_info, properties, payload)
|
147
|
+
@adaptor.handle { message._process_ }
|
148
|
+
rescue Warren::Exceptions::TemporaryIssue => e
|
149
|
+
warn { "Temporary Issue: #{e.message}" }
|
150
|
+
pause!
|
151
|
+
message.requeue(e)
|
152
|
+
rescue StandardError => e
|
153
|
+
message.dead_letter(e)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def log_message(payload)
|
158
|
+
debug { 'Started message process' }
|
159
|
+
debug { payload }
|
160
|
+
yield
|
161
|
+
ensure
|
162
|
+
debug { 'Finished message process' }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Namespace for framework adaptors.
|
5
|
+
#
|
6
|
+
# A FrameworkAdaptor should implement the following instance methods:
|
7
|
+
#
|
8
|
+
# == recovered? => Bool
|
9
|
+
# Indicates that any temporary issues (such as database connectivity problems)
|
10
|
+
# are resolved and consumers may restart.
|
11
|
+
#
|
12
|
+
# == handle
|
13
|
+
#
|
14
|
+
# Wraps the processing of each message, is expected to `yield` to allow
|
15
|
+
# processing. May be responsible for handling connection pools, and
|
16
|
+
# framework-specific exceptions. Raising {Warren::Exceptions::TemporaryIssue}
|
17
|
+
# here will cause consumers to sleep until `recovered?` returns true.
|
18
|
+
#
|
19
|
+
# == env => String
|
20
|
+
#
|
21
|
+
# Returns the current environment of the application.
|
22
|
+
#
|
23
|
+
# == logger => Logger
|
24
|
+
#
|
25
|
+
# Returns your application logger. Is expected to be compatible with the
|
26
|
+
# standard library Logger class.
|
27
|
+
# @see https://ruby-doc.org/stdlib-2.7.0/libdoc/logger/rdoc/Logger.html
|
28
|
+
#
|
29
|
+
# == load_application
|
30
|
+
#
|
31
|
+
# Called upon running `warren consumer start`. Should ensure your application
|
32
|
+
# is correctly loaded sufficiently for processing messages
|
33
|
+
#
|
34
|
+
module FrameworkAdaptor
|
35
|
+
# The RailsAdaptor provides error handling and application
|
36
|
+
# loading for Rails applications
|
37
|
+
class RailsAdaptor
|
38
|
+
# Matches errors associated with database connection loss.
|
39
|
+
# To understand exactly how this works, we need to go under the hood of
|
40
|
+
# `rescue`.
|
41
|
+
# When an exception is raised in Ruby, the interpreter begins unwinding
|
42
|
+
# the stack, looking for `rescue` statements. For each one it
|
43
|
+
# finds it performs the check `ExceptionClass === raised_exception`,
|
44
|
+
# and if this returns true, it enters the rescue block, otherwise it
|
45
|
+
# continues unwinding the stack.
|
46
|
+
# Under normal circumstances Class#=== returns true for instances of that
|
47
|
+
# class. Here we override that behaviour and explicitly check for a
|
48
|
+
# database connection instead. This ensures that regardless of what
|
49
|
+
# exception gets thrown if we loose access to the database, we correctly
|
50
|
+
# handle the message
|
51
|
+
class ConnectionMissing
|
52
|
+
def self.===(_)
|
53
|
+
# We used to inspect the exception, and try and check it against a list
|
54
|
+
# of errors that might indicate connectivity issues. But this list
|
55
|
+
# just grew and grew over time. So instead we just explicitly check
|
56
|
+
# the outcome
|
57
|
+
!ActiveRecord::Base.connection.active?
|
58
|
+
rescue StandardError => _e
|
59
|
+
# Unfortunately ActiveRecord::Base.connection.active? can throw an
|
60
|
+
# exception if it is unable to connect, and furthermore the class
|
61
|
+
# depends on the adapter used.
|
62
|
+
true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Checks that the database has recovered to allow message processing
|
68
|
+
#
|
69
|
+
# @return [Bool] Returns true if the application has recovered
|
70
|
+
#
|
71
|
+
def recovered?
|
72
|
+
ActiveRecord::Base.connection.reconnect!
|
73
|
+
true
|
74
|
+
rescue StandardError
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Checks ensures a database connection has been checked out before
|
80
|
+
# yielding to allow message processing. Rescues loss of the database
|
81
|
+
# connection and raises {Warren::Exceptions::TemporaryIssue} to send
|
82
|
+
# the consumers to sleep until it recovers.
|
83
|
+
#
|
84
|
+
# @return [Void]
|
85
|
+
#
|
86
|
+
def handle
|
87
|
+
with_connection do
|
88
|
+
yield
|
89
|
+
rescue ConnectionMissing => e
|
90
|
+
raise Warren::Exceptions::TemporaryIssue, e.message
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def with_connection
|
95
|
+
begin
|
96
|
+
ActiveRecord::Base.connection
|
97
|
+
rescue StandardError => e
|
98
|
+
raise Warren::Exceptions::TemporaryIssue, e.message
|
99
|
+
end
|
100
|
+
|
101
|
+
yield
|
102
|
+
ensure
|
103
|
+
ActiveRecord::Base.clear_active_connections!
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the rails environment
|
107
|
+
#
|
108
|
+
# @return [ActiveSupport::StringInquirer] The rails environment
|
109
|
+
def env
|
110
|
+
Rails.env
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the configured logger
|
114
|
+
#
|
115
|
+
# @return [Logger,ActiveSupport::Logger,...] The application logger
|
116
|
+
def logger
|
117
|
+
Rails.logger
|
118
|
+
end
|
119
|
+
|
120
|
+
# Triggers full loading of the rails application and dependencies
|
121
|
+
#
|
122
|
+
# @return [Void]
|
123
|
+
def load_application
|
124
|
+
$stdout.puts 'Loading application...'
|
125
|
+
require './config/environment'
|
126
|
+
Warren.load_configuration
|
127
|
+
$stdout.puts 'Loaded!'
|
128
|
+
rescue LoadError
|
129
|
+
# Need to work out an elegant way to handle non-rails
|
130
|
+
# apps
|
131
|
+
$stdout.puts 'Could not auto-load application'
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|