busybee 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -7
- data/README.md +70 -42
- data/docs/client/quick_start.md +279 -0
- data/docs/client.md +825 -0
- data/docs/configuration.md +550 -0
- data/docs/grpc.md +50 -25
- data/docs/testing.md +118 -28
- data/docs/workers.md +982 -0
- data/exe/busybee +6 -0
- data/lib/busybee/cli.rb +173 -0
- data/lib/busybee/client/error_handling.rb +37 -0
- data/lib/busybee/client/job_operations.rb +236 -0
- data/lib/busybee/client/message_operations.rb +84 -0
- data/lib/busybee/client/process_operations.rb +108 -0
- data/lib/busybee/client/variable_operations.rb +64 -0
- data/lib/busybee/client.rb +87 -0
- data/lib/busybee/configure.rb +290 -0
- data/lib/busybee/credentials/camunda_cloud.rb +58 -0
- data/lib/busybee/credentials/insecure.rb +24 -0
- data/lib/busybee/credentials/oauth.rb +157 -0
- data/lib/busybee/credentials/tls.rb +43 -0
- data/lib/busybee/credentials.rb +200 -0
- data/lib/busybee/defaults.rb +20 -0
- data/lib/busybee/error.rb +50 -0
- data/lib/busybee/grpc/error.rb +60 -0
- data/lib/busybee/grpc.rb +2 -2
- data/lib/busybee/job.rb +219 -0
- data/lib/busybee/job_stream.rb +85 -0
- data/lib/busybee/logging.rb +61 -0
- data/lib/busybee/railtie.rb +113 -0
- data/lib/busybee/runner/hybrid.rb +64 -0
- data/lib/busybee/runner/multi.rb +101 -0
- data/lib/busybee/runner/polling.rb +54 -0
- data/lib/busybee/runner/streaming.rb +159 -0
- data/lib/busybee/runner.rb +97 -0
- data/lib/busybee/runtime_config.rb +184 -0
- data/lib/busybee/serialization.rb +100 -0
- data/lib/busybee/testing/activated_job.rb +33 -8
- data/lib/busybee/testing/helpers/execution.rb +139 -0
- data/lib/busybee/testing/helpers/support.rb +78 -0
- data/lib/busybee/testing/helpers.rb +56 -66
- data/lib/busybee/testing/matchers/complete_job.rb +55 -0
- data/lib/busybee/testing/matchers/fail_job.rb +75 -0
- data/lib/busybee/testing/matchers/have_activated.rb +1 -1
- data/lib/busybee/testing/matchers/have_available_jobs.rb +44 -0
- data/lib/busybee/testing/matchers/throw_bpmn_error_on.rb +72 -0
- data/lib/busybee/testing.rb +5 -33
- data/lib/busybee/version.rb +1 -1
- data/lib/busybee/worker/configuration.rb +287 -0
- data/lib/busybee/worker/dsl.rb +187 -0
- data/lib/busybee/worker/shutdown.rb +27 -0
- data/lib/busybee/worker.rb +130 -0
- data/lib/busybee.rb +134 -2
- metadata +80 -3
data/exe/busybee
ADDED
data/lib/busybee/cli.rb
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Busybee
|
|
6
|
+
class CLI
|
|
7
|
+
attr_reader :runtime_config, :worker_class_names, :worker_classes
|
|
8
|
+
|
|
9
|
+
def self.main(args)
|
|
10
|
+
new(args).run
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(args)
|
|
14
|
+
@worker_class_names = []
|
|
15
|
+
@parsed_options = {}
|
|
16
|
+
parse_options!(args.dup)
|
|
17
|
+
load_environment!
|
|
18
|
+
extract_workers_from_yaml! if @parsed_options[:config_file]
|
|
19
|
+
load_workers!
|
|
20
|
+
build_config!
|
|
21
|
+
apply_global_config!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run
|
|
25
|
+
@runner = Runner.for(*@worker_classes, runtime_config: @runtime_config, client: client)
|
|
26
|
+
setup_signal_handlers!
|
|
27
|
+
@runner.run!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def client
|
|
33
|
+
@client ||= Busybee::Client.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_signal(_signal)
|
|
37
|
+
if @runner.stopping?
|
|
38
|
+
@runner.kill!
|
|
39
|
+
exit!(1)
|
|
40
|
+
else
|
|
41
|
+
@runner.stop!
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def setup_signal_handlers!
|
|
46
|
+
%w[INT QUIT TERM].each do |signal|
|
|
47
|
+
trap(signal) do
|
|
48
|
+
Thread.new { handle_signal(signal) }.join
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load_environment!
|
|
54
|
+
return if ENV["BUSYBEE_SKIP_RAILS"]
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
require "rails"
|
|
58
|
+
rescue LoadError
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
require "./config/environment"
|
|
63
|
+
rescue StandardError, LoadError => e
|
|
64
|
+
Busybee.logger&.error("Failed to load Rails environment: [#{e.class}] #{e.message}. " \
|
|
65
|
+
"Set BUSYBEE_SKIP_RAILS=1 to skip Rails loading.")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_workers_from_yaml!
|
|
69
|
+
@yaml_kwargs = RuntimeConfig.parse_yaml(@parsed_options[:config_file])
|
|
70
|
+
@worker_class_names = (@yaml_kwargs[:workers] || {}).keys
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_config!
|
|
74
|
+
if @yaml_kwargs
|
|
75
|
+
kwargs = @yaml_kwargs.dup
|
|
76
|
+
# Merge CLI process-wide flags into YAML-sourced kwargs
|
|
77
|
+
%i[log_format worker_name cluster_address].each do |field|
|
|
78
|
+
kwargs[field] = @parsed_options[field] if @parsed_options[field]
|
|
79
|
+
end
|
|
80
|
+
@runtime_config = RuntimeConfig.new(**kwargs)
|
|
81
|
+
else
|
|
82
|
+
@runtime_config = RuntimeConfig.new(**@parsed_options)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def load_workers!
|
|
87
|
+
raise Busybee::NoWorkersSpecified, "No worker classes specified" if @worker_class_names.empty?
|
|
88
|
+
|
|
89
|
+
@worker_classes = @worker_class_names.map do |name|
|
|
90
|
+
Kernel.const_get(name)
|
|
91
|
+
rescue NameError => e
|
|
92
|
+
raise Busybee::WorkerNotFound, "Could not load worker class '#{name}': #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_options!(args)
|
|
97
|
+
option_parser.parse!(args)
|
|
98
|
+
@worker_class_names = args
|
|
99
|
+
validate_config_exclusions!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_config_exclusions!
|
|
103
|
+
return unless @parsed_options[:config_file]
|
|
104
|
+
|
|
105
|
+
if @parsed_options[:worker_mode]
|
|
106
|
+
raise ArgumentError, "--config and --worker-mode are mutually exclusive. " \
|
|
107
|
+
"Set worker_mode in the YAML config file instead."
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return if @worker_class_names.empty?
|
|
111
|
+
|
|
112
|
+
raise ArgumentError, "--config and positional worker args are mutually exclusive. " \
|
|
113
|
+
"List workers in the YAML config file instead."
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def option_parser # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
117
|
+
OptionParser.new do |opts|
|
|
118
|
+
opts.banner = "Usage: busybee [options] WorkerClass [WorkerClass ...]"
|
|
119
|
+
|
|
120
|
+
opts.on("-v", "--version", "Print version and exit") do
|
|
121
|
+
puts Busybee::VERSION
|
|
122
|
+
exit
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
opts.on("-h", "--help", "Print this help message and exit") do
|
|
126
|
+
puts opts
|
|
127
|
+
exit
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
opts.on("-c", "--config FILE", "YAML configuration file") do |file|
|
|
131
|
+
@parsed_options[:config_file] = file
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
opts.on("-m", "--worker-mode MODE", "Worker mode (polling, streaming, hybrid)") do |mode|
|
|
135
|
+
@parsed_options[:worker_mode] = mode.to_sym
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
opts.on("-l", "--log-format FORMAT", "Log format (text, json)") do |format|
|
|
139
|
+
@parsed_options[:log_format] = format.to_sym
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
opts.on("-n", "--worker-name NAME", "Worker name for identification") do |name|
|
|
143
|
+
@parsed_options[:worker_name] = name
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
opts.on("-a", "--cluster-address ADDR", "Cluster address (host:port)") do |addr|
|
|
147
|
+
@parsed_options[:cluster_address] = addr
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Applies process-wide flags to gem-level config before runners start.
|
|
153
|
+
# Only sets values that were explicitly provided via CLI flags.
|
|
154
|
+
# Logs when overriding a value already set (e.g., by the Railtie).
|
|
155
|
+
def apply_global_config!
|
|
156
|
+
apply_global_setting!(:log_format)
|
|
157
|
+
apply_global_setting!(:worker_name)
|
|
158
|
+
apply_global_setting!(:cluster_address)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def apply_global_setting!(field)
|
|
162
|
+
cli_value = @runtime_config.public_send(field)
|
|
163
|
+
return unless cli_value
|
|
164
|
+
|
|
165
|
+
existing = Busybee.instance_variable_get(:"@#{field}")
|
|
166
|
+
if existing
|
|
167
|
+
flag = "--#{field.to_s.tr('_', '-')}"
|
|
168
|
+
Busybee.logger&.info("#{flag} overriding configured value: #{existing.inspect} → #{cli_value.inspect}")
|
|
169
|
+
end
|
|
170
|
+
Busybee.public_send(:"#{field}=", cli_value)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "busybee"
|
|
4
|
+
require "busybee/grpc/error"
|
|
5
|
+
require "busybee/logging"
|
|
6
|
+
|
|
7
|
+
module Busybee
|
|
8
|
+
class Client
|
|
9
|
+
# Provides GRPC error wrapping and optional retry logic.
|
|
10
|
+
module ErrorHandling
|
|
11
|
+
# Execute a block with optional GRPC retry and error wrapping.
|
|
12
|
+
# @yield Block that makes GRPC call
|
|
13
|
+
# @return Result of the block
|
|
14
|
+
# @raise [Busybee::GRPC::Error] Wrapped GRPC error
|
|
15
|
+
def with_retry
|
|
16
|
+
attempts = 0
|
|
17
|
+
max_attempts = Busybee.grpc_retry_enabled ? 2 : 1
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
attempts += 1
|
|
21
|
+
yield
|
|
22
|
+
rescue *Busybee.grpc_retry_errors => e
|
|
23
|
+
if attempts < max_attempts
|
|
24
|
+
Busybee::Logging.warn("GRPC call failed, retrying in #{Busybee.grpc_retry_delay_ms}ms",
|
|
25
|
+
error_class: e.class.name)
|
|
26
|
+
sleep(Busybee.grpc_retry_delay_ms / 1000.0)
|
|
27
|
+
retry
|
|
28
|
+
end
|
|
29
|
+
message = attempts > 1 ? "GRPC call failed after retry" : "GRPC call failed"
|
|
30
|
+
raise Busybee::GRPC::Error, message
|
|
31
|
+
rescue ::GRPC::BadStatus => e
|
|
32
|
+
raise Busybee::GRPC::Error, "GRPC call failed"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/duration"
|
|
5
|
+
require "busybee/grpc"
|
|
6
|
+
require "busybee/serialization"
|
|
7
|
+
|
|
8
|
+
module Busybee
|
|
9
|
+
class Client
|
|
10
|
+
# Job completion, failure, and error throwing operations.
|
|
11
|
+
module JobOperations
|
|
12
|
+
# Complete a job with optional output variables.
|
|
13
|
+
#
|
|
14
|
+
# @param job_key [Integer] The unique job identifier
|
|
15
|
+
# @param vars [Hash] Variables to return to the workflow engine
|
|
16
|
+
# @return [Object] Response from the gateway (truthy)
|
|
17
|
+
# @raise [Busybee::GRPC::Error] if completion fails
|
|
18
|
+
#
|
|
19
|
+
# @example Complete a job without variables
|
|
20
|
+
# client.complete_job(123456)
|
|
21
|
+
#
|
|
22
|
+
# @example Complete a job with variables
|
|
23
|
+
# client.complete_job(123456, vars: { result: "success", orderId: 999 })
|
|
24
|
+
#
|
|
25
|
+
def complete_job(job_key, vars: {})
|
|
26
|
+
request = Busybee::GRPC::CompleteJobRequest.new(
|
|
27
|
+
jobKey: job_key.to_i,
|
|
28
|
+
variables: Busybee::Serialization.to_json(vars)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
with_retry do
|
|
32
|
+
stub.complete_job(request)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Fail a job with an error message.
|
|
37
|
+
#
|
|
38
|
+
# @param job_key [Integer] The unique job identifier
|
|
39
|
+
# @param error_message [String] Error message describing the failure
|
|
40
|
+
# @param retries [Integer, nil] Override the number of remaining retries
|
|
41
|
+
# @param backoff [Integer, ActiveSupport::Duration, nil] Delay before retry in milliseconds
|
|
42
|
+
# @return [Object] Response from the gateway (truthy)
|
|
43
|
+
# @raise [Busybee::GRPC::Error] if failure operation fails
|
|
44
|
+
#
|
|
45
|
+
# @example Fail a job with default backoff
|
|
46
|
+
# client.fail_job(123456, "Payment gateway timeout")
|
|
47
|
+
#
|
|
48
|
+
# @example Fail with custom retry count
|
|
49
|
+
# client.fail_job(123456, "Transient error", retries: 3)
|
|
50
|
+
#
|
|
51
|
+
# @example Fail with custom backoff duration
|
|
52
|
+
# client.fail_job(123456, "Rate limited", backoff: 30.seconds)
|
|
53
|
+
#
|
|
54
|
+
def fail_job(job_key, error_message, retries: nil, backoff: nil)
|
|
55
|
+
backoff_ms = backoff || Busybee.default_fail_job_backoff
|
|
56
|
+
backoff_ms = backoff_ms.is_a?(ActiveSupport::Duration) ? backoff_ms.in_milliseconds.to_i : backoff_ms.to_i
|
|
57
|
+
|
|
58
|
+
request = Busybee::GRPC::FailJobRequest.new(
|
|
59
|
+
jobKey: job_key.to_i,
|
|
60
|
+
errorMessage: error_message.to_s,
|
|
61
|
+
retryBackOff: backoff_ms
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
request.retries = retries.to_i if retries
|
|
65
|
+
|
|
66
|
+
with_retry do
|
|
67
|
+
stub.fail_job(request)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Throw a BPMN error to be caught by an error boundary event.
|
|
72
|
+
#
|
|
73
|
+
# @param job_key [Integer] The unique job identifier
|
|
74
|
+
# @param error_code [String] BPMN error code (typically UPPERCASE_SNAKE_CASE)
|
|
75
|
+
# @param message [String] Optional error message for context
|
|
76
|
+
# @return [Object] Response from the gateway (truthy)
|
|
77
|
+
# @raise [Busybee::GRPC::Error] if throw operation fails
|
|
78
|
+
#
|
|
79
|
+
# @example Throw a BPMN error
|
|
80
|
+
# client.throw_bpmn_error(123456, "ORDER_NOT_FOUND", message: "Order 550e8400 not found")
|
|
81
|
+
#
|
|
82
|
+
# @example Throw a BPMN error without message
|
|
83
|
+
# client.throw_bpmn_error(123456, "PAYMENT_FAILED")
|
|
84
|
+
#
|
|
85
|
+
def throw_bpmn_error(job_key, error_code, message: "")
|
|
86
|
+
request = Busybee::GRPC::ThrowErrorRequest.new(
|
|
87
|
+
jobKey: job_key.to_i,
|
|
88
|
+
errorCode: error_code.to_s,
|
|
89
|
+
errorMessage: message.to_s
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
with_retry do
|
|
93
|
+
stub.throw_error(request)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Update the retry count for a job.
|
|
98
|
+
#
|
|
99
|
+
# @param job_key [Integer] The unique job identifier
|
|
100
|
+
# @param retries [Integer] The new number of retries
|
|
101
|
+
# @return [Object] Response from the gateway (truthy)
|
|
102
|
+
# @raise [Busybee::GRPC::Error] if update operation fails
|
|
103
|
+
#
|
|
104
|
+
# @example Update job retries
|
|
105
|
+
# client.update_job_retries(123456, 5)
|
|
106
|
+
#
|
|
107
|
+
def update_job_retries(job_key, retries)
|
|
108
|
+
request = Busybee::GRPC::UpdateJobRetriesRequest.new(
|
|
109
|
+
jobKey: job_key.to_i,
|
|
110
|
+
retries: retries.to_i
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
with_retry do
|
|
114
|
+
stub.update_job_retries(request)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Update the timeout for a job.
|
|
119
|
+
#
|
|
120
|
+
# @param job_key [Integer] The unique job identifier
|
|
121
|
+
# @param timeout [Integer, ActiveSupport::Duration] New timeout in milliseconds
|
|
122
|
+
# @return [Object] Response from the gateway (truthy)
|
|
123
|
+
# @raise [Busybee::GRPC::Error] if update operation fails
|
|
124
|
+
#
|
|
125
|
+
# @example Update job timeout with milliseconds
|
|
126
|
+
# client.update_job_timeout(123456, 30_000)
|
|
127
|
+
#
|
|
128
|
+
# @example Update job timeout with Duration
|
|
129
|
+
# client.update_job_timeout(123456, 30.seconds)
|
|
130
|
+
#
|
|
131
|
+
def update_job_timeout(job_key, timeout)
|
|
132
|
+
timeout_ms = timeout.is_a?(ActiveSupport::Duration) ? timeout.in_milliseconds.to_i : timeout.to_i
|
|
133
|
+
|
|
134
|
+
request = Busybee::GRPC::UpdateJobTimeoutRequest.new(
|
|
135
|
+
jobKey: job_key.to_i,
|
|
136
|
+
timeout: timeout_ms
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
with_retry do
|
|
140
|
+
stub.update_job_timeout(request)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Activate and process jobs with a block (bounded, non-streaming).
|
|
145
|
+
#
|
|
146
|
+
# @param job_type [String] The job type to activate
|
|
147
|
+
# @param max_jobs [Integer] Maximum number of jobs to activate
|
|
148
|
+
# @param job_timeout [Integer, ActiveSupport::Duration, nil] Job lock timeout in milliseconds
|
|
149
|
+
# (defaults to Busybee.default_job_lock_timeout)
|
|
150
|
+
# @param request_timeout [Integer, ActiveSupport::Duration, nil] Request timeout in milliseconds
|
|
151
|
+
# (defaults to Busybee.default_job_request_timeout)
|
|
152
|
+
# @yield [job] Yields each activated job to the block
|
|
153
|
+
# @yieldparam job [Busybee::Job] The activated job
|
|
154
|
+
# @return [Integer] Count of jobs processed
|
|
155
|
+
# @raise [ArgumentError] if no block given
|
|
156
|
+
# @raise [Busybee::GRPC::Error] if activation fails
|
|
157
|
+
#
|
|
158
|
+
# @example Process jobs
|
|
159
|
+
# client.with_each_job("send-email") do |job|
|
|
160
|
+
# send_email(job.variables.email, job.variables.subject)
|
|
161
|
+
# job.complete!
|
|
162
|
+
# end
|
|
163
|
+
#
|
|
164
|
+
def with_each_job(job_type, max_jobs: Busybee::Defaults::DEFAULT_MAX_JOBS, # rubocop:disable Metrics/AbcSize
|
|
165
|
+
job_timeout: nil,
|
|
166
|
+
request_timeout: nil)
|
|
167
|
+
raise ArgumentError, "block required" unless block_given?
|
|
168
|
+
|
|
169
|
+
job_timeout ||= Busybee.default_job_lock_timeout
|
|
170
|
+
request_timeout ||= Busybee.default_job_request_timeout
|
|
171
|
+
|
|
172
|
+
request = Busybee::GRPC::ActivateJobsRequest.new(
|
|
173
|
+
type: job_type.to_s,
|
|
174
|
+
worker: Busybee.worker_name,
|
|
175
|
+
maxJobsToActivate: max_jobs.to_i,
|
|
176
|
+
timeout: milliseconds_from(job_timeout),
|
|
177
|
+
requestTimeout: milliseconds_from(request_timeout)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
count = 0
|
|
181
|
+
responses = with_retry { stub.activate_jobs(request) }
|
|
182
|
+
|
|
183
|
+
responses.each do |response|
|
|
184
|
+
response.jobs.each do |raw_job|
|
|
185
|
+
job = Busybee::Job.new(raw_job, client: self)
|
|
186
|
+
yield job
|
|
187
|
+
count += 1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
count
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Open a long-lived stream for job activation.
|
|
195
|
+
#
|
|
196
|
+
# Unlike {#with_each_job}, this method returns a {Busybee::JobStream} that
|
|
197
|
+
# continuously receives jobs as they become available. The stream remains
|
|
198
|
+
# open until explicitly closed via {Busybee::JobStream#close}.
|
|
199
|
+
#
|
|
200
|
+
# @note The stream blocks while iterating. Call {Busybee::JobStream#close}
|
|
201
|
+
# from another thread or use a signal handler to terminate the stream.
|
|
202
|
+
#
|
|
203
|
+
# @param job_type [String] The job type to activate
|
|
204
|
+
# @param job_timeout [Integer, ActiveSupport::Duration, nil] Job lock timeout in milliseconds
|
|
205
|
+
# (defaults to Busybee.default_job_lock_timeout)
|
|
206
|
+
# @return [Busybee::JobStream] Stream of activated jobs
|
|
207
|
+
# @raise [Busybee::GRPC::Error] if stream creation fails
|
|
208
|
+
#
|
|
209
|
+
# @example Process jobs with graceful shutdown
|
|
210
|
+
# stream = client.open_job_stream("send-email", job_timeout: 60.seconds)
|
|
211
|
+
# trap("INT") { stream.close }
|
|
212
|
+
#
|
|
213
|
+
# stream.each do |job|
|
|
214
|
+
# send_email(job.variables)
|
|
215
|
+
# job.complete!
|
|
216
|
+
# end
|
|
217
|
+
#
|
|
218
|
+
def open_job_stream(job_type, job_timeout: nil)
|
|
219
|
+
job_timeout ||= Busybee.default_job_lock_timeout
|
|
220
|
+
|
|
221
|
+
request = Busybee::GRPC::StreamActivatedJobsRequest.new(
|
|
222
|
+
type: job_type.to_s,
|
|
223
|
+
worker: Busybee.worker_name,
|
|
224
|
+
timeout: milliseconds_from(job_timeout)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
with_retry do
|
|
228
|
+
Busybee::JobStream.new(
|
|
229
|
+
stub.stream_activated_jobs(request, return_op: true),
|
|
230
|
+
client: self
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/duration"
|
|
5
|
+
require "busybee/grpc"
|
|
6
|
+
require "busybee/serialization"
|
|
7
|
+
|
|
8
|
+
module Busybee
|
|
9
|
+
class Client
|
|
10
|
+
# Message and signal operations for process communication.
|
|
11
|
+
module MessageOperations
|
|
12
|
+
# Publish a message to trigger message catch events in process instances.
|
|
13
|
+
#
|
|
14
|
+
# @param name [String] The message name
|
|
15
|
+
# @param correlation_key [String] Correlation key to match against process instances
|
|
16
|
+
# @param ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in milliseconds or Duration object
|
|
17
|
+
# (defaults to Busybee.default_message_ttl)
|
|
18
|
+
# @param vars [Hash] Variables to pass with the message
|
|
19
|
+
# @param tenant_id [String, nil] Tenant ID for multi-tenancy
|
|
20
|
+
# @return [Integer] The message key
|
|
21
|
+
# @raise [ArgumentError] if vars is not a Hash
|
|
22
|
+
# @raise [Busybee::GRPC::Error] if publishing fails
|
|
23
|
+
#
|
|
24
|
+
# @example Publish a message with default TTL
|
|
25
|
+
# key = client.publish_message("order-ready", correlation_key: "order-123")
|
|
26
|
+
# # => 12345
|
|
27
|
+
#
|
|
28
|
+
# @example With explicit TTL and variables
|
|
29
|
+
# client.publish_message("order-ready",
|
|
30
|
+
# correlation_key: "order-123",
|
|
31
|
+
# ttl: 30.seconds,
|
|
32
|
+
# vars: { orderId: 123 })
|
|
33
|
+
#
|
|
34
|
+
def publish_message(name, correlation_key:, vars: {}, ttl: nil, tenant_id: nil)
|
|
35
|
+
raise ArgumentError, "vars must be a Hash" unless vars.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
ttl ||= Busybee.default_message_ttl
|
|
38
|
+
ttl_ms = ttl.is_a?(ActiveSupport::Duration) ? ttl.in_milliseconds.to_i : ttl.to_i
|
|
39
|
+
|
|
40
|
+
request = Busybee::GRPC::PublishMessageRequest.new(
|
|
41
|
+
name: name.to_s,
|
|
42
|
+
correlationKey: correlation_key.to_s,
|
|
43
|
+
variables: Busybee::Serialization.to_json(vars),
|
|
44
|
+
timeToLive: ttl_ms,
|
|
45
|
+
tenantId: tenant_id&.to_s
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
with_retry do
|
|
49
|
+
stub.publish_message(request).key
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Broadcast a signal to all process instances with matching signal catch events.
|
|
54
|
+
#
|
|
55
|
+
# @param signal_name [String] The signal name
|
|
56
|
+
# @param vars [Hash] Variables to pass with the signal
|
|
57
|
+
# @param tenant_id [String, nil] Tenant ID for multi-tenancy
|
|
58
|
+
# @return [Integer] The signal key
|
|
59
|
+
# @raise [ArgumentError] if vars is not a Hash
|
|
60
|
+
# @raise [Busybee::GRPC::Error] if broadcasting fails
|
|
61
|
+
#
|
|
62
|
+
# @example Broadcast a signal
|
|
63
|
+
# key = client.broadcast_signal("cancel-all-orders")
|
|
64
|
+
# # => 54321
|
|
65
|
+
#
|
|
66
|
+
# @example With variables
|
|
67
|
+
# client.broadcast_signal("cancel-all-orders", vars: { reason: "system-maintenance" })
|
|
68
|
+
#
|
|
69
|
+
def broadcast_signal(signal_name, vars: {}, tenant_id: nil)
|
|
70
|
+
raise ArgumentError, "vars must be a Hash" unless vars.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
request = Busybee::GRPC::BroadcastSignalRequest.new(
|
|
73
|
+
signalName: signal_name.to_s,
|
|
74
|
+
variables: Busybee::Serialization.to_json(vars),
|
|
75
|
+
tenantId: tenant_id&.to_s
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
with_retry do
|
|
79
|
+
stub.broadcast_signal(request).key
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "busybee/grpc"
|
|
4
|
+
require "busybee/serialization"
|
|
5
|
+
|
|
6
|
+
module Busybee
|
|
7
|
+
class Client
|
|
8
|
+
# Process deployment, instance creation, and cancellation operations.
|
|
9
|
+
module ProcessOperations
|
|
10
|
+
# Deploy one or more BPMN files.
|
|
11
|
+
#
|
|
12
|
+
# @param paths [Array<String>] Paths to BPMN files
|
|
13
|
+
# @param tenant_id [String, nil] Tenant ID for multi-tenancy
|
|
14
|
+
# @return [Hash{String => Integer}] Map of bpmn_process_id => process_definition_key
|
|
15
|
+
# @raise [Busybee::GRPC::Error] if deployment fails
|
|
16
|
+
#
|
|
17
|
+
# @example Deploy a single file
|
|
18
|
+
# client.deploy_process("workflows/order.bpmn")
|
|
19
|
+
# # => { "order-fulfillment" => 2251799813685249 }
|
|
20
|
+
#
|
|
21
|
+
# @example Deploy multiple files
|
|
22
|
+
# client.deploy_process("order.bpmn", "payment.bpmn")
|
|
23
|
+
# # => { "order-fulfillment" => 123, "payment-process" => 456 }
|
|
24
|
+
#
|
|
25
|
+
def deploy_process(*paths, tenant_id: nil) # rubocop:disable Metrics/AbcSize
|
|
26
|
+
resources = paths.map do |path|
|
|
27
|
+
Busybee::GRPC::Resource.new(
|
|
28
|
+
name: File.basename(path),
|
|
29
|
+
content: File.read(path)
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
request = Busybee::GRPC::DeployResourceRequest.new(
|
|
34
|
+
resources: resources,
|
|
35
|
+
tenantId: tenant_id&.to_s
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
with_retry do
|
|
39
|
+
response = stub.deploy_resource(request)
|
|
40
|
+
response.deployments.each_with_object({}) do |deployment, result|
|
|
41
|
+
result[deployment.process.bpmnProcessId] = deployment.process.processDefinitionKey
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Start a process instance.
|
|
47
|
+
#
|
|
48
|
+
# @param bpmn_process_id [String] The BPMN process ID to start
|
|
49
|
+
# @param vars [Hash] Variables to pass to the process instance
|
|
50
|
+
# @param version [Integer, Symbol, nil] Process version (:latest, nil, or specific version number)
|
|
51
|
+
# @param tenant_id [String, nil] Tenant ID for multi-tenancy
|
|
52
|
+
# @return [Integer] The process_instance_key
|
|
53
|
+
# @raise [ArgumentError] if vars is not a Hash
|
|
54
|
+
# @raise [Busybee::GRPC::Error] if starting the process fails
|
|
55
|
+
#
|
|
56
|
+
# @example Start a process instance with variables
|
|
57
|
+
# key = client.start_instance("order-fulfillment", vars: { orderId: 123 })
|
|
58
|
+
# # => 67890
|
|
59
|
+
#
|
|
60
|
+
def start_instance(bpmn_process_id, vars: {}, version: :latest, tenant_id: nil)
|
|
61
|
+
raise ArgumentError, "vars must be a Hash" unless vars.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
request = Busybee::GRPC::CreateProcessInstanceRequest.new(
|
|
64
|
+
bpmnProcessId: bpmn_process_id.to_s,
|
|
65
|
+
variables: Busybee::Serialization.to_json(vars),
|
|
66
|
+
version: version == :latest || version.nil? ? -1 : version.to_i,
|
|
67
|
+
tenantId: tenant_id&.to_s
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
with_retry do
|
|
71
|
+
stub.create_process_instance(request).processInstanceKey
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
alias start_process_instance start_instance
|
|
75
|
+
|
|
76
|
+
# Cancel a running process instance.
|
|
77
|
+
#
|
|
78
|
+
# @param process_instance_key [Integer, String] The process instance key to cancel
|
|
79
|
+
# @param ignore_missing [Boolean] If true, return false instead of raising when instance not found
|
|
80
|
+
# @return [Boolean] true if cancelled, false if not found and ignore_missing is true
|
|
81
|
+
# @raise [Busybee::GRPC::Error] if cancellation fails (unless ignore_missing for NotFound)
|
|
82
|
+
#
|
|
83
|
+
# @example Cancel an instance
|
|
84
|
+
# client.cancel_instance(67890)
|
|
85
|
+
# # => true
|
|
86
|
+
#
|
|
87
|
+
# @example Safely cancel without error if missing
|
|
88
|
+
# client.cancel_instance(99999, ignore_missing: true)
|
|
89
|
+
# # => false
|
|
90
|
+
#
|
|
91
|
+
def cancel_instance(process_instance_key, ignore_missing: false)
|
|
92
|
+
request = Busybee::GRPC::CancelProcessInstanceRequest.new(
|
|
93
|
+
processInstanceKey: process_instance_key.to_i
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
with_retry do
|
|
97
|
+
stub.cancel_process_instance(request)
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
rescue Busybee::GRPC::Error => e
|
|
101
|
+
raise unless ignore_missing && e.grpc_status == :not_found
|
|
102
|
+
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
alias cancel_process_instance cancel_instance
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|