katello 3.14.0.rc1 → 3.14.0.rc2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of katello might be problematic. Click here for more details.

Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/katello/containers/container.js +5 -5
  3. data/app/assets/javascripts/katello/hosts/activation_key_edit.js +1 -1
  4. data/app/assets/javascripts/katello/hosts/host_and_hostgroup_edit.js +1 -1
  5. data/app/assets/javascripts/katello/sync_management/sync_management.js +6 -6
  6. data/app/controllers/katello/api/v2/repositories_controller.rb +1 -1
  7. data/app/lib/actions/katello/content_view/publish.rb +13 -4
  8. data/app/lib/actions/katello/content_view_puppet_environment/destroy.rb +3 -1
  9. data/app/lib/actions/katello/content_view_version/create_repos.rb +25 -0
  10. data/app/lib/actions/katello/content_view_version/incremental_update.rb +32 -12
  11. data/app/lib/actions/katello/environment/destroy.rb +5 -1
  12. data/app/lib/actions/katello/host/hypervisors_update.rb +12 -1
  13. data/app/lib/actions/katello/repository/clone_to_version.rb +3 -11
  14. data/app/lib/actions/pulp/repository/copy_units.rb +3 -6
  15. data/app/lib/actions/pulp/repository/refresh.rb +1 -1
  16. data/app/lib/actions/pulp/repository/sync.rb +3 -1
  17. data/app/lib/katello/resources/candlepin.rb +6 -0
  18. data/app/lib/katello/resources/candlepin/admin.rb +21 -0
  19. data/app/lib/katello/util/package_clause_generator.rb +10 -0
  20. data/app/lib/katello/util/support.rb +19 -0
  21. data/app/models/katello/glue/pulp/repo.rb +0 -13
  22. data/app/models/katello/host/subscription_facet.rb +2 -1
  23. data/app/models/katello/kt_environment.rb +1 -0
  24. data/app/models/katello/ping.rb +35 -3
  25. data/app/models/katello/repository.rb +5 -2
  26. data/app/models/katello/rhsm_fact_parser.rb +6 -1
  27. data/app/models/katello/root_repository.rb +1 -1
  28. data/app/services/katello/candlepin/consumer.rb +2 -1
  29. data/app/{lib/actions/candlepin/import_pool_handler.rb → services/katello/candlepin/event_handler.rb} +2 -2
  30. data/app/services/katello/candlepin_event_listener.rb +106 -0
  31. data/app/services/katello/candlepin_listening_service.rb +91 -0
  32. data/app/services/katello/event_daemon.rb +91 -0
  33. data/app/services/katello/event_monitor/poller_thread.rb +108 -0
  34. data/app/services/katello/event_queue.rb +4 -0
  35. data/app/services/katello/pulp/repository.rb +13 -6
  36. data/app/services/katello/pulp/repository/yum.rb +52 -12
  37. data/lib/katello/engine.rb +13 -14
  38. data/lib/katello/middleware/event_daemon.rb +14 -0
  39. data/lib/katello/plugin.rb +2 -0
  40. data/lib/katello/tasks/upgrade_check.rake +2 -7
  41. data/lib/katello/version.rb +1 -1
  42. data/locale/bn/katello.po +1 -1
  43. data/locale/cs/katello.po +1 -1
  44. data/locale/de/katello.po +2 -2
  45. data/locale/en/katello.po +1 -1
  46. data/locale/es/katello.po +2 -2
  47. data/locale/fr/katello.po +2 -2
  48. data/locale/gu/katello.po +1 -1
  49. data/locale/hi/katello.po +1 -1
  50. data/locale/it/katello.po +2 -2
  51. data/locale/ja/katello.po +2 -2
  52. data/locale/katello.pot +1 -1
  53. data/locale/kn/katello.po +1 -1
  54. data/locale/ko/katello.po +2 -2
  55. data/locale/mr/katello.po +1 -1
  56. data/locale/or/katello.po +1 -1
  57. data/locale/pa/katello.po +1 -1
  58. data/locale/pt/katello.po +1 -1
  59. data/locale/pt_BR/katello.po +2 -2
  60. data/locale/ru/katello.po +2 -2
  61. data/locale/ta/katello.po +1 -1
  62. data/locale/te/katello.po +1 -1
  63. data/locale/zh_CN/katello.po +2 -2
  64. data/locale/zh_TW/katello.po +2 -2
  65. data/webpack/components/Content/Details/__tests__/__snapshots__/ContentDetails.test.js.snap +2 -2
  66. data/webpack/scenes/RedHatRepositories/components/EnabledRepository/EnabledRepository.js +2 -2
  67. data/webpack/scenes/RedHatRepositories/components/RepositorySetRepository/RepositorySetRepository.js +3 -3
  68. data/webpack/scenes/Subscriptions/Details/__tests__/__snapshots__/SubscriptionDetails.test.js.snap +2 -2
  69. data/webpack/scenes/Subscriptions/SubscriptionsPage.scss +6 -0
  70. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/SubscriptionsTableHelpers.js +1 -1
  71. data/webpack/scenes/Subscriptions/components/SubscriptionsTable/__tests__/SubscriptionsTable.fixtures.js +1 -1
  72. metadata +11 -10
  73. data/app/lib/actions/candlepin/candlepin_listening_service.rb +0 -114
  74. data/app/lib/actions/candlepin/listen_on_candlepin_events.rb +0 -216
  75. data/app/lib/actions/katello/event_queue/monitor.rb +0 -122
  76. data/app/lib/actions/katello/event_queue/poller_thread.rb +0 -83
  77. data/app/lib/actions/katello/event_queue/suspended_action.rb +0 -23
@@ -149,6 +149,12 @@ module Katello
149
149
  end # class << self
150
150
  end # UpstreamCandlepinResource
151
151
 
152
+ module AdminResource
153
+ def path
154
+ "#{self.prefix}/admin"
155
+ end
156
+ end
157
+
152
158
  module ConsumerResource
153
159
  def path(id = nil)
154
160
  "#{self.prefix}/consumers/#{id}"
@@ -0,0 +1,21 @@
1
+ module Katello
2
+ module Resources
3
+ module Candlepin
4
+ class Admin < CandlepinResource
5
+ extend AdminResource
6
+
7
+ def self.queues
8
+ response = get("#{path}/queues")
9
+ JSON.parse(response.body).first
10
+ end
11
+
12
+ def self.queue_depth(queue_name)
13
+ queue = queues.select { |q| q['queueName'] == queue_name }
14
+ queue['pendingMessageCount'].to_i
15
+ rescue
16
+ nil # be graceful when candlepin is down
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -3,6 +3,16 @@ module Katello
3
3
  class PackageClauseGenerator
4
4
  include Util::FilterClauseGenerator
5
5
 
6
+ def copy_clause
7
+ clauses = super
8
+ {"$and" => [{"is_modular" => false}, clauses]} unless clauses.blank?
9
+ end
10
+
11
+ def remove_clause
12
+ clauses = super
13
+ {"$and" => [{"is_modular" => false}, clauses]} unless clauses.blank?
14
+ end
15
+
6
16
  protected
7
17
 
8
18
  def fetch_filters
@@ -72,6 +72,25 @@ module Katello
72
72
  stringify(params.keys) - stringify(rule.keys)
73
73
  end
74
74
 
75
+ def self.with_db_connection(logger = Rails.logger)
76
+ yield
77
+ rescue PG::ConnectionBad, ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished => e
78
+ logger.error(e.message)
79
+ logger.error("Lost database connection. Attempting reconnect.")
80
+
81
+ active_record_retry_connect
82
+
83
+ retry
84
+ end
85
+
86
+ def self.active_record_retry_connect
87
+ sleep 3
88
+ ActiveRecord::Base.connection.reconnect!
89
+ rescue
90
+ logger.error("Trying to reconnect to the database.")
91
+ retry
92
+ end
93
+
75
94
  # Used for retrying active record transactions when race conditions could cause
76
95
  # RecordNotUnique exceptions
77
96
  def self.active_record_retry(retries = 3)
@@ -44,19 +44,6 @@ module Katello
44
44
  !repo.distributors_match?(repo_details["distributors"], smart_proxy)
45
45
  end
46
46
  end
47
-
48
- def self.build_override_config(options)
49
- config = {}
50
- if options[:filters].present? && (options[:solve_dependencies] || options[:resolve_dependencies])
51
- if Setting[:dependency_solving_algorithm] == 'greedy'
52
- config[:recursive] = true
53
- else
54
- config[:recursive_conservative] = true
55
- end
56
- end
57
-
58
- config
59
- end
60
47
  end
61
48
  end
62
49
 
@@ -127,7 +127,8 @@ module Katello
127
127
  guest_ids = self.candlepin_consumer.virtual_guests.pluck(:id)
128
128
  end
129
129
 
130
- subscription_facets = SubscriptionFacet.where(:host_id => guest_ids)
130
+ subscription_facets = SubscriptionFacet.where(:host_id => guest_ids).
131
+ where("hypervisor_host_id != ? OR hypervisor_host_id is NULL", self.host.id)
131
132
  subscription_facets.update_all(:hypervisor_host_id => self.host.id)
132
133
  elsif (virtual_host = self.candlepin_consumer.virtual_host)
133
134
  self.hypervisor_host = virtual_host
@@ -76,6 +76,7 @@ module Katello
76
76
 
77
77
  scoped_search :on => :name, :complete_value => true
78
78
  scoped_search :on => :organization_id, :complete_value => true, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER
79
+ scoped_search :on => :id, :complete_value => true, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER
79
80
 
80
81
  def library?
81
82
  self.library
@@ -2,10 +2,12 @@ module Katello
2
2
  class Ping
3
3
  OK_RETURN_CODE = 'ok'.freeze
4
4
  FAIL_RETURN_CODE = 'FAIL'.freeze
5
+ WARN_RETURN_CODE = 'WARN'.freeze
5
6
  PACKAGES = %w(katello candlepin pulp qpid foreman tfm hammer).freeze
7
+
6
8
  class << self
7
9
  def services(capsule_id = nil)
8
- services = [:pulp, :pulp_auth, :candlepin, :candlepin_auth, :foreman_tasks]
10
+ services = [:pulp, :pulp_auth, :candlepin, :candlepin_auth, :foreman_tasks, :katello_events, :candlepin_events]
9
11
  services += [:pulp3] if fetch_proxy(capsule_id)&.pulp3_enabled?
10
12
  services
11
13
  end
@@ -25,11 +27,13 @@ module Katello
25
27
  ping_pulp_with_auth(result[:pulp_auth], result[:pulp][:status]) if result.include?(:pulp_auth)
26
28
  ping_candlepin_with_auth(result[:candlepin_auth]) if result.include?(:candlepin_auth)
27
29
  ping_foreman_tasks(result[:foreman_tasks]) if result.include?(:foreman_tasks)
30
+ ping_katello_events(result[:katello_events]) if result.include?(:katello_events)
31
+ ping_candlepin_events(result[:candlepin_events]) if result.include?(:candlepin_events)
28
32
 
29
33
  # set overall status result code
30
34
  result = {:services => result}
31
35
  result[:services].each_value do |v|
32
- result[:status] = v[:status] == OK_RETURN_CODE ? OK_RETURN_CODE : FAIL_RETURN_CODE
36
+ result[:status] = [OK_RETURN_CODE, WARN_RETURN_CODE].include?(v[:status]) ? OK_RETURN_CODE : FAIL_RETURN_CODE
33
37
  end
34
38
  result
35
39
  end
@@ -41,6 +45,34 @@ module Katello
41
45
  }
42
46
  end
43
47
 
48
+ def daemon_status_message(status)
49
+ "#{status[:processed_count]} Processed, #{status[:failed_count]} Failed, #{status[:queue_depth]} in queue"
50
+ end
51
+
52
+ def ping_katello_events(result)
53
+ exception_watch(result) do
54
+ status = Katello::EventMonitor::PollerThread.status
55
+
56
+ if status[:queue_depth] && status[:queue_depth] > 1000
57
+ result[:status] = WARN_RETURN_CODE
58
+ end
59
+
60
+ result[:message] = daemon_status_message(status)
61
+ end
62
+ end
63
+
64
+ def ping_candlepin_events(result)
65
+ exception_watch(result) do
66
+ status = Katello::CandlepinEventListener.status
67
+
68
+ if status[:queue_depth] && status[:queue_depth] > 1000
69
+ result[:status] = WARN_RETURN_CODE
70
+ end
71
+
72
+ result[:message] = daemon_status_message(status)
73
+ end
74
+ end
75
+
44
76
  def ping_pulp3_without_auth(service_result, capsule_id)
45
77
  exception_watch(service_result) do
46
78
  Katello::Ping.pulp3_without_auth(fetch_proxy(capsule_id).pulp3_url("api/v3"))
@@ -104,8 +136,8 @@ module Katello
104
136
  # check for exception - set the result code properly
105
137
  def exception_watch(result)
106
138
  start = Time.new
107
- yield
108
139
  result[:status] = OK_RETURN_CODE
140
+ yield
109
141
  result[:duration_ms] = ((Time.new - start) * 1000).round.to_s
110
142
  result
111
143
  rescue => e
@@ -140,7 +140,6 @@ module Katello
140
140
  scoped_search :on => :distribution_variant, :complete_value => true
141
141
  scoped_search :on => :distribution_bootable, :complete_value => true
142
142
  scoped_search :on => :distribution_uuid, :complete_value => true
143
- scoped_search :on => :ignore_global_proxy, :relation => :root, :complete_value => true
144
143
  scoped_search :on => :redhat, :complete_value => { :true => true, :false => false }, :ext_method => :search_by_redhat
145
144
  scoped_search :on => :container_repository_name, :complete_value => true
146
145
  scoped_search :on => :description, :relation => :root, :only_explicit => true
@@ -157,7 +156,7 @@ module Katello
157
156
  :content_type, :product_id, :checksum_type, :docker_upstream_name, :mirror_on_sync, :"mirror_on_sync?",
158
157
  :download_policy, :verify_ssl_on_sync, :"verify_ssl_on_sync?", :upstream_username, :upstream_password,
159
158
  :ostree_upstream_sync_policy, :ostree_upstream_sync_depth, :deb_releases, :deb_components, :deb_architectures,
160
- :ignore_global_proxy, :ssl_ca_cert_id, :ssl_ca_cert, :ssl_client_cert, :ssl_client_cert_id, :ssl_client_key_id,
159
+ :ssl_ca_cert_id, :ssl_ca_cert, :ssl_client_cert, :ssl_client_cert_id, :ssl_client_key_id,
161
160
  :ssl_client_key, :ignorable_content, :description, :docker_tags_whitelist, :ansible_collection_requirements, :http_proxy_policy, :http_proxy_id, :to => :root
162
161
 
163
162
  delegate :content_id, to: :root, allow_nil: true
@@ -315,6 +314,10 @@ module Katello
315
314
  found
316
315
  end
317
316
 
317
+ def siblings
318
+ content_view_version.archived_repos.where.not(:id => id)
319
+ end
320
+
318
321
  def clones
319
322
  self.root.repositories.where.not(:id => library_instance_id || id)
320
323
  end
@@ -23,7 +23,7 @@ module Katello
23
23
  def get_facts_for_interface(interface)
24
24
  {
25
25
  'link' => true,
26
- 'macaddress' => facts["net.interface.#{interface}.mac_address"],
26
+ 'macaddress' => get_rhsm_mac(interface),
27
27
  'ipaddress' => get_rhsm_ip(interface)
28
28
  }
29
29
  end
@@ -107,5 +107,10 @@ module Katello
107
107
  ip = facts["net.interface.#{interface}.ipv4_address"]
108
108
  Net::Validations.validate_ip(ip) ? ip : nil
109
109
  end
110
+
111
+ def get_rhsm_mac(interface)
112
+ # if slave then permanent_mac_address contains the physical mac
113
+ facts["net.interface.#{interface}.permanent_mac_address"] || facts["net.interface.#{interface}.mac_address"]
114
+ end
110
115
  end
111
116
  end
@@ -290,7 +290,7 @@ module Katello
290
290
 
291
291
  def pulp_update_needed?
292
292
  changeable_attributes = %w(url unprotected checksum_type docker_upstream_name download_policy mirror_on_sync verify_ssl_on_sync
293
- upstream_username upstream_password ostree_upstream_sync_policy ostree_upstream_sync_depth ignore_global_proxy ignorable_content
293
+ upstream_username upstream_password ostree_upstream_sync_policy ostree_upstream_sync_depth ignorable_content
294
294
  ssl_ca_cert_id ssl_client_cert_id ssl_client_key_id http_proxy_policy http_proxy_id)
295
295
  changeable_attributes += %w(name container_repository_name docker_tags_whitelist) if docker?
296
296
  changeable_attributes += %w(deb_releases deb_components deb_architectures gpg_key_id) if deb?
@@ -100,9 +100,10 @@ module Katello
100
100
  end
101
101
 
102
102
  def virtual_guests
103
+ return @virtual_guests unless @virtual_guests.nil?
103
104
  return [] if self.uuid.nil?
104
105
  guest_uuids = Resources::Candlepin::Consumer.virtual_guests(self.uuid).map { |guest| guest['uuid'] }
105
- ::Host.joins(:subscription_facet).where("#{Katello::Host::SubscriptionFacet.table_name}.uuid" => guest_uuids)
106
+ @virtual_guests = ::Host.joins(:subscription_facet).where("#{Katello::Host::SubscriptionFacet.table_name}.uuid" => guest_uuids)
106
107
  end
107
108
 
108
109
  def virtual_host
@@ -1,6 +1,6 @@
1
- module Actions
1
+ module Katello
2
2
  module Candlepin
3
- class ImportPoolHandler
3
+ class EventHandler
4
4
  attr_reader :message_handler
5
5
 
6
6
  def initialize(logger)
@@ -0,0 +1,106 @@
1
+ module Katello
2
+ class CandlepinEventListener
3
+ PROCESSED_COUNT_CACHE_KEY = 'candlepin_events_processed'.freeze
4
+ FAILED_COUNT_CACHE_KEY = 'candlepin_events_failed'.freeze
5
+ AMQP_QUEUE_NAME = 'event.org.candlepin.audit.AMQPBusPublisher'.freeze
6
+
7
+ CandlepinEvent = Struct.new(:message_id, :subject, :content)
8
+
9
+ @logger = ::Foreman::Logging.logger('katello/candlepin_events')
10
+ @failed_count = 0
11
+ @processed_count = 0
12
+
13
+ def self.start_service
14
+ loop do
15
+ begin
16
+ result = Katello::CandlepinListeningService.instance.start
17
+
18
+ break if result == :connected
19
+
20
+ @logger.info("Attempting to restart Candlepin Listening Service")
21
+ sleep 5
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.run
27
+ @thread.kill if @thread
28
+
29
+ # run in own thread so connecting to qpid won't block the main process
30
+ @thread = Thread.new do
31
+ Rails.application.executor.wrap do
32
+ initialize_listening_service
33
+ start_service
34
+
35
+ Katello::CandlepinListeningService.instance.poll_for_messages do |message|
36
+ if message[:result]
37
+ result = message[:result]
38
+ event = CandlepinEvent.new(result.message_id, result.subject, result.content)
39
+ act_on_event(event)
40
+ elsif message[:error]
41
+ @logger.error("Disconnected from Candlepin Listening Service, reconnecting")
42
+ start_service
43
+ end
44
+ end
45
+ end
46
+ end
47
+ rescue => e
48
+ @logger.error("Fatal error in Candlepin Listening Service")
49
+ close
50
+ raise e
51
+ end
52
+
53
+ def self.status
54
+ {
55
+ processed_count: Rails.cache.fetch(PROCESSED_COUNT_CACHE_KEY) { @processed_count },
56
+ failed_count: Rails.cache.fetch(FAILED_COUNT_CACHE_KEY) { @failed_count },
57
+ queue_depth: Katello::Resources::Candlepin::Admin.queue_depth(AMQP_QUEUE_NAME)
58
+ }
59
+ end
60
+
61
+ def self.reset_status
62
+ Rails.cache.write(PROCESSED_COUNT_CACHE_KEY, 0)
63
+ Rails.cache.write(FAILED_COUNT_CACHE_KEY, 0)
64
+ end
65
+
66
+ def self.act_on_event(event)
67
+ ::Katello::Util::Support.with_db_connection(@logger) do
68
+ ::Katello::Candlepin::EventHandler.new(@logger).handle(event)
69
+ end
70
+ @processed_count += 1
71
+
72
+ Rails.cache.write(PROCESSED_COUNT_CACHE_KEY, @processed_count, expires_in: 24.hours)
73
+ rescue => e
74
+ @failed_count += 1
75
+ Rails.cache.write(FAILED_COUNT_CACHE_KEY, @failed_count, expires_in: 24.hours)
76
+ @logger.error("Error handling Candlepin event")
77
+ @logger.error(e.message)
78
+ @logger.error(e.backtrace.join("\n"))
79
+ end
80
+
81
+ def self.configured?
82
+ SETTINGS[:katello].key?(:qpid) &&
83
+ SETTINGS[:katello][:qpid].key?(:url) &&
84
+ SETTINGS[:katello][:qpid].key?(:subscriptions_queue_address)
85
+ end
86
+
87
+ def self.initialize_listening_service
88
+ if configured?
89
+ Katello::CandlepinListeningService.initialize_service(@logger,
90
+ SETTINGS[:katello][:qpid][:url],
91
+ SETTINGS[:katello][:qpid][:subscriptions_queue_address])
92
+ else
93
+ fail("Katello has not been configured for qpid.url and qpid.subscriptions_queue_address")
94
+ end
95
+ rescue => e
96
+ @logger.error(e.message)
97
+ @logger.error(e.backtrace)
98
+ end
99
+
100
+ def self.close
101
+ Katello::CandlepinListeningService.close
102
+ @thread.kill if @thread
103
+ reset_status
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,91 @@
1
+ module Katello
2
+ class CandlepinListeningService
3
+ TIMEOUT = Qpid::Messaging::Duration::SECOND
4
+ NO_MESSAGE_AVAILABLE_ERROR_TYPE = 'NoMessageAvailable'.freeze
5
+ SLEEP_INTERVAL = 3
6
+
7
+ class ConnectionError < StandardError
8
+ end
9
+
10
+ class << self
11
+ attr_reader :instance
12
+
13
+ def initialize_service(logger, url, address)
14
+ @instance = self.new(logger, url, address)
15
+ end
16
+
17
+ def close
18
+ @instance.close if @instance
19
+ @instance = nil
20
+ end
21
+ end
22
+
23
+ def initialize(logger, url, address)
24
+ @url = url
25
+ @address = address
26
+ @connection = create_connection
27
+ @logger = logger
28
+ end
29
+
30
+ def create_connection
31
+ Qpid::Messaging::Connection.new(:url => @url, :options => {:transport => 'ssl'})
32
+ end
33
+
34
+ def close
35
+ @logger.info("Stopping Candlepin Listening Service")
36
+ @thread.kill if @thread
37
+ @connection.close
38
+ end
39
+
40
+ def retrieve
41
+ result = @receiver.fetch(TIMEOUT)
42
+ result
43
+ rescue => e
44
+ if e.class.name.include? "TransportFailure"
45
+ raise ConnectionError, "failed to connect to #{@url}"
46
+ else
47
+ raise e unless e.class.name.include? NO_MESSAGE_AVAILABLE_ERROR_TYPE
48
+ end
49
+ ensure
50
+ safe_release(result) if result
51
+ end
52
+
53
+ def safe_release(message)
54
+ @session.acknowledge(:message => message, :sync => true)
55
+ rescue => e
56
+ @session.release(message)
57
+ raise e
58
+ end
59
+
60
+ def start
61
+ unless @connection.open?
62
+ @connection.open
63
+ @session = @connection.create_session
64
+ @receiver = @session.create_receiver(@address)
65
+ @logger.info("Candlepin Event Listener started")
66
+ end
67
+
68
+ :connected
69
+ rescue => e
70
+ raise e unless e.class.name.include? "TransportFailure"
71
+ end
72
+
73
+ def fetch_message
74
+ {:result => retrieve, :error => nil}
75
+ rescue ConnectionError => e
76
+ {:result => nil, :error => e.message}
77
+ end
78
+
79
+ def poll_for_messages
80
+ @thread.kill if @thread
81
+ @thread = Thread.new do
82
+ loop do
83
+ message = fetch_message
84
+ yield(message) if block_given?
85
+
86
+ sleep SLEEP_INTERVAL if message[:result].nil? && message[:error].nil?
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end