senotrusov-ruby-daemonic-threads 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +239 -0
- data/README +1 -0
- data/lib/ruby-daemonic-threads.rb +37 -0
- data/lib/ruby-daemonic-threads/config.rb +49 -0
- data/lib/ruby-daemonic-threads/daemons.rb +49 -0
- data/lib/ruby-daemonic-threads/http.rb +19 -0
- data/lib/ruby-daemonic-threads/http/daemon.rb +98 -0
- data/lib/ruby-daemonic-threads/http/request.rb +378 -0
- data/lib/ruby-daemonic-threads/http/server.rb +78 -0
- data/lib/ruby-daemonic-threads/patches/timezone.rb +37 -0
- data/lib/ruby-daemonic-threads/process.rb +53 -0
- data/lib/ruby-daemonic-threads/prototype.rb +194 -0
- data/lib/ruby-daemonic-threads/queues.rb +72 -0
- data/lib/ruby-daemonic-threads/runner.rb +134 -0
- metadata +110 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
# Copyright 2009 Stanislav Senotrusov <senotrusov@gmail.com>
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
|
17
|
+
# To reproduce race condition, start daemon and do concurrent HTTP get for some resource
|
18
|
+
# ab -c 100 -n 1000 http://127.0.0.1:4000/daemon/foo_resources/7.xml
|
19
|
+
|
20
|
+
# Rails default to lazy load this, so it gaves random exceptions in multithreaded env
|
21
|
+
ActiveSupport::TimeWithZone
|
22
|
+
|
23
|
+
# @@loaded_zones hash must be mutexed
|
24
|
+
class TZInfo::Timezone
|
25
|
+
@@loaded_zones_mutex = Mutex.new
|
26
|
+
|
27
|
+
class << self
|
28
|
+
alias_method :unsafe_get, :get
|
29
|
+
|
30
|
+
def get(identifier)
|
31
|
+
@@loaded_zones_mutex.synchronize do
|
32
|
+
unsafe_get(identifier)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
# Copyright 2009 Stanislav Senotrusov <senotrusov@gmail.com>
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
|
17
|
+
class DaemonicThreads::Process
|
18
|
+
|
19
|
+
def initialize(controller)
|
20
|
+
@controller = controller
|
21
|
+
@name = controller.name
|
22
|
+
|
23
|
+
@config = DaemonicThreads::Config.new(RAILS_ROOT + '/config/daemons.yml')
|
24
|
+
@http = DaemonicThreads::HTTP::Server.new(self)
|
25
|
+
@queues = DaemonicThreads::Queues.new(self)
|
26
|
+
@daemons = DaemonicThreads::Daemons.new(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :controller, :name, :config, :http, :queues, :daemons
|
30
|
+
|
31
|
+
def start
|
32
|
+
@queues.restore
|
33
|
+
|
34
|
+
@http.start
|
35
|
+
@daemons.start
|
36
|
+
end
|
37
|
+
|
38
|
+
def join
|
39
|
+
@http.join
|
40
|
+
@daemons.join
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop
|
44
|
+
@http.stop
|
45
|
+
@daemons.stop
|
46
|
+
end
|
47
|
+
|
48
|
+
def before_exit
|
49
|
+
@queues.store
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,194 @@
|
|
1
|
+
|
2
|
+
# Copyright 2009 Stanislav Senotrusov <senotrusov@gmail.com>
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
|
17
|
+
# Демон - экземпляр некоторого класса, запущенный в процессе.
|
18
|
+
# В одном процессе параллельно могут быть запущено произвольное число демонов.
|
19
|
+
#
|
20
|
+
# Исключение из initialize() останавливает весь процесс, потому что непонятно - как и что там запустилось и будут ли теперь работать корректно start и join.
|
21
|
+
#
|
22
|
+
# Как правило, initialize() не переопределяется, а для запуска тредов, поддемонов, сетевых подключение используется initialize_daemon().
|
23
|
+
#
|
24
|
+
# initialize_daemon() более толерантен к ошибкам.
|
25
|
+
# Исключение из группы IPSocket::SOCKET_EXEPTIONS полученное из initialize_daemon() перестартовывает демон.
|
26
|
+
# Тем не менее, остальные исключения, полученные из initialize_daemon() останавливают весь процесс
|
27
|
+
#
|
28
|
+
# Исключения, полученные из тредов, перестартовывает демон.
|
29
|
+
#
|
30
|
+
# После отработки initialize в произвольной последовательности вызываются join() и stop().
|
31
|
+
# join() и stop() вызывается только один раз.
|
32
|
+
# Исключение в stop() останавливает весь процесс, потому что непонятно - всему ли была доведена команда остановиться.
|
33
|
+
# Исключение в join() останавливает весь процесс, потому что непонятно - всё ли остановилось.
|
34
|
+
#
|
35
|
+
# Таким образом, реализуется несколько боязливая стратегия работы - если что-то идёт не так, процесс останавливается.
|
36
|
+
# Рестарт демонов происходит только когда ошибка понятна, и она, скорее всего, не приводит к фатальным последствиям.
|
37
|
+
# Такой ошибкой является сбой сети.
|
38
|
+
# Если в ваших демонах есть другие понятные вам ошибки - ловити их сами или декларируйте в RESTART_ON.
|
39
|
+
#
|
40
|
+
# Если тред запустит самостоятельно демона, то он может получить из этого демона exception.
|
41
|
+
# Если этот exception не относится к категории тех, которые решаются перезапуском демона, то весь процесс останавливается.
|
42
|
+
# На самом деле, когда вы получаете такой exception из spawn_daemon @runner.process.controller.stop уже выполнен. Вот-вот всё закроется.
|
43
|
+
|
44
|
+
|
45
|
+
# Вывод inspect очень большой, потому что он долго ходит по перекрёсным ссылкам.
|
46
|
+
# Сюрпризом будет то, что обычный exception no method error, message которого выполняет internaly some kind of inspect,
|
47
|
+
# может выполнятся секунду-другую, если exception случился паралельно.
|
48
|
+
|
49
|
+
|
50
|
+
module DaemonicThreads::Prototype
|
51
|
+
RESTART_ON = IPSocket::SOCKET_EXEPTIONS + [DaemonicThreads::MustTerminatedState] # TODO: inheritable
|
52
|
+
|
53
|
+
def initialize(name, runner, parent = nil)
|
54
|
+
@name = name
|
55
|
+
@runner = runner
|
56
|
+
@config = runner.config
|
57
|
+
@process = runner.process
|
58
|
+
@logger = Rails.logger
|
59
|
+
@parent = parent
|
60
|
+
|
61
|
+
@config["queues"].each do |queue_handler, queue_name|
|
62
|
+
instance_variable_set("@#{queue_handler}", @process.queues[queue_name])
|
63
|
+
end if @config["queues"]
|
64
|
+
|
65
|
+
@threads = ThreadGroup.new
|
66
|
+
@daemons = []
|
67
|
+
@creatures_mutex = Mutex.new
|
68
|
+
@stop_condition = ConditionVariable.new
|
69
|
+
@must_terminate = false
|
70
|
+
|
71
|
+
initialize_http if respond_to?(:initialize_http)
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :logger
|
75
|
+
|
76
|
+
def join
|
77
|
+
@creatures_mutex.synchronize do
|
78
|
+
@stop_condition.wait(@creatures_mutex) unless @must_terminate
|
79
|
+
end
|
80
|
+
|
81
|
+
deinitialize_http if respond_to?(:deinitialize_http)
|
82
|
+
|
83
|
+
@daemons.each {|daemon| daemon.join }
|
84
|
+
|
85
|
+
@threads.list.each {|thread| thread.join }
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def stop
|
90
|
+
@creatures_mutex.synchronize do
|
91
|
+
@must_terminate = true
|
92
|
+
@stop_condition.signal
|
93
|
+
end
|
94
|
+
|
95
|
+
@daemons.each {|daemon| daemon.stop }
|
96
|
+
|
97
|
+
@threads.list.each do |thread|
|
98
|
+
@config["queues"].each do |queue_handler, queue_name|
|
99
|
+
instance_variable_get("@#{queue_handler}").release_blocked thread
|
100
|
+
end
|
101
|
+
end if @config["queues"]
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def perform_initialize_daemon(*args)
|
106
|
+
initialize_daemon(*args) if respond_to? :initialize_daemon
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# Можно запускать из initialize_daemon или из любого треда
|
111
|
+
def spawn_daemon name, klass, *args
|
112
|
+
@creatures_mutex.synchronize do
|
113
|
+
raise(DaemonicThreads::MustTerminatedState, "Unable to spawn new daemons after stop() is called") if @must_terminate
|
114
|
+
|
115
|
+
# Мы не ловим никаких exceptions, потому что они поймаются или panic_on_exception (тред или http-запрос) или runner-ом (initialize, initialize_daemon).
|
116
|
+
# Полагаться на себя тот должен, кто spawn_daemon вызвал из треда, запущенного без помощи spawn_thread, а значит без должной обработки ошибок.
|
117
|
+
|
118
|
+
@daemons.push(daemon = klass.new(name, @runner, self))
|
119
|
+
daemon.perform_initialize_daemon(*args)
|
120
|
+
return daemon
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Можно запускать из initialize_daemon или из любого треда
|
126
|
+
def spawn_thread(thread_name, *args)
|
127
|
+
@creatures_mutex.synchronize do
|
128
|
+
raise(DaemonicThreads::MustTerminatedState, "Unable to spawn new threads after stop() is called") if @must_terminate
|
129
|
+
|
130
|
+
thread_title = "#{self.class}#thread:`#{thread_name}' @name:`#{@name}'"
|
131
|
+
|
132
|
+
@threads.add(thread = Thread.new { execute_thread(thread_name, thread_title, args) } )
|
133
|
+
|
134
|
+
return thread
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def execute_thread(thread_name, thread_title, args)
|
139
|
+
|
140
|
+
panic_on_exception(thread_title) do
|
141
|
+
Thread.current[:title] = thread_title
|
142
|
+
Thread.current[:started_at] = Time.now
|
143
|
+
|
144
|
+
if block_given?
|
145
|
+
yield(*args)
|
146
|
+
elsif respond_to?(thread_name)
|
147
|
+
__send__(thread_name, *args)
|
148
|
+
else
|
149
|
+
raise("Thread block was not given or method `#{thread_name}' not found. Don't know what to do.")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
ensure
|
154
|
+
panic_on_exception("#{thread_title} -- Release ActiveRecord connection to pool") { ActiveRecord::Base.clear_active_connections! }
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def panic_on_exception(title = nil, handler = nil)
|
159
|
+
yield
|
160
|
+
rescue *(RESTART_ON) => exception
|
161
|
+
begin
|
162
|
+
exception.log!(@logger, :warn, title, (@process.controller.env == "production" ? :inspect : :inspect_with_backtrace))
|
163
|
+
handler.call(exception) if handler
|
164
|
+
@runner.restart_daemon
|
165
|
+
rescue Exception => handler_exception
|
166
|
+
begin
|
167
|
+
handler_exception.log!(@logger, :fatal, title)
|
168
|
+
ensure
|
169
|
+
@process.controller.stop
|
170
|
+
end
|
171
|
+
end
|
172
|
+
rescue Exception => exception
|
173
|
+
begin
|
174
|
+
exception.log!(@logger, :fatal, title)
|
175
|
+
handler.call(exception) if handler
|
176
|
+
rescue Exception => handler_exception
|
177
|
+
handler_exception.log!(@logger, :fatal, title)
|
178
|
+
ensure
|
179
|
+
@process.controller.stop
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
def must_terminate?
|
185
|
+
@creatures_mutex.synchronize { @must_terminate }
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
def log severity, message = nil
|
190
|
+
@logger.__send__(severity, "#{self.class}##{caller.first.match(/`(.*)'/)[1]} -- #{block_given? ? yield : message}" )
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
# Copyright 2009 Stanislav Senotrusov <senotrusov@gmail.com>
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
|
17
|
+
# NOTE:
|
18
|
+
# It is not a quarantied delivery queues.
|
19
|
+
# It rely on normal process startup/shutdown sequence.
|
20
|
+
# So, if you get sigfault all data will be lost.
|
21
|
+
# On the other hand, queues tends to be quite quickly, since all done in memory
|
22
|
+
|
23
|
+
class DaemonicThreads::Queues
|
24
|
+
|
25
|
+
DEFAULT_STORAGE_DIR = Rails.root + 'tmp' + 'queues'
|
26
|
+
|
27
|
+
def initialize(process)
|
28
|
+
@queues = {}
|
29
|
+
@storage_dir = DEFAULT_STORAGE_DIR
|
30
|
+
@queue_names = process.config.queue_names
|
31
|
+
|
32
|
+
raise("Queues storage directory #{@storage_dir} is not available!") unless storage_available?
|
33
|
+
end
|
34
|
+
|
35
|
+
def restore
|
36
|
+
@queue_names.each do |name|
|
37
|
+
if File.exists?(filename = "#{@storage_dir}/#{name}")
|
38
|
+
@queues[name] = SmartQueue.new(File.read(filename))
|
39
|
+
File.unlink(filename)
|
40
|
+
else
|
41
|
+
@queues[name] = SmartQueue.new
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def store
|
47
|
+
@queues.each do |name, queue|
|
48
|
+
File.write("#{@storage_dir}/#{name}", queue.to_storage)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def [] name
|
53
|
+
@queues[name]
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def storage_available?
|
59
|
+
if File.directory?(@storage_dir) && File.writable?(@storage_dir)
|
60
|
+
return true
|
61
|
+
else
|
62
|
+
begin
|
63
|
+
Dir.mkdir(@storage_dir)
|
64
|
+
return true
|
65
|
+
rescue SystemCallError
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,134 @@
|
|
1
|
+
|
2
|
+
# Copyright 2009 Stanislav Senotrusov <senotrusov@gmail.com>
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
|
17
|
+
class DaemonicThreads::Runner
|
18
|
+
RESTART_DELAY = 15
|
19
|
+
|
20
|
+
def initialize(name, config, process)
|
21
|
+
@name = name
|
22
|
+
@config = config
|
23
|
+
@process = process
|
24
|
+
@logger = Rails.logger
|
25
|
+
|
26
|
+
@mutex = Mutex.new
|
27
|
+
@delay = ConditionVariable.new
|
28
|
+
@must_terminate = false
|
29
|
+
@restarting = false
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :name, :config, :process
|
33
|
+
|
34
|
+
def start
|
35
|
+
@watchdog_thread = Thread.new_with_exception_handling(lambda { @process.controller.stop }, @logger, :fatal, "#{self.class}#watchdog @name:`#{@name}'") { watchdog }
|
36
|
+
end
|
37
|
+
|
38
|
+
def join
|
39
|
+
@watchdog_thread.join if @watchdog_thread
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop
|
43
|
+
restart_process_on_exception do
|
44
|
+
@mutex.synchronize do
|
45
|
+
@must_terminate = true
|
46
|
+
@delay.signal
|
47
|
+
|
48
|
+
return if @restarting
|
49
|
+
|
50
|
+
@daemon.stop if @daemon
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Нельзя вызывать restart_daemon(), из initialize(), потому что перестартовывать ещё нечего.
|
57
|
+
# Такой вызов может произойти, если в initialize() запускаются треды, которые при ошибке вызывают restart_daemon.
|
58
|
+
# Запускайте новые треды в методе initialize_daemon(). Он выполняется после завершения initialize()
|
59
|
+
#
|
60
|
+
# Нельзя вызывать restart_daemon() после того, как отработал join().
|
61
|
+
# Таким действием можно непреднамеренно перезапустить новое, только что созданное приложение.
|
62
|
+
# Пауза между завершением старого и запуском нового приложения (RESTART_DELAY), несколько снижает вероятность такой ошибки.
|
63
|
+
# Польностью ошибку исключить можно, только если в join() делать join для всех запущенных тредов.
|
64
|
+
# Это делается автоматически, если использовать методы spawn_daemon и spawn_thread.
|
65
|
+
#
|
66
|
+
def restart_daemon
|
67
|
+
restart_process_on_exception do
|
68
|
+
@mutex.synchronize do
|
69
|
+
return if @must_terminate || @restarting
|
70
|
+
|
71
|
+
@restarting = true
|
72
|
+
|
73
|
+
unless @daemon
|
74
|
+
raise "#{self.class}#restart_daemon @name:`#{@name} -- Called restart_daemon(), but @daemon does not exists! This may occur when you somehow call restart_daemon() from initialize() and then raises then exception from initialize(). Or you spawn threads in initialize() instead of in spawn_threads() and one thread ends with exception before initialize() fully completes. Or it may be both cases."
|
75
|
+
end
|
76
|
+
|
77
|
+
@daemon.stop
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def watchdog
|
85
|
+
loop do
|
86
|
+
@mutex.synchronize do
|
87
|
+
return if @must_terminate
|
88
|
+
@restarting = false
|
89
|
+
|
90
|
+
@logger.info "#{self.class}#watchdog @name:`#{@name}' -- Starting..."
|
91
|
+
@daemon = @config["class-constantized"].new(@name, self)
|
92
|
+
end
|
93
|
+
|
94
|
+
begin
|
95
|
+
@daemon.perform_initialize_daemon
|
96
|
+
rescue *(@config["class-constantized"]::RESTART_ON) => exception
|
97
|
+
exception.log! @logger, :warn, "#{self.class}#watchdog @name:`#{@name}' -- Restarting daemon because of exception", (@process.controller.env == "production" ? :inspect : :inspect_with_backtrace)
|
98
|
+
restart_daemon
|
99
|
+
end
|
100
|
+
|
101
|
+
@daemon.join
|
102
|
+
|
103
|
+
delay
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def delay
|
108
|
+
@mutex.synchronize do
|
109
|
+
unless @must_terminate
|
110
|
+
@logger.info "#{self.class}#delay @name:`#{@name}' -- Catch termination, restarting in #{RESTART_DELAY} seconds..."
|
111
|
+
|
112
|
+
Thread.new_with_exception_handling(lambda { @process.controller.stop }, @logger, :fatal, "#{self.class}#delay @name:`#{@name}'") do
|
113
|
+
# Если кто-то пошлёт сигнал во время сна, ожидающий получит два сигнала по пробуждении. В этом конкретном случае это не ппроблема, потому как этот кто-то - это стоп, и после него получать сигнал будет некому
|
114
|
+
sleep RESTART_DELAY
|
115
|
+
@delay.signal
|
116
|
+
end
|
117
|
+
|
118
|
+
@delay.wait(@mutex)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def restart_process_on_exception
|
124
|
+
yield
|
125
|
+
rescue Exception => exception
|
126
|
+
begin
|
127
|
+
exception.log!(@logger, :fatal, "#{self.class}##{caller.first.match(/`(.*)'/)[1]} @name:`#{@name}' -- Stopping process because of exception")
|
128
|
+
ensure
|
129
|
+
@process.controller.stop
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|