pgbus 0.0.1 → 0.1.2
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/README.md +37 -3
- data/Rakefile +98 -1
- data/app/controllers/pgbus/application_controller.rb +8 -0
- data/app/controllers/pgbus/recurring_tasks_controller.rb +36 -0
- data/app/helpers/pgbus/application_helper.rb +41 -0
- data/app/models/pgbus/application_record.rb +7 -0
- data/app/models/pgbus/batch_entry.rb +31 -0
- data/app/models/pgbus/blocked_execution.rb +40 -0
- data/app/models/pgbus/process_entry.rb +9 -0
- data/app/models/pgbus/processed_event.rb +9 -0
- data/app/models/pgbus/recurring_execution.rb +33 -0
- data/app/models/pgbus/recurring_task.rb +42 -0
- data/app/models/pgbus/semaphore.rb +29 -0
- data/app/views/layouts/pgbus/application.html.erb +1 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +9 -1
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +55 -18
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +46 -8
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +79 -0
- data/app/views/pgbus/recurring_tasks/index.html.erb +6 -0
- data/app/views/pgbus/recurring_tasks/show.html.erb +122 -0
- data/config/routes.rb +7 -0
- data/lib/active_job/queue_adapters/pgbus_adapter.rb +29 -0
- data/lib/generators/pgbus/add_recurring_generator.rb +56 -0
- data/lib/generators/pgbus/install_generator.rb +76 -2
- data/lib/generators/pgbus/templates/add_recurring_tables.rb.erb +31 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +72 -4
- data/lib/generators/pgbus/templates/recurring.yml.erb +40 -0
- data/lib/generators/pgbus/templates/upgrade_pgmq.rb.erb +30 -0
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +60 -0
- data/lib/pgbus/active_job/adapter.rb +3 -6
- data/lib/pgbus/active_job/executor.rb +26 -12
- data/lib/pgbus/batch.rb +65 -72
- data/lib/pgbus/cli.rb +11 -16
- data/lib/pgbus/client.rb +32 -15
- data/lib/pgbus/concurrency/blocked_execution.rb +32 -37
- data/lib/pgbus/concurrency/semaphore.rb +11 -39
- data/lib/pgbus/concurrency.rb +10 -2
- data/lib/pgbus/configuration.rb +48 -0
- data/lib/pgbus/engine.rb +19 -1
- data/lib/pgbus/event_bus/handler.rb +10 -23
- data/lib/pgbus/instrumentation.rb +29 -0
- data/lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql +2123 -0
- data/lib/pgbus/pgmq_schema.rb +159 -0
- data/lib/pgbus/process/consumer.rb +17 -9
- data/lib/pgbus/process/dispatcher.rb +33 -41
- data/lib/pgbus/process/heartbeat.rb +15 -23
- data/lib/pgbus/process/signal_handler.rb +23 -1
- data/lib/pgbus/process/supervisor.rb +79 -2
- data/lib/pgbus/process/worker.rb +42 -13
- data/lib/pgbus/recurring/already_recorded.rb +7 -0
- data/lib/pgbus/recurring/command_job.rb +28 -0
- data/lib/pgbus/recurring/config_loader.rb +35 -0
- data/lib/pgbus/recurring/schedule.rb +102 -0
- data/lib/pgbus/recurring/scheduler.rb +102 -0
- data/lib/pgbus/recurring/task.rb +111 -0
- data/lib/pgbus/serializer.rb +16 -6
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +217 -36
- data/lib/pgbus.rb +8 -0
- data/lib/tasks/pgbus_pgmq.rake +62 -0
- metadata +51 -24
- data/.bun-version +0 -1
- data/.claude/commands/architect.md +0 -100
- data/.claude/commands/github-review-comments.md +0 -237
- data/.claude/commands/lfg.md +0 -271
- data/.claude/commands/review-pr.md +0 -69
- data/.claude/commands/security.md +0 -122
- data/.claude/commands/tdd.md +0 -148
- data/.claude/rules/agents.md +0 -49
- data/.claude/rules/coding-style.md +0 -91
- data/.claude/rules/git-workflow.md +0 -56
- data/.claude/rules/performance.md +0 -73
- data/.claude/rules/testing.md +0 -67
- data/CLAUDE.md +0 -80
- data/CODE_OF_CONDUCT.md +0 -10
- data/bun.lock +0 -18
- data/docs/README.md +0 -28
- data/docs/switch_from_good_job.md +0 -279
- data/docs/switch_from_sidekiq.md +0 -226
- data/docs/switch_from_solid_queue.md +0 -247
- data/package.json +0 -9
- data/sig/pgbus.rbs +0 -4
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -28,14 +28,16 @@ module Pgbus
|
|
|
28
28
|
def run
|
|
29
29
|
setup_signals
|
|
30
30
|
start_heartbeat
|
|
31
|
+
resolve_wildcard_queues
|
|
31
32
|
Pgbus.logger.info { "[Pgbus] Worker started: queues=#{queues.join(",")} threads=#{threads} pid=#{::Process.pid}" }
|
|
32
33
|
|
|
33
34
|
loop do
|
|
34
|
-
break if @shutting_down
|
|
35
|
-
break if recycle_needed?
|
|
36
|
-
|
|
37
35
|
process_signals
|
|
38
|
-
|
|
36
|
+
break if @shutting_down && @pool.queue_length.zero?
|
|
37
|
+
break if recycle_needed? && @pool.queue_length.zero?
|
|
38
|
+
|
|
39
|
+
claim_and_execute unless @shutting_down || recycle_needed?
|
|
40
|
+
interruptible_sleep(config.polling_interval) if (@shutting_down || recycle_needed?) && !@pool.queue_length.zero?
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
shutdown
|
|
@@ -56,29 +58,31 @@ module Pgbus
|
|
|
56
58
|
|
|
57
59
|
def claim_and_execute
|
|
58
60
|
idle = @pool.max_length - @pool.queue_length
|
|
59
|
-
return
|
|
61
|
+
return interruptible_sleep(config.polling_interval) if idle <= 0
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
tagged_messages = fetch_messages(idle)
|
|
62
64
|
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
+
if tagged_messages.empty?
|
|
66
|
+
interruptible_sleep(config.polling_interval)
|
|
65
67
|
return
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
queue_name = message.respond_to?(:queue_name) ? message.queue_name : queues.first
|
|
70
|
+
tagged_messages.each do |queue_name, message|
|
|
70
71
|
@pool.post { process_message(message, queue_name) }
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
75
|
+
# Returns an array of [queue_name, message] pairs so we always know
|
|
76
|
+
# which queue each message came from (PGMQ messages don't carry this).
|
|
74
77
|
def fetch_messages(qty)
|
|
75
78
|
if queues.size == 1
|
|
76
|
-
|
|
79
|
+
queue = queues.first
|
|
80
|
+
messages = Pgbus.client.read_batch(queue, qty: qty) || []
|
|
81
|
+
messages.map { |m| [queue, m] }
|
|
77
82
|
else
|
|
78
|
-
# Multi-queue read: read from each queue proportionally
|
|
79
83
|
per_queue = [(qty / queues.size.to_f).ceil, 1].max
|
|
80
84
|
queues.flat_map do |q|
|
|
81
|
-
Pgbus.client.read_batch(q, qty: per_queue) || []
|
|
85
|
+
(Pgbus.client.read_batch(q, qty: per_queue) || []).map { |m| [q, m] }
|
|
82
86
|
end.first(qty)
|
|
83
87
|
end
|
|
84
88
|
rescue StandardError => e
|
|
@@ -95,6 +99,31 @@ module Pgbus
|
|
|
95
99
|
Pgbus.logger.error { "[Pgbus] Unhandled error processing message: #{e.message}" }
|
|
96
100
|
end
|
|
97
101
|
|
|
102
|
+
# Resolve "*" to all non-DLQ queues from pgmq.meta, stripping the prefix.
|
|
103
|
+
# Called once at startup. If no wildcard, this is a no-op.
|
|
104
|
+
def resolve_wildcard_queues
|
|
105
|
+
return unless @queues.include?("*")
|
|
106
|
+
|
|
107
|
+
dlq_suffix = config.dead_letter_queue_suffix
|
|
108
|
+
prefix = "#{config.queue_prefix}_"
|
|
109
|
+
|
|
110
|
+
all_queues = ActiveRecord::Base.connection.select_values("SELECT queue_name FROM pgmq.meta ORDER BY queue_name")
|
|
111
|
+
resolved = all_queues
|
|
112
|
+
.reject { |q| q.end_with?(dlq_suffix) }
|
|
113
|
+
.map { |q| q.delete_prefix(prefix) }
|
|
114
|
+
|
|
115
|
+
if resolved.empty?
|
|
116
|
+
Pgbus.logger.warn { "[Pgbus] Wildcard queue '*' resolved to no queues — falling back to default" }
|
|
117
|
+
@queues = [config.default_queue]
|
|
118
|
+
else
|
|
119
|
+
@queues = resolved
|
|
120
|
+
Pgbus.logger.info { "[Pgbus] Wildcard queue '*' resolved to: #{@queues.join(", ")}" }
|
|
121
|
+
end
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
Pgbus.logger.error { "[Pgbus] Failed to resolve wildcard queues: #{e.message} — falling back to default" }
|
|
124
|
+
@queues = [config.default_queue]
|
|
125
|
+
end
|
|
126
|
+
|
|
98
127
|
def recycle_needed?
|
|
99
128
|
exceeded_max_jobs? || exceeded_max_memory? || exceeded_max_lifetime?
|
|
100
129
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Recurring
|
|
5
|
+
# Job class for command-based recurring tasks.
|
|
6
|
+
# Evaluates a method call chain on a constant, e.g. "OldRecord.cleanup!".
|
|
7
|
+
#
|
|
8
|
+
# Only supports `ConstantName.method_name` and `ConstantName.method_name(args)`.
|
|
9
|
+
# Rejects arbitrary Ruby expressions for safety.
|
|
10
|
+
class CommandJob < ::ActiveJob::Base
|
|
11
|
+
SAFE_COMMAND = /\A([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\.([a-z_][a-z0-9_!?]*)\z/
|
|
12
|
+
|
|
13
|
+
def perform(command)
|
|
14
|
+
match = SAFE_COMMAND.match(command)
|
|
15
|
+
unless match
|
|
16
|
+
raise ArgumentError,
|
|
17
|
+
"Unsafe recurring command: #{command.inspect}. " \
|
|
18
|
+
"Must be in the form 'ClassName.method_name'."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
klass = match[1].safe_constantize
|
|
22
|
+
raise ArgumentError, "Unknown class in recurring command: #{match[1]}" unless klass
|
|
23
|
+
|
|
24
|
+
klass.public_send(match[2])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "erb"
|
|
5
|
+
|
|
6
|
+
module Pgbus
|
|
7
|
+
module Recurring
|
|
8
|
+
module ConfigLoader
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def load(path, env: nil)
|
|
12
|
+
return {} unless path && File.exist?(path.to_s)
|
|
13
|
+
|
|
14
|
+
env ||= detect_env
|
|
15
|
+
raw = File.read(path)
|
|
16
|
+
parsed = YAML.safe_load(ERB.new(raw).result, permitted_classes: [Symbol], aliases: true)
|
|
17
|
+
return {} unless parsed.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
# If the parsed hash has an environment key, use that subtree
|
|
20
|
+
parsed.key?(env) ? parsed.fetch(env, {}) : parsed
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
Pgbus.logger.error { "[Pgbus] Failed to load recurring config from #{path}: #{e.message}" }
|
|
23
|
+
{}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def detect_env
|
|
27
|
+
if defined?(Rails)
|
|
28
|
+
Rails.env.to_s
|
|
29
|
+
else
|
|
30
|
+
ENV.fetch("PGBUS_ENV", "development")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Recurring
|
|
5
|
+
class Schedule
|
|
6
|
+
attr_reader :tasks
|
|
7
|
+
|
|
8
|
+
def initialize(config: Pgbus.configuration)
|
|
9
|
+
@config = config
|
|
10
|
+
@tasks = load_tasks
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def due_tasks(time = Time.now)
|
|
14
|
+
tasks.select { |task| task_due?(task, time) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def enqueue_task(task, run_at:)
|
|
18
|
+
queue = resolve_queue(task)
|
|
19
|
+
|
|
20
|
+
RecurringExecution.record(task.key, run_at) do
|
|
21
|
+
payload = build_payload(task)
|
|
22
|
+
headers = build_headers(task, run_at)
|
|
23
|
+
|
|
24
|
+
Pgbus.client.ensure_queue(queue)
|
|
25
|
+
Pgbus.client.send_message(queue, payload, headers: headers)
|
|
26
|
+
|
|
27
|
+
Pgbus.logger.info do
|
|
28
|
+
"[Pgbus] Enqueued recurring task #{task.key} (#{task.class_name || task.command}) " \
|
|
29
|
+
"for run_at=#{run_at.iso8601}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
rescue AlreadyRecorded
|
|
33
|
+
Pgbus.logger.debug { "[Pgbus] Recurring task #{task.key} already enqueued for #{run_at.iso8601}" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_payload(task)
|
|
37
|
+
if task.command
|
|
38
|
+
{
|
|
39
|
+
"job_class" => "Pgbus::Recurring::CommandJob",
|
|
40
|
+
"arguments" => [task.command],
|
|
41
|
+
"queue_name" => task.queue_name || @config.default_queue,
|
|
42
|
+
"priority" => nil
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
{
|
|
46
|
+
"job_class" => task.class_name,
|
|
47
|
+
"arguments" => task.arguments,
|
|
48
|
+
"queue_name" => task.queue_name || @config.default_queue,
|
|
49
|
+
"priority" => task.priority.zero? ? nil : task.priority
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def load_tasks
|
|
57
|
+
raw = @config.recurring_tasks || {}
|
|
58
|
+
raw.filter_map do |key, options|
|
|
59
|
+
options = options.transform_keys(&:to_s).transform_keys(&:to_sym) if options.is_a?(Hash)
|
|
60
|
+
task = Task.from_configuration(key, **(options || {}))
|
|
61
|
+
if task.valid?
|
|
62
|
+
task
|
|
63
|
+
else
|
|
64
|
+
Pgbus.logger.warn { "[Pgbus] Skipping invalid recurring task '#{key}': #{task.errors.join(", ")}" }
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def task_due?(task, time)
|
|
71
|
+
# A task is due when its most recent cron occurrence (previous_time)
|
|
72
|
+
# falls within the current tick window. We also check match? to
|
|
73
|
+
# handle the exact-boundary case where time == cron time.
|
|
74
|
+
cron = task.parsed_schedule
|
|
75
|
+
return false unless cron
|
|
76
|
+
|
|
77
|
+
# Check if `time` itself matches the cron (exact boundary hit)
|
|
78
|
+
return true if cron.match?(time)
|
|
79
|
+
|
|
80
|
+
# Check if the previous occurrence was recent enough that we should
|
|
81
|
+
# still fire it (handles the case where we tick slightly after the
|
|
82
|
+
# cron time). The window is the scheduler interval.
|
|
83
|
+
prev = task.previous_time(time)
|
|
84
|
+
return false unless prev
|
|
85
|
+
|
|
86
|
+
(time - prev) <= @config.recurring_schedule_interval
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resolve_queue(task)
|
|
90
|
+
@config.queue_name(task.queue_name || @config.default_queue)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_headers(task, run_at)
|
|
94
|
+
{
|
|
95
|
+
"pgbus.recurring_key" => task.key,
|
|
96
|
+
"pgbus.recurring_run_at" => run_at.iso8601,
|
|
97
|
+
"pgbus.recurring_schedule" => task.schedule
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Recurring
|
|
5
|
+
class Scheduler
|
|
6
|
+
include Process::SignalHandler
|
|
7
|
+
|
|
8
|
+
attr_reader :schedule, :config
|
|
9
|
+
|
|
10
|
+
def initialize(config: Pgbus.configuration)
|
|
11
|
+
@config = config
|
|
12
|
+
@schedule = Schedule.new(config: config)
|
|
13
|
+
@shutting_down = false
|
|
14
|
+
@last_runs = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
setup_signals
|
|
19
|
+
start_heartbeat
|
|
20
|
+
|
|
21
|
+
Pgbus.logger.info do
|
|
22
|
+
"[Pgbus] Scheduler started: #{schedule.tasks.size} recurring tasks, " \
|
|
23
|
+
"interval=#{config.recurring_schedule_interval}s"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
break if @shutting_down
|
|
28
|
+
|
|
29
|
+
process_signals
|
|
30
|
+
break if @shutting_down
|
|
31
|
+
|
|
32
|
+
tick(Time.now)
|
|
33
|
+
break if @shutting_down
|
|
34
|
+
|
|
35
|
+
interruptible_sleep(config.recurring_schedule_interval)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
shutdown
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def tick(now)
|
|
42
|
+
schedule.due_tasks(now).each do |task|
|
|
43
|
+
run_at = task.previous_time(now)
|
|
44
|
+
next unless run_at
|
|
45
|
+
|
|
46
|
+
schedule.enqueue_task(task, run_at: run_at)
|
|
47
|
+
@last_runs[task.key] = now
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Pgbus.logger.error do
|
|
50
|
+
"[Pgbus] Error scheduling recurring task #{task.key}: #{e.class}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def last_run_at(key)
|
|
56
|
+
@last_runs[key]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def task_statuses
|
|
60
|
+
schedule.tasks.map do |task|
|
|
61
|
+
{
|
|
62
|
+
key: task.key,
|
|
63
|
+
class_name: task.class_name,
|
|
64
|
+
command: task.command,
|
|
65
|
+
schedule: task.schedule,
|
|
66
|
+
human_schedule: task.human_schedule,
|
|
67
|
+
queue_name: task.queue_name,
|
|
68
|
+
arguments: task.arguments,
|
|
69
|
+
priority: task.priority,
|
|
70
|
+
description: task.description,
|
|
71
|
+
next_run_at: task.next_time,
|
|
72
|
+
last_run_at: @last_runs[task.key]
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def graceful_shutdown
|
|
78
|
+
@shutting_down = true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def immediate_shutdown
|
|
82
|
+
@shutting_down = true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def start_heartbeat
|
|
88
|
+
@heartbeat = Process::Heartbeat.new(
|
|
89
|
+
kind: "scheduler",
|
|
90
|
+
metadata: { pid: ::Process.pid, tasks: schedule.tasks.size }
|
|
91
|
+
)
|
|
92
|
+
@heartbeat.start
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def shutdown
|
|
96
|
+
@heartbeat&.stop
|
|
97
|
+
restore_signals
|
|
98
|
+
Pgbus.logger.info { "[Pgbus] Scheduler stopped" }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fugit"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module Recurring
|
|
7
|
+
class Task
|
|
8
|
+
attr_reader :key, :class_name, :command, :schedule, :queue_name,
|
|
9
|
+
:arguments, :priority, :description
|
|
10
|
+
|
|
11
|
+
def self.from_configuration(key, **options)
|
|
12
|
+
options = options.transform_keys(&:to_sym)
|
|
13
|
+
new(
|
|
14
|
+
key: key,
|
|
15
|
+
class_name: options[:class],
|
|
16
|
+
command: options[:command],
|
|
17
|
+
schedule: options[:schedule],
|
|
18
|
+
queue_name: options[:queue],
|
|
19
|
+
arguments: Array(options[:args]),
|
|
20
|
+
priority: options.fetch(:priority, 0).to_i,
|
|
21
|
+
description: options[:description]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(key:, class_name: nil, command: nil, schedule: nil,
|
|
26
|
+
queue_name: nil, arguments: [], priority: 0, description: nil)
|
|
27
|
+
@key = key
|
|
28
|
+
@class_name = class_name
|
|
29
|
+
@command = command
|
|
30
|
+
@schedule = schedule
|
|
31
|
+
@queue_name = queue_name
|
|
32
|
+
@arguments = arguments || []
|
|
33
|
+
@priority = priority || 0
|
|
34
|
+
@description = description
|
|
35
|
+
@errors = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid?
|
|
39
|
+
@errors = []
|
|
40
|
+
validate!
|
|
41
|
+
@errors.empty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def errors
|
|
45
|
+
valid? unless defined?(@validated)
|
|
46
|
+
@errors
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parsed_schedule
|
|
50
|
+
@parsed_schedule ||= parse_schedule
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def next_time(from = Time.now)
|
|
54
|
+
parsed_schedule&.next_time(from)&.to_t
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def previous_time(from = Time.now)
|
|
58
|
+
parsed_schedule&.previous_time(from)&.to_t
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def human_schedule
|
|
62
|
+
return nil unless parsed_schedule
|
|
63
|
+
|
|
64
|
+
parsed_schedule.to_cron_s
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def job_class
|
|
68
|
+
class_name&.safe_constantize
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_h
|
|
72
|
+
{
|
|
73
|
+
key: key,
|
|
74
|
+
class_name: class_name,
|
|
75
|
+
command: command,
|
|
76
|
+
schedule: schedule,
|
|
77
|
+
queue_name: queue_name,
|
|
78
|
+
arguments: arguments,
|
|
79
|
+
priority: priority,
|
|
80
|
+
description: description
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def validate!
|
|
87
|
+
@validated = true
|
|
88
|
+
|
|
89
|
+
if schedule.nil? || schedule.to_s.strip.empty?
|
|
90
|
+
@errors << "Schedule is required"
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@errors << "Either class or command is required" if class_name.to_s.strip.empty? && command.to_s.strip.empty?
|
|
95
|
+
|
|
96
|
+
return if parsed_schedule.is_a?(Fugit::Cron)
|
|
97
|
+
|
|
98
|
+
@errors << "Schedule '#{schedule}' is not a valid cron expression"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def parse_schedule
|
|
102
|
+
return nil if schedule.nil? || schedule.to_s.strip.empty?
|
|
103
|
+
|
|
104
|
+
parsed = Fugit.parse(schedule.to_s, multi: :fail)
|
|
105
|
+
parsed.is_a?(Fugit::Cron) ? parsed : nil
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/pgbus/serializer.rb
CHANGED
|
@@ -7,15 +7,25 @@ module Pgbus
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
def serialize_job(active_job)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Instrumentation.instrument("pgbus.serializer.serialize", kind: :job) do
|
|
11
|
+
data = active_job.serialize
|
|
12
|
+
# GlobalID is handled by ActiveJob's serialize — it converts AR objects
|
|
13
|
+
# to GlobalID URIs automatically. We just JSON-encode the result.
|
|
14
|
+
JSON.generate(data)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def serialize_job_hash(active_job)
|
|
19
|
+
Instrumentation.instrument("pgbus.serializer.serialize", kind: :job) do
|
|
20
|
+
active_job.serialize
|
|
21
|
+
end
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def deserialize_job(json_string)
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
Instrumentation.instrument("pgbus.serializer.deserialize", kind: :job) do
|
|
26
|
+
data = JSON.parse(json_string)
|
|
27
|
+
ActiveJob::Base.deserialize(data)
|
|
28
|
+
end
|
|
19
29
|
end
|
|
20
30
|
|
|
21
31
|
def serialize_event(event)
|
data/lib/pgbus/version.rb
CHANGED