good_job 1.4.1 → 1.8.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.
data/lib/good_job/cli.rb CHANGED
@@ -15,6 +15,11 @@ module GoodJob
15
15
  # Requiring this loads the application's configuration and classes.
16
16
  RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
17
17
 
18
+ # @!visibility private
19
+ def self.exit_on_failure?
20
+ true
21
+ end
22
+
18
23
  # @!macro thor.desc
19
24
  # @!method $1
20
25
  # @return [void]
@@ -27,7 +32,8 @@ module GoodJob
27
32
  See option descriptions for the matching environment variable name.
28
33
 
29
34
  == Configuring queues
30
- \x5Separate multiple queues with commas; exclude queues with a leading minus;
35
+
36
+ Separate multiple queues with commas; exclude queues with a leading minus;
31
37
  separate isolated execution pools with semicolons and threads with colons.
32
38
 
33
39
  DESCRIPTION
@@ -43,10 +49,26 @@ module GoodJob
43
49
  type: :numeric,
44
50
  banner: 'SECONDS',
45
51
  desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 5)"
52
+ method_option :max_cache,
53
+ type: :numeric,
54
+ banner: 'COUNT',
55
+ desc: "Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)"
56
+ method_option :shutdown_timeout,
57
+ type: :numeric,
58
+ banner: 'SECONDS',
59
+ desc: "Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever))"
60
+ method_option :daemonize,
61
+ type: :boolean,
62
+ desc: "Run as a background daemon (default: false)"
63
+ method_option :pidfile,
64
+ type: :string,
65
+ desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
46
66
  def start
47
67
  set_up_application!
48
68
  configuration = GoodJob::Configuration.new(options)
49
69
 
70
+ Daemon.new(pidfile: configuration.pidfile).daemonize if configuration.daemonize?
71
+
50
72
  notifier = GoodJob::Notifier.new
51
73
  poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
52
74
  scheduler = GoodJob::Scheduler.from_configuration(configuration)
@@ -63,9 +85,8 @@ module GoodJob
63
85
  break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
64
86
  end
65
87
 
66
- notifier.shutdown
67
- poller.shutdown
68
- scheduler.shutdown
88
+ executors = [notifier, poller, scheduler]
89
+ GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout)
69
90
  end
70
91
 
71
92
  default_task :start
@@ -8,20 +8,23 @@ module GoodJob
8
8
  # Default number of threads to use per {Scheduler}
9
9
  DEFAULT_MAX_THREADS = 5
10
10
  # Default number of seconds between polls for jobs
11
- DEFAULT_POLL_INTERVAL = 5
11
+ DEFAULT_POLL_INTERVAL = 10
12
+ # Default number of threads to use per {Scheduler}
13
+ DEFAULT_MAX_CACHE = 10000
12
14
  # Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs}
13
15
  DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
16
+ # Default to always wait for jobs to finish for {#shutdown}
17
+ DEFAULT_SHUTDOWN_TIMEOUT = -1
14
18
 
15
- # @!attribute [r] options
16
- # The options that were explicitly set when initializing +Configuration+.
17
- # @return [Hash]
18
- #
19
- # @!attribute [r] env
20
- # The environment from which to read GoodJob's environment variables. By
21
- # default, this is the current process's environment, but it can be set
22
- # to something else in {#initialize}.
23
- # @return [Hash]
24
- attr_reader :options, :env
19
+ # The options that were explicitly set when initializing +Configuration+.
20
+ # @return [Hash]
21
+ attr_reader :options
22
+
23
+ # The environment from which to read GoodJob's environment variables. By
24
+ # default, this is the current process's environment, but it can be set
25
+ # to something else in {#initialize}.
26
+ # @return [Hash]
27
+ attr_reader :env
25
28
 
26
29
  # @param options [Hash] Any explicitly specified configuration options to
27
30
  # use. Keys are symbols that match the various methods on this class.
@@ -43,8 +46,12 @@ module GoodJob
43
46
  # Value to use if none was specified in the configuration.
44
47
  # @return [Symbol]
45
48
  def execution_mode(default: :external)
46
- if options[:execution_mode]
49
+ if defined?(GOOD_JOB_WITHIN_CLI) && GOOD_JOB_WITHIN_CLI
50
+ :external
51
+ elsif options[:execution_mode]
47
52
  options[:execution_mode]
53
+ elsif rails_config[:execution_mode]
54
+ rails_config[:execution_mode]
48
55
  elsif env['GOOD_JOB_EXECUTION_MODE'].present?
49
56
  env['GOOD_JOB_EXECUTION_MODE'].to_sym
50
57
  else
@@ -72,9 +79,10 @@ module GoodJob
72
79
  def max_threads
73
80
  (
74
81
  options[:max_threads] ||
75
- env['GOOD_JOB_MAX_THREADS'] ||
76
- env['RAILS_MAX_THREADS'] ||
77
- DEFAULT_MAX_THREADS
82
+ rails_config[:max_threads] ||
83
+ env['GOOD_JOB_MAX_THREADS'] ||
84
+ env['RAILS_MAX_THREADS'] ||
85
+ DEFAULT_MAX_THREADS
78
86
  ).to_i
79
87
  end
80
88
 
@@ -85,6 +93,7 @@ module GoodJob
85
93
  # @return [String]
86
94
  def queue_string
87
95
  options[:queues] ||
96
+ rails_config[:queues] ||
88
97
  env['GOOD_JOB_QUEUES'] ||
89
98
  '*'
90
99
  end
@@ -96,20 +105,67 @@ module GoodJob
96
105
  def poll_interval
97
106
  (
98
107
  options[:poll_interval] ||
99
- env['GOOD_JOB_POLL_INTERVAL'] ||
100
- DEFAULT_POLL_INTERVAL
108
+ rails_config[:poll_interval] ||
109
+ env['GOOD_JOB_POLL_INTERVAL'] ||
110
+ DEFAULT_POLL_INTERVAL
101
111
  ).to_i
102
112
  end
103
113
 
114
+ # The maximum number of future-scheduled jobs to store in memory.
115
+ # Storing future-scheduled jobs in memory reduces execution latency
116
+ # at the cost of increased memory usage. 10,000 stored jobs = ~20MB.
117
+ # @return [Integer]
118
+ def max_cache
119
+ (
120
+ options[:max_cache] ||
121
+ rails_config[:max_cache] ||
122
+ env['GOOD_JOB_MAX_CACHE'] ||
123
+ DEFAULT_MAX_CACHE
124
+ ).to_i
125
+ end
126
+
127
+ # The number of seconds to wait for jobs to finish when shutting down
128
+ # before stopping the thread. +-1+ is forever.
129
+ # @return [Numeric]
130
+ def shutdown_timeout
131
+ (
132
+ options[:shutdown_timeout] ||
133
+ rails_config[:shutdown_timeout] ||
134
+ env['GOOD_JOB_SHUTDOWN_TIMEOUT'] ||
135
+ DEFAULT_SHUTDOWN_TIMEOUT
136
+ ).to_f
137
+ end
138
+
104
139
  # Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
105
140
  # This configuration is only used when {GoodJob.preserve_job_records} is +true+.
106
- # @return [Boolean]
141
+ # @return [Integer]
107
142
  def cleanup_preserved_jobs_before_seconds_ago
108
143
  (
109
144
  options[:before_seconds_ago] ||
110
- env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
111
- DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
145
+ rails_config[:cleanup_preserved_jobs_before_seconds_ago] ||
146
+ env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
147
+ DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
112
148
  ).to_i
113
149
  end
150
+
151
+ # Tests whether to daemonize the process.
152
+ # @return [Boolean]
153
+ def daemonize?
154
+ options[:daemonize] || false
155
+ end
156
+
157
+ # Path of the pidfile to create when running as a daemon.
158
+ # @return [Pathname,String]
159
+ def pidfile
160
+ options[:pidfile] ||
161
+ env['GOOD_JOB_PIDFILE'] ||
162
+ Rails.application.root.join('tmp', 'pids', 'good_job.pid')
163
+ end
164
+
165
+ private
166
+
167
+ def rails_config
168
+ Rails.application.config.good_job
169
+ end
114
170
  end
115
171
  end
@@ -0,0 +1,59 @@
1
+ module GoodJob
2
+ #
3
+ # Manages daemonization of the current process.
4
+ #
5
+ class Daemon
6
+ # The path of the generated pidfile.
7
+ # @return [Pathname,String]
8
+ attr_reader :pidfile
9
+
10
+ # @param pidfile [Pathname,String] Pidfile path
11
+ def initialize(pidfile:)
12
+ @pidfile = pidfile
13
+ end
14
+
15
+ # Daemonizes the current process and writes out a pidfile.
16
+ def daemonize
17
+ check_pid
18
+ Process.daemon
19
+ write_pid
20
+ end
21
+
22
+ private
23
+
24
+ def write_pid
25
+ File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
26
+ at_exit { File.delete(pidfile) if File.exist?(pidfile) }
27
+ rescue Errno::EEXIST
28
+ check_pid
29
+ retry
30
+ end
31
+
32
+ def delete_pid
33
+ File.delete(pidfile) if File.exist?(pidfile)
34
+ end
35
+
36
+ def check_pid
37
+ case pid_status(pidfile)
38
+ when :running, :not_owned
39
+ abort "A server is already running. Check #{pidfile}"
40
+ when :dead
41
+ File.delete(pidfile)
42
+ end
43
+ end
44
+
45
+ def pid_status(pidfile)
46
+ return :exited unless File.exist?(pidfile)
47
+
48
+ pid = ::File.read(pidfile).to_i
49
+ return :dead if pid.zero?
50
+
51
+ Process.kill(0, pid) # check process status
52
+ :running
53
+ rescue Errno::ESRCH
54
+ :dead
55
+ rescue Errno::EPERM
56
+ :not_owned
57
+ end
58
+ end
59
+ end
data/lib/good_job/job.rb CHANGED
@@ -15,6 +15,8 @@ module GoodJob
15
15
 
16
16
  self.table_name = 'good_jobs'.freeze
17
17
 
18
+ attr_readonly :serialized_params
19
+
18
20
  # Parse a string representing a group of queues into a more readable data
19
21
  # structure.
20
22
  # @return [Hash]
@@ -72,6 +74,12 @@ module GoodJob
72
74
  # @return [ActiveRecord::Relation]
73
75
  scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
74
76
 
77
+ # Order jobs by scheduled (unscheduled or soonest first).
78
+ # @!method schedule_ordered
79
+ # @!scope class
80
+ # @return [ActiveRecord::Relation]
81
+ scope :schedule_ordered, -> { order(Arel.sql('COALESCE(scheduled_at, created_at) ASC')) }
82
+
75
83
  # Get Jobs were completed before the given timestamp. If no timestamp is
76
84
  # provided, get all jobs that have been completed. By default, GoodJob
77
85
  # deletes jobs after they are completed and this will find no jobs.
@@ -147,6 +155,23 @@ module GoodJob
147
155
  [good_job, result, error] if good_job
148
156
  end
149
157
 
158
+ # Fetches the scheduled execution time of the next eligible Job(s).
159
+ # @return [Array<(DateTime)>]
160
+ def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
161
+ query = advisory_unlocked.unfinished.schedule_ordered
162
+
163
+ after ||= Time.current
164
+ after_query = query.where('scheduled_at > ?', after).or query.where(scheduled_at: nil).where('created_at > ?', after)
165
+ after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
166
+
167
+ if now_limit&.positive?
168
+ now_query = query.where('scheduled_at < ?', Time.current).or query.where(scheduled_at: nil)
169
+ now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
170
+ end
171
+
172
+ Array(now_at) + after_at
173
+ end
174
+
150
175
  # Places an ActiveJob job on a queue by creating a new {Job} record.
151
176
  # @param active_job [ActiveJob::Base]
152
177
  # The job to enqueue.
@@ -0,0 +1,74 @@
1
+ require 'concurrent/delay'
2
+
3
+ module GoodJob
4
+ #
5
+ # JobPerformer queries the database for jobs and performs them on behalf of a
6
+ # {Scheduler}. It mainly functions as glue between a {Scheduler} and the jobs
7
+ # it should be executing.
8
+ #
9
+ # The JobPerformer must be safe to execute across multiple threads.
10
+ #
11
+ class JobPerformer
12
+ # @param queue_string [String] Queues to execute jobs from
13
+ def initialize(queue_string)
14
+ @queue_string = queue_string
15
+
16
+ @job_query = Concurrent::Delay.new { GoodJob::Job.queue_string(queue_string) }
17
+ @parsed_queues = Concurrent::Delay.new { GoodJob::Job.queue_parser(queue_string) }
18
+ end
19
+
20
+ # A meaningful name to identify the performer in logs and for debugging.
21
+ # @return [String] The queues from which Jobs are worked
22
+ def name
23
+ @queue_string
24
+ end
25
+
26
+ # Perform the next eligible job
27
+ # @return [nil, Object] Returns job result or +nil+ if no job was found
28
+ def next
29
+ job_query.perform_with_advisory_lock
30
+ end
31
+
32
+ # Tests whether this performer should be used in GoodJob's current state.
33
+ #
34
+ # For example, state will be a LISTEN/NOTIFY message that is passed down
35
+ # from the Notifier to the Scheduler. The Scheduler is able to ask
36
+ # its performer "does this message relate to you?", and if not, ignore it
37
+ # to minimize thread wake-ups, database queries, and thundering herds.
38
+ #
39
+ # @return [Boolean] whether the performer's {#next} method should be
40
+ # called in the current state.
41
+ def next?(state = {})
42
+ return true unless state[:queue_name]
43
+
44
+ if parsed_queues[:exclude]
45
+ parsed_queues[:exclude].exclude?(state[:queue_name])
46
+ elsif parsed_queues[:include]
47
+ parsed_queues[:include].include?(state[:queue_name])
48
+ else
49
+ true
50
+ end
51
+ end
52
+
53
+ # The Returns timestamps of when next tasks may be available.
54
+ # @param after [DateTime, Time, nil] future jobs scheduled after this time
55
+ # @param limit [Integer] number of future timestamps to return
56
+ # @param now_limit [Integer] number of past timestamps to return
57
+ # @return [Array<(Time, Timestamp)>, nil]
58
+ def next_at(after: nil, limit: nil, now_limit: nil)
59
+ job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :queue_string
65
+
66
+ def job_query
67
+ @job_query.value
68
+ end
69
+
70
+ def parsed_queues
71
+ @parsed_queues.value
72
+ end
73
+ end
74
+ end
@@ -92,8 +92,6 @@ module GoodJob
92
92
  # @return [ActiveRecord::Relation]
93
93
  scope :owns_advisory_locked, -> { joins_advisory_locks.where('"pg_locks"."pid" = pg_backend_pid()') }
94
94
 
95
- # @!attribute [r] create_with_advisory_lock
96
- # @return [Boolean]
97
95
  # Whether an advisory lock should be acquired in the same transaction
98
96
  # that created the record.
99
97
  #
@@ -107,6 +105,8 @@ module GoodJob
107
105
  # record = MyLockableRecord.create(create_with_advisory_lock: true)
108
106
  # record.advisory_locked?
109
107
  # => true
108
+ #
109
+ # @return [Boolean]
110
110
  attr_accessor :create_with_advisory_lock
111
111
 
112
112
  after_create -> { advisory_lock }, if: :create_with_advisory_lock
@@ -1,16 +1,16 @@
1
1
  module GoodJob
2
2
  # Delegates the interface of a single {Scheduler} to multiple Schedulers.
3
3
  class MultiScheduler
4
- # @return [array<Scheduler>] List of the scheduler delegates
4
+ # @return [Array<Scheduler>] List of the scheduler delegates
5
5
  attr_reader :schedulers
6
6
 
7
7
  def initialize(schedulers)
8
8
  @schedulers = schedulers
9
9
  end
10
10
 
11
- # Delegates to {Scheduler#shutdown}.
12
- def shutdown(wait: true)
13
- schedulers.each { |s| s.shutdown(wait: wait) }
11
+ # Delegates to {Scheduler#running?}.
12
+ def running?
13
+ schedulers.all?(&:running?)
14
14
  end
15
15
 
16
16
  # Delegates to {Scheduler#shutdown?}.
@@ -18,9 +18,14 @@ module GoodJob
18
18
  schedulers.all?(&:shutdown?)
19
19
  end
20
20
 
21
+ # Delegates to {Scheduler#shutdown}.
22
+ def shutdown(timeout: -1)
23
+ GoodJob._shutdown_all(schedulers, timeout: timeout)
24
+ end
25
+
21
26
  # Delegates to {Scheduler#restart}.
22
- def restart(wait: true)
23
- schedulers.each { |s| s.restart(wait: wait) }
27
+ def restart(timeout: -1)
28
+ GoodJob._shutdown_all(schedulers, :restart, timeout: timeout)
24
29
  end
25
30
 
26
31
  # Delegates to {Scheduler#create_thread}.
@@ -15,7 +15,7 @@ module GoodJob # :nodoc:
15
15
  # Default Postgres channel for LISTEN/NOTIFY
16
16
  CHANNEL = 'good_job'.freeze
17
17
  # Defaults for instance of Concurrent::ThreadPoolExecutor
18
- POOL_OPTIONS = {
18
+ EXECUTOR_OPTIONS = {
19
19
  name: name,
20
20
  min_threads: 0,
21
21
  max_threads: 1,
@@ -30,7 +30,7 @@ module GoodJob # :nodoc:
30
30
  # @!attribute [r] instances
31
31
  # @!scope class
32
32
  # List of all instantiated Notifiers in the current process.
33
- # @return [array<GoodJob:Adapter>]
33
+ # @return [Array<GoodJob:Adapter>]
34
34
  cattr_reader :instances, default: [], instance_reader: false
35
35
 
36
36
  # Send a message via Postgres NOTIFY
@@ -53,7 +53,7 @@ module GoodJob # :nodoc:
53
53
 
54
54
  self.class.instances << self
55
55
 
56
- create_pool
56
+ create_executor
57
57
  listen
58
58
  end
59
59
 
@@ -63,34 +63,43 @@ module GoodJob # :nodoc:
63
63
  @listening.true?
64
64
  end
65
65
 
66
- # Restart the notifier.
67
- # When shutdown, start; or shutdown and start.
68
- # @param wait [Boolean] Wait for background thread to finish
69
- # @return [void]
70
- def restart(wait: true)
71
- shutdown(wait: wait)
72
- create_pool
73
- listen
74
- end
66
+ # Tests whether the notifier is running.
67
+ # @return [true, false, nil]
68
+ delegate :running?, to: :executor, allow_nil: true
69
+
70
+ # Tests whether the scheduler is shutdown.
71
+ # @return [true, false, nil]
72
+ delegate :shutdown?, to: :executor, allow_nil: true
75
73
 
76
74
  # Shut down the notifier.
77
75
  # This stops the background LISTENing thread.
78
- # If +wait+ is +true+, the notifier will wait for background thread to shutdown.
79
- # If +wait+ is +false+, this method will return immediately even though threads may still be running.
80
76
  # Use {#shutdown?} to determine whether threads have stopped.
81
- # @param wait [Boolean] Wait for actively executing threads to finish
77
+ # @param timeout [nil, Numeric] Seconds to wait for active threads.
78
+ #
79
+ # * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
80
+ # * +-1+, the scheduler will wait until the shutdown is complete.
81
+ # * +0+, the scheduler will immediately shutdown and stop any threads.
82
+ # * A positive number will wait that many seconds before stopping any remaining active threads.
82
83
  # @return [void]
83
- def shutdown(wait: true)
84
- return unless @pool.running?
84
+ def shutdown(timeout: -1)
85
+ return if executor.nil? || executor.shutdown?
85
86
 
86
- @pool.shutdown
87
- @pool.wait_for_termination if wait
87
+ executor.shutdown if executor.running?
88
+
89
+ if executor.shuttingdown? && timeout # rubocop:disable Style/GuardClause
90
+ executor_wait = timeout.negative? ? nil : timeout
91
+ executor.kill unless executor.wait_for_termination(executor_wait)
92
+ end
88
93
  end
89
94
 
90
- # Tests whether the notifier is shutdown.
91
- # @return [true, false, nil]
92
- def shutdown?
93
- !@pool.running?
95
+ # Restart the notifier.
96
+ # When shutdown, start; or shutdown and start.
97
+ # @param timeout [nil, Numeric] Seconds to wait; shares same values as {#shutdown}.
98
+ # @return [void]
99
+ def restart(timeout: -1)
100
+ shutdown(timeout: timeout) if running?
101
+ create_executor
102
+ listen
94
103
  end
95
104
 
96
105
  # Invoked on completion of ThreadPoolExecutor task
@@ -109,36 +118,36 @@ module GoodJob # :nodoc:
109
118
 
110
119
  private
111
120
 
112
- def create_pool
113
- @pool = Concurrent::ThreadPoolExecutor.new(POOL_OPTIONS)
121
+ attr_reader :executor
122
+
123
+ def create_executor
124
+ @executor = Concurrent::ThreadPoolExecutor.new(EXECUTOR_OPTIONS)
114
125
  end
115
126
 
116
127
  def listen
117
- future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
128
+ future = Concurrent::Future.new(args: [@recipients, executor, @listening], executor: @executor) do |thr_recipients, thr_executor, thr_listening|
118
129
  with_listen_connection do |conn|
119
130
  ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
120
131
  conn.async_exec("LISTEN #{CHANNEL}").clear
121
132
  end
122
133
 
123
134
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
124
- while pool.running?
125
- listening.make_true
135
+ thr_listening.make_true
136
+ while thr_executor.running?
126
137
  conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
127
- listening.make_false
128
138
  next unless channel == CHANNEL
129
139
 
130
140
  ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
131
141
  parsed_payload = JSON.parse(payload, symbolize_names: true)
132
- recipients.each do |recipient|
142
+ thr_recipients.each do |recipient|
133
143
  target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
134
144
  target.send(method_name, parsed_payload)
135
145
  end
136
146
  end
137
- listening.make_false
138
147
  end
139
148
  end
140
149
  ensure
141
- listening.make_false
150
+ thr_listening.make_false
142
151
  ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
143
152
  conn.async_exec("UNLISTEN *").clear
144
153
  end