KeeperPat-feedupdater 0.2.6

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.
@@ -0,0 +1,717 @@
1
+ #--
2
+ # Copyright (c) 2005 Robert Aman
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'feed_updater/version'
25
+
26
+ # :stopdoc:
27
+ # ROFLCOPTERS?
28
+ # This mainly exists because of the approximately 1 billion different
29
+ # ways to deploy this script.
30
+ if !defined?(FileStalker)
31
+ class FileStalker
32
+ def self.hunt(paths)
33
+ for path in paths
34
+ if File.exists?(File.expand_path(path))
35
+ return File.expand_path(path)
36
+ elsif File.exists?(File.expand_path(
37
+ File.dirname(__FILE__) + path))
38
+ return File.expand_path(File.dirname(__FILE__) + path)
39
+ elsif File.exists?(File.expand_path(
40
+ File.dirname(__FILE__) + "/" + path))
41
+ return File.expand_path(File.dirname(__FILE__) + "/" + path)
42
+ end
43
+ end
44
+ return nil
45
+ end
46
+ end
47
+ end
48
+ # :startdoc:
49
+
50
+ $:.unshift(File.dirname(__FILE__))
51
+ $:.unshift(File.dirname(__FILE__) + "/feed_updater/vendor")
52
+
53
+ # Make it so Rubygems can find the executable
54
+ $:.unshift(File.expand_path(File.dirname(__FILE__) + "/../bin"))
55
+
56
+ require 'rubygems'
57
+
58
+ if !defined?(Daemons)
59
+ begin
60
+ gem('daemons', '>= 0.4.4')
61
+ require('daemons')
62
+ rescue LoadError
63
+ require 'daemons'
64
+ end
65
+ end
66
+
67
+ if !defined?(FeedTools::FEED_TOOLS_VERSION)
68
+ begin
69
+ if File.exists?(File.expand_path(
70
+ File.dirname(__FILE__) + "/../../feedtools/lib"))
71
+ $:.unshift(File.expand_path(
72
+ File.dirname(__FILE__) + "/../../feedtools/lib"))
73
+ require("feed_tools")
74
+ else
75
+ gem('feedtools', '>= 0.2.23')
76
+ require('feedtools')
77
+ end
78
+ rescue LoadError
79
+ require 'feed_tools'
80
+ end
81
+ end
82
+
83
+ require 'benchmark'
84
+ require 'thread'
85
+ require 'logger'
86
+
87
+ class FeedUpdaterLogger < Logger
88
+ attr_accessor :prefix
89
+
90
+ alias_method :old_log, :log
91
+ def log(level, message)
92
+ if defined?(@prefix) && @prefix != nil
93
+ self.old_log(level, "#{self.prefix}#{message}")
94
+ else
95
+ self.old_log(level, message)
96
+ end
97
+ end
98
+
99
+ def debug(message)
100
+ self.log(0, message)
101
+ end
102
+
103
+ def info(message)
104
+ self.log(1, message)
105
+ end
106
+
107
+ def warn(message)
108
+ self.log(2, message)
109
+ end
110
+
111
+ def error(message)
112
+ self.log(3, message)
113
+ end
114
+
115
+ def fatal(message)
116
+ self.log(4, message)
117
+ end
118
+ end
119
+
120
+ module FeedTools
121
+ # A simple daemon for scheduled updating of feeds.
122
+ class FeedUpdater
123
+ # Declares an on_begin event. The given block will be called before the
124
+ # update sequence runs to allow for any setup required. The block is
125
+ # not passed any arguments.
126
+ def self.on_begin(&block)
127
+ raise "No block supplied for on_begin." if block.nil?
128
+ @@on_begin = block
129
+ end
130
+
131
+ # Declares an on_update event. The given block will be called after
132
+ # every feed update. The block is passed the feed object that was loaded
133
+ # and the time it took in seconds to successfully load it.
134
+ def self.on_update(&block)
135
+ raise "No block supplied for on_update." if block.nil?
136
+ @@on_update = block
137
+ end
138
+
139
+ # Declares an on_error event. The given block will be called after
140
+ # an error occurs during a feed update. The block is passed the href of
141
+ # the feed that errored out, and the exception object that was raised.
142
+ def self.on_error(&block)
143
+ raise "No block supplied for on_error." if block.nil?
144
+ @@on_error = block
145
+ end
146
+
147
+ # Declares an on_complete event. The given block will be called after
148
+ # all feeds have been updated. The block is passed a list of feeds that
149
+ # FeedUpdater attempted to update.
150
+ def self.on_complete(&block)
151
+ raise "No block supplied for on_complete." if block.nil?
152
+ @@on_complete = block
153
+ end
154
+
155
+ # Returns the initial directory that the daemon was started from.
156
+ def initial_directory()
157
+ @initial_directory = nil if !defined?(@initial_directory)
158
+ return @initial_directory
159
+ end
160
+
161
+ # Returns the directory where the pid files are stored.
162
+ def pid_file_dir()
163
+ @pid_file_dir = nil if !defined?(@pid_file_dir)
164
+ return @pid_file_dir
165
+ end
166
+
167
+ # Sets the directory where the pid files are stored.
168
+ def pid_file_dir=(new_pid_file_dir)
169
+ @pid_file_dir = new_pid_file_dir
170
+ end
171
+
172
+ # Returns the directory where the log files are stored.
173
+ def log_file_dir()
174
+ @log_file_dir = nil if !defined?(@log_file_dir)
175
+ return @log_file_dir
176
+ end
177
+
178
+ # Sets the directory where the log files are stored.
179
+ def log_file_dir=(new_log_file_dir)
180
+ @log_file_dir = new_log_file_dir
181
+ end
182
+
183
+ # Returns the path to the log file.
184
+ def log_file()
185
+ if !defined?(@log_file) || @log_file.nil?
186
+ if self.log_file_dir.nil?
187
+ @log_file = File.expand_path("./feed_updater.log")
188
+ else
189
+ @log_file = File.expand_path(
190
+ self.log_file_dir + "/feed_updater.log")
191
+ end
192
+ end
193
+ return @log_file
194
+ end
195
+
196
+ # Returns the logger object.
197
+ def logger()
198
+ if !defined?(@logger) || @logger.nil?
199
+ @logger = FeedUpdaterLogger.new(self.log_file)
200
+ @logger.level = self.updater_options[:log_level]
201
+ @logger.datetime_format = nil
202
+ @logger.progname = nil
203
+ @logger.prefix = "FeedUpdater".ljust(20)
204
+ end
205
+ return @logger
206
+ end
207
+
208
+ # Restarts the logger object. This needs to be done after the program
209
+ # forks.
210
+ def restart_logger()
211
+ begin
212
+ self.logger.close()
213
+ rescue IOError
214
+ end
215
+ @logger = FeedUpdaterLogger.new(self.log_file)
216
+ @logger.level = self.updater_options[:log_level]
217
+ @logger.datetime_format = nil
218
+ @logger.progname = nil
219
+ @logger.prefix = "FeedUpdater".ljust(20)
220
+ end
221
+
222
+ # Returns a list of feeds to be updated.
223
+ def feed_href_list()
224
+ if !defined?(@feed_href_list) || @feed_href_list.nil?
225
+ @feed_href_list_override = false
226
+ if self.updater_options.nil?
227
+ @feed_href_list = nil
228
+ elsif self.updater_options[:feed_href_list].kind_of?(Array)
229
+ @feed_href_list = self.updater_options[:feed_href_list]
230
+ else
231
+ @feed_href_list = nil
232
+ end
233
+ end
234
+ return @feed_href_list
235
+ end
236
+
237
+ # Sets a list of feeds to be updated.
238
+ def feed_href_list=(new_feed_href_list)
239
+ @feed_href_list_override = true
240
+ @feed_href_list = new_feed_href_list
241
+ end
242
+
243
+ # Returns either :running or :stopped depending on the daemon's current
244
+ # status.
245
+ def status()
246
+ @status = :stopped if @status.nil?
247
+ return @status
248
+ end
249
+
250
+ # Returns a hash of the currently set updater options.
251
+ def updater_options()
252
+ if !defined?(@updater_options) || @updater_options.nil?
253
+ @updater_options = {
254
+ :start_delay => true,
255
+ :threads => 1,
256
+ :sleep_time => 60,
257
+ :log_level => 0
258
+ }
259
+ end
260
+ return @updater_options
261
+ end
262
+
263
+ # Returns a hash of the currently set daemon options.
264
+ def daemon_options()
265
+ if !defined?(@daemon_options) || @daemon_options.nil?
266
+ @daemon_options = {
267
+ :app_name => "feed_updater_daemon",
268
+ :dir_mode => :normal,
269
+ :dir => self.pid_file_dir,
270
+ :backtrace => true,
271
+ :ontop => false
272
+ }
273
+ end
274
+ @daemon_options[:dir] = self.pid_file_dir
275
+ return @daemon_options
276
+ end
277
+
278
+ # Returns a reference to the daemon application.
279
+ def application()
280
+ @application = nil if !defined?(@application)
281
+ return @application
282
+ end
283
+
284
+ # Returns the process id of the daemon. This should return nil if the
285
+ # daemon is not running.
286
+ def pid()
287
+ if File.exists?(File.expand_path(pid_file_dir + "/feed_updater.pid"))
288
+ begin
289
+ pid_file = File.open(
290
+ File.expand_path(pid_file_dir + "/feed_updater.pid"), "r")
291
+ return pid_file.read.to_s.strip.to_i
292
+ rescue Exception
293
+ return nil
294
+ end
295
+ else
296
+ return nil if self.application.nil?
297
+ begin
298
+ return self.application.pid.pid
299
+ rescue Exception
300
+ return nil
301
+ end
302
+ end
303
+ end
304
+
305
+ def cloaker(&blk) #:nodoc:
306
+ (class << self; self; end).class_eval do
307
+ define_method(:cloaker_, &blk)
308
+ meth = instance_method(:cloaker_)
309
+ remove_method(:cloaker_)
310
+ meth
311
+ end
312
+ end
313
+ protected :cloaker
314
+
315
+ # Starts the daemon.
316
+ def start()
317
+ self.logger.prefix = "FeedUpdater".ljust(20)
318
+ if !defined?(@@on_update) || @@on_update.nil?
319
+ raise "No on_update handler block given."
320
+ end
321
+ if self.status == :running || self.pid != nil
322
+ puts "FeedUpdater is already running."
323
+ return self.pid
324
+ end
325
+ if defined?(ActiveRecord)
326
+ ActiveRecord::Base.allow_concurrency = true
327
+ end
328
+
329
+ @initial_directory = File.expand_path(".")
330
+
331
+ if @application.nil?
332
+ self.logger()
333
+ feed_update_proc = lambda do
334
+ # Reestablish correct location
335
+ Dir.chdir(File.expand_path(self.initial_directory))
336
+
337
+ sleep(1)
338
+
339
+ self.restart_logger()
340
+ self.logger.info("Using environment: #{FEED_TOOLS_ENV}")
341
+
342
+ if FeedTools.configurations[:feed_cache].nil?
343
+ FeedTools.configurations[:feed_cache] =
344
+ "FeedTools::DatabaseFeedCache"
345
+ end
346
+
347
+ if !FeedTools.feed_cache.connected?
348
+ FeedTools.configurations[:feed_cache] =
349
+ "FeedTools::DatabaseFeedCache"
350
+ FeedTools.feed_cache.initialize_cache()
351
+ begin
352
+ ActiveRecord::Base.default_timezone = :utc
353
+ ActiveRecord::Base.connection
354
+ rescue Exception
355
+ config_path = FileStalker.hunt([
356
+ "./config/database.yml",
357
+ "../config/database.yml",
358
+ "../../config/database.yml",
359
+ "./database.yml",
360
+ "../database.yml",
361
+ "../../database.yml"
362
+ ])
363
+ if config_path != nil
364
+ require 'yaml'
365
+ config_hash = File.open(config_path) do |file|
366
+ config_hash = YAML.load(file.read)
367
+ unless config_hash[FEED_TOOLS_ENV].nil?
368
+ config_hash = config_hash[FEED_TOOLS_ENV]
369
+ end
370
+ config_hash
371
+ end
372
+ self.logger.info(
373
+ "Attempting to manually load the database " +
374
+ "connection described in:")
375
+ self.logger.info(config_path)
376
+ begin
377
+ ActiveRecord::Base.configurations = config_hash
378
+ ActiveRecord::Base.establish_connection(config_hash)
379
+ ActiveRecord::Base.connection
380
+ rescue Exception => error
381
+ self.logger.fatal(
382
+ "Error initializing database:")
383
+ self.logger.fatal(error)
384
+ exit
385
+ end
386
+ else
387
+ self.logger.fatal(
388
+ "The database.yml file does not appear " +
389
+ "to exist.")
390
+ exit
391
+ end
392
+ end
393
+ end
394
+ if !FeedTools.feed_cache.connected?
395
+ self.logger.fatal("Not connected to the feed cache.")
396
+ if FeedTools.feed_cache.respond_to?(:table_exists?)
397
+ if !FeedTools.feed_cache.table_exists?
398
+ self.logger.fatal(
399
+ "The FeedTools cache table is missing.")
400
+ end
401
+ end
402
+ exit
403
+ end
404
+
405
+ # A random start delay is introduced so that we don't have multiple
406
+ # feed updater daemons getting kicked off at the same time by
407
+ # multiple users.
408
+ if self.updater_options[:start_delay]
409
+ delay = (rand(45) + 15)
410
+ self.logger.info("Startup delay set for #{delay} minutes.")
411
+ sleep(delay.minutes)
412
+ end
413
+
414
+ # The main feed update loop.
415
+ loop do
416
+ result = nil
417
+ sleepy_time = self.updater_options[:sleep_time].to_i.minutes
418
+ begin
419
+ result = Benchmark.measure do
420
+ self.update_feeds()
421
+ end
422
+ self.logger.info(
423
+ "#{@feed_href_list.size} feed(s) updated " +
424
+ "in #{result.real.round} seconds.")
425
+ sleepy_time =
426
+ (self.updater_options[:sleep_time].to_i.minutes -
427
+ result.real.round)
428
+ rescue Exception => error
429
+ self.logger.error("Feed update sequence errored out.")
430
+ self.logger.error(error.class.name + ": " + error.message)
431
+ self.logger.error("\n" + error.backtrace.join("\n").to_s)
432
+ end
433
+
434
+ @feed_href_list = nil
435
+ @feed_href_list_override = false
436
+ ObjectSpace.garbage_collect()
437
+ if sleepy_time > 0
438
+ self.logger.info(
439
+ "Sleeping for #{(sleepy_time / 1.minute.to_f).round} " +
440
+ "minutes...")
441
+ sleep(sleepy_time)
442
+ else
443
+ self.logger.info(
444
+ "Update took more than " +
445
+ "#{self.updater_options[:sleep_time].to_i} minutes, " +
446
+ "restarting immediately.")
447
+ end
448
+ end
449
+ end
450
+ options = self.daemon_options.merge({
451
+ :proc => feed_update_proc,
452
+ :mode => :proc
453
+ })
454
+ @application = Daemons::ApplicationGroup.new(
455
+ 'feed_updater', options).new_application(options)
456
+ @application.start()
457
+ else
458
+ @application.start()
459
+ end
460
+ self.logger.prefix = nil
461
+ self.logger.level = 0
462
+ self.logger.info("-" * 79)
463
+ self.logger.info("Daemon started, " +
464
+ "FeedUpdater #{FeedTools::FEED_UPDATER_VERSION::STRING} / " +
465
+ "FeedTools #{FeedTools::FEED_TOOLS_VERSION::STRING}")
466
+ self.logger.info(" @ #{Time.now.utc.to_s}")
467
+ self.logger.info("-" * 79)
468
+ self.logger.level = self.updater_options[:log_level]
469
+ self.logger.prefix = "FeedUpdater".ljust(20)
470
+ @status = :running
471
+ return self.pid
472
+ end
473
+
474
+ # Stops the daemon.
475
+ def stop()
476
+ if self.pid.nil?
477
+ puts "FeedUpdater isn't running."
478
+ end
479
+ begin
480
+ # Die.
481
+ Process.kill('TERM', self.pid)
482
+ rescue Exception
483
+ end
484
+ begin
485
+ # No, really, I mean it. You need to die.
486
+ system("kill #{self.pid} 2> /dev/null")
487
+
488
+ # Perhaps I wasn't clear somehow?
489
+ system("kill -9 #{self.pid} 2> /dev/null")
490
+ rescue Exception
491
+ end
492
+ begin
493
+ # Clean the pid file up.
494
+ if File.exists?(
495
+ File.expand_path(self.pid_file_dir + "/feed_updater.pid"))
496
+ File.delete(
497
+ File.expand_path(self.pid_file_dir + "/feed_updater.pid"))
498
+ end
499
+ rescue Exception
500
+ end
501
+ self.logger.prefix = "FeedUpdater".ljust(20)
502
+ self.logger.level = 0
503
+ self.logger.info("Daemon stopped.")
504
+ self.logger.level = self.updater_options[:log_level]
505
+ @status = :stopped
506
+ return nil if self.application.nil?
507
+ begin
508
+ self.application.stop()
509
+ rescue Exception
510
+ end
511
+ return nil
512
+ end
513
+
514
+ # Restarts the daemon.
515
+ def restart()
516
+ self.stop()
517
+ self.start()
518
+ end
519
+
520
+ def progress_precentage()
521
+ if !defined?(@remaining_href_list) || !defined?(@feed_href_list)
522
+ return nil
523
+ end
524
+ if @remaining_href_list.nil? || @feed_href_list.nil?
525
+ return nil
526
+ end
527
+ if @feed_href_list == []
528
+ return nil
529
+ end
530
+ return 100.0 - (100.0 *
531
+ (@remaining_href_list.size.to_f / @feed_href_list.size.to_f))
532
+ end
533
+
534
+ # Updates all of the feeds.
535
+ def update_feeds()
536
+ self.logger.level = 0
537
+ self.logger.prefix = "FeedUpdater".ljust(20)
538
+ ObjectSpace.garbage_collect()
539
+ if defined?(@@on_begin) && @@on_begin != nil
540
+ self.logger.info("Running custom startup event...")
541
+ self.cloaker(&(@@on_begin)).bind(self).call()
542
+ end
543
+ if defined?(@feed_href_list_override) && @feed_href_list_override
544
+ self.logger.info("Using custom feed list...")
545
+ self.feed_href_list()
546
+ else
547
+ self.logger.info("Loading default feed list...")
548
+ begin
549
+ expire_time = (Time.now - 1.hour).utc
550
+ expire_time_string = sprintf('%04d-%02d-%02d %02d:%02d:%02d',
551
+ expire_time.year, expire_time.month, expire_time.day,
552
+ expire_time.hour, expire_time.min, expire_time.sec)
553
+ @feed_href_list =
554
+ FeedTools.feed_cache.connection.execute(
555
+ "SELECT href FROM cached_feeds WHERE " +
556
+ "last_retrieved < '#{expire_time_string}'").to_a.flatten
557
+ rescue Exception
558
+ self.logger.warn("Default feed list failed, using fallback.")
559
+ @feed_href_list =
560
+ FeedTools.feed_cache.find(:all).collect do |feed|
561
+ feed.href
562
+ end
563
+ self.logger.warn(
564
+ "Fallback succeeded. Custom feed list override recommended.")
565
+ end
566
+ end
567
+ self.logger.info("Updating #{@feed_href_list.size} feed(s)...")
568
+ self.logger.level = self.updater_options[:log_level]
569
+
570
+ @threads = []
571
+ @remaining_href_list = @feed_href_list.dup
572
+
573
+ ObjectSpace.garbage_collect()
574
+
575
+ begin_updating = false
576
+ self.logger.info(
577
+ "Starting up #{self.updater_options[:threads]} thread(s)...")
578
+
579
+ mutex = Mutex.new
580
+ for i in 0...self.updater_options[:threads]
581
+ updater_thread = Thread.new do
582
+ self.logger.level = self.updater_options[:log_level]
583
+ self.logger.datetime_format = "%s"
584
+ self.logger.progname = "FeedUpdater".ljust(20)
585
+
586
+ while !Thread.current.respond_to?(:thread_id) &&
587
+ begin_updating == false
588
+ Thread.pass
589
+ end
590
+ mutex.synchronize do
591
+ self.logger.prefix =
592
+ "Thread #{Thread.current.thread_id} ".ljust(20)
593
+ self.logger.info("Thread started.")
594
+
595
+ begin
596
+ FeedTools.feed_cache.initialize_cache()
597
+ if !FeedTools.feed_cache.set_up_correctly?
598
+ self.logger.info("Problem with cache connection...")
599
+ end
600
+ rescue Exception => error
601
+ self.logger.info(error)
602
+ end
603
+ end
604
+
605
+ ObjectSpace.garbage_collect()
606
+ Thread.pass
607
+
608
+ while @remaining_href_list.size > 0
609
+ progress = nil
610
+ mutex.synchronize do
611
+ Thread.current.progress = self.progress_precentage()
612
+ Thread.current.href = @remaining_href_list.shift()
613
+ progress = sprintf("%.2f", Thread.current.progress)
614
+ end
615
+ begin
616
+ begin
617
+ Thread.current.feed = nil
618
+ feed_load_benchmark = Benchmark.measure do
619
+ Thread.current.feed =
620
+ FeedTools::Feed.open(Thread.current.href)
621
+ end
622
+ Thread.pass
623
+ if Thread.current.feed.live?
624
+ unless @@on_update.nil?
625
+ mutex.synchronize do
626
+ progress = sprintf("%.2f", Thread.current.progress)
627
+ self.logger.prefix =
628
+ ("Thread #{Thread.current.thread_id} (#{progress}%)"
629
+ ).ljust(20)
630
+ self.cloaker(&(@@on_update)).bind(self).call(
631
+ Thread.current.feed, feed_load_benchmark.real)
632
+ end
633
+ end
634
+ else
635
+ mutex.synchronize do
636
+ progress = sprintf("%.2f", Thread.current.progress)
637
+ self.logger.prefix =
638
+ ("Thread #{Thread.current.thread_id} (#{progress}%)"
639
+ ).ljust(20)
640
+ self.logger.info(
641
+ "'#{Thread.current.href}' unchanged " +
642
+ "or unavailable, skipping.")
643
+ end
644
+ end
645
+ rescue Exception => error
646
+ mutex.synchronize do
647
+ progress = sprintf("%.2f", Thread.current.progress)
648
+ self.logger.prefix =
649
+ ("Thread #{Thread.current.thread_id} (#{progress}%)"
650
+ ).ljust(20)
651
+ if @@on_error != nil
652
+ self.cloaker(&(@@on_error)).bind(self).call(
653
+ Thread.current.href, error)
654
+ else
655
+ self.logger.error(
656
+ "Error updating '#{Thread.current.href}':")
657
+ self.logger.error(error.class.name + ": " + error.message)
658
+ self.logger.error(error.class.backtrace)
659
+ end
660
+ end
661
+ end
662
+ rescue Exception => error
663
+ mutex.synchronize do
664
+ progress = sprintf("%.2f", Thread.current.progress)
665
+ self.logger.prefix = ("Thread #{Thread.current.thread_id} " +
666
+ "(#{progress}%)"
667
+ ).ljust(20)
668
+ self.logger.fatal("Critical unhandled error.")
669
+ self.logger.fatal("Error updating '#{Thread.current.href}':")
670
+ self.logger.fatal(error.class.name + ": " + error.message)
671
+ self.logger.fatal(error.class.backtrace)
672
+ end
673
+ end
674
+ ObjectSpace.garbage_collect()
675
+ Thread.pass
676
+ end
677
+ end
678
+ @threads << updater_thread
679
+ class <<updater_thread
680
+ attr_accessor :thread_id
681
+ attr_accessor :progress
682
+ attr_accessor :href
683
+ attr_accessor :feed
684
+ end
685
+ updater_thread.thread_id = i
686
+ end
687
+ mutex.synchronize do
688
+ self.logger.prefix = "FeedUpdater".ljust(20)
689
+ self.logger.info(
690
+ "#{@threads.size} thread(s) successfully started...")
691
+ begin_updating = true
692
+ end
693
+ Thread.pass
694
+
695
+ ObjectSpace.garbage_collect()
696
+ Thread.pass
697
+ for i in 0...@threads.size
698
+ mutex.synchronize do
699
+ self.logger.prefix = "FeedUpdater".ljust(20)
700
+ self.logger.info(
701
+ "Joining on thread #{@threads[i].thread_id}...")
702
+ end
703
+ @threads[i].join
704
+ end
705
+ self.logger.prefix = "FeedUpdater".ljust(20)
706
+ ObjectSpace.garbage_collect()
707
+
708
+ self.logger.progname = nil
709
+ unless @@on_complete.nil?
710
+ self.cloaker(&(@@on_complete)).bind(self).call(@feed_href_list)
711
+ end
712
+ self.logger.level = 0
713
+ self.logger.info("Finished updating.")
714
+ self.logger.level = self.updater_options[:log_level]
715
+ end
716
+ end
717
+ end