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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -7
  3. data/README.md +70 -42
  4. data/docs/client/quick_start.md +279 -0
  5. data/docs/client.md +825 -0
  6. data/docs/configuration.md +550 -0
  7. data/docs/grpc.md +50 -25
  8. data/docs/testing.md +118 -28
  9. data/docs/workers.md +982 -0
  10. data/exe/busybee +6 -0
  11. data/lib/busybee/cli.rb +173 -0
  12. data/lib/busybee/client/error_handling.rb +37 -0
  13. data/lib/busybee/client/job_operations.rb +236 -0
  14. data/lib/busybee/client/message_operations.rb +84 -0
  15. data/lib/busybee/client/process_operations.rb +108 -0
  16. data/lib/busybee/client/variable_operations.rb +64 -0
  17. data/lib/busybee/client.rb +87 -0
  18. data/lib/busybee/configure.rb +290 -0
  19. data/lib/busybee/credentials/camunda_cloud.rb +58 -0
  20. data/lib/busybee/credentials/insecure.rb +24 -0
  21. data/lib/busybee/credentials/oauth.rb +157 -0
  22. data/lib/busybee/credentials/tls.rb +43 -0
  23. data/lib/busybee/credentials.rb +200 -0
  24. data/lib/busybee/defaults.rb +20 -0
  25. data/lib/busybee/error.rb +50 -0
  26. data/lib/busybee/grpc/error.rb +60 -0
  27. data/lib/busybee/grpc.rb +2 -2
  28. data/lib/busybee/job.rb +219 -0
  29. data/lib/busybee/job_stream.rb +85 -0
  30. data/lib/busybee/logging.rb +61 -0
  31. data/lib/busybee/railtie.rb +113 -0
  32. data/lib/busybee/runner/hybrid.rb +64 -0
  33. data/lib/busybee/runner/multi.rb +101 -0
  34. data/lib/busybee/runner/polling.rb +54 -0
  35. data/lib/busybee/runner/streaming.rb +159 -0
  36. data/lib/busybee/runner.rb +97 -0
  37. data/lib/busybee/runtime_config.rb +184 -0
  38. data/lib/busybee/serialization.rb +100 -0
  39. data/lib/busybee/testing/activated_job.rb +33 -8
  40. data/lib/busybee/testing/helpers/execution.rb +139 -0
  41. data/lib/busybee/testing/helpers/support.rb +78 -0
  42. data/lib/busybee/testing/helpers.rb +56 -66
  43. data/lib/busybee/testing/matchers/complete_job.rb +55 -0
  44. data/lib/busybee/testing/matchers/fail_job.rb +75 -0
  45. data/lib/busybee/testing/matchers/have_activated.rb +1 -1
  46. data/lib/busybee/testing/matchers/have_available_jobs.rb +44 -0
  47. data/lib/busybee/testing/matchers/throw_bpmn_error_on.rb +72 -0
  48. data/lib/busybee/testing.rb +5 -33
  49. data/lib/busybee/version.rb +1 -1
  50. data/lib/busybee/worker/configuration.rb +287 -0
  51. data/lib/busybee/worker/dsl.rb +187 -0
  52. data/lib/busybee/worker/shutdown.rb +27 -0
  53. data/lib/busybee/worker.rb +130 -0
  54. data/lib/busybee.rb +134 -2
  55. metadata +80 -3
data/docs/client.md ADDED
@@ -0,0 +1,825 @@
1
+ # Busybee::Client
2
+
3
+ The `Busybee::Client` class is the main entry point for interacting with the Zeebe workflow engine from your Ruby app. It provides methods for deploying workflows, starting and cancelling process instances, publishing messages, and activating jobs for processing. The Client wraps the low-level GRPC layer with keyword arguments, sensible defaults, and proper exception handling.
4
+
5
+ If you haven't used Zeebe or Busybee before, check out the [quick start guide](client/quick_start.md), which will get you from zero to a deployed process and running instance in about 10 minutes. This doc is a more complete explanation and reference.
6
+
7
+ | Section | Description |
8
+ |---------|-------------|
9
+ | [Providing Credentials](#providing-credentials) | How to connect and authenticate to a Zeebe cluster |
10
+ | [Error Handling](#error-handling) | Exception hierarchy and retry configuration |
11
+ | [Working with Jobs](#working-with-jobs) | Conceptual guide to job processing (polling vs streaming) |
12
+ | [API Reference](#api-reference) | Complete method documentation |
13
+ | [Configuration Reference](configuration.md) | Full configuration options (logging, retry, Rails integration) |
14
+
15
+ ## Providing Credentials
16
+
17
+ An instance of Busybee::Client relies on an instance of Busybee::Credentials to tell it where to find the Zeebe cluster and how to authenticate to it.
18
+
19
+ There are four types of credentials supported by Busybee for different environments:
20
+
21
+ | Credential Class | Type Symbol | Use Cases | SSL/TLS | Authentication |
22
+ |------|----------|-----|----------------|----|
23
+ | Busybee::Credentials::Insecure | `:insecure` | Local development, Docker, CI | No | None |
24
+ | Busybee::Credentials::TLS | `:tls` | Self-hosted with SSL/TLS | Yes | Server cert only |
25
+ | Busybee::Credentials::OAuth | `:oauth` | Self-hosted with OAuth | Yes | OAuth2 client credentials |
26
+ | Busybee::Credentials::CamundaCloud | `:camunda_cloud` | Camunda Cloud SaaS | Yes | OAuth2 (auto-configured) |
27
+
28
+ The most long-form way to create a Busybee::Client is to create an instance of one of these classes first, and pass that as the argument to Client.new:
29
+
30
+ ```ruby
31
+ credentials = Busybee::Credentials::Insecure.new(cluster_address: "zeebe:26500")
32
+ client = Busybee::Client.new(credentials)
33
+ ```
34
+
35
+ You can also configure the gem with a single set of credentials, and call Client.new with no arguments in order to use the configured credentials implicitly:
36
+
37
+ ```ruby
38
+ # in config/application.rb or config/initializers/busybee.rb:
39
+ Busybee.configure do |config|
40
+ config.credentials = Busybee::Credentials::TLS.new(
41
+ cluster_address: "zeebe:26500",
42
+ certificate_file: "/path/to/ca.crt" # optional, uses system default otherwise
43
+ )
44
+ end
45
+
46
+ # then, anywhere in application code:
47
+ client = Busybee::Client.new
48
+ ```
49
+
50
+ Or, just configure the cluster_address and credential_type, and let Busybee read your secret values out of your ENV vars:
51
+
52
+ ```ruby
53
+ # in config/application.rb or config/initializers/busybee.rb:
54
+ Busybee.configure do |config|
55
+ config.cluster_address = "zeebe:26500"
56
+ config.credential_type = :oauth # token URL, audience, scope, client ID, and client secret will be read from env vars
57
+ end
58
+
59
+ # then, anywhere in application code:
60
+ client = Busybee::Client.new
61
+ ```
62
+
63
+ For testing, it can be helpful to create multiple clients with different credentials. You can always pass a complete set of credentials directly to Client.new and let it figure out what type of credentials to build automatically, if you wish:
64
+
65
+ ```ruby
66
+ insecure_client = Busybee::Client.new(
67
+ cluster_address: "insecure_cluster:26500",
68
+ insecure: true
69
+ )
70
+
71
+ tls_client = Busybee::Client.new(
72
+ # if cluster_address is not given to any of these, it will use the configured cluster_address (see below):
73
+ certificate_file: "/path/to/ca.crt"
74
+ )
75
+
76
+ oauth_client = Busybee::Client.new(
77
+ cluster_address: "oauth_cluster:26500",
78
+ token_url: "https://auth.example.com/oauth/token",
79
+ client_id: "my-client-id",
80
+ client_secret: "my-client-secret",
81
+ audience: "my-token-audience"
82
+ )
83
+
84
+ camunda_cloud_client = Busybee::Client.new(
85
+ # for Camunda Cloud, the cluster address and OAuth configuration are derived automatically:
86
+ client_id: "my-client-id",
87
+ client_secret: "my-client-secret",
88
+ cluster_id: "my-cluster-id", # usually a UUID
89
+ region: "my-cluster-region" # e.g., "bru-2"
90
+ )
91
+ ```
92
+
93
+ ### Cluster Address Resolution
94
+
95
+ When `cluster_address` is not explicitly provided, Busybee uses this precedence:
96
+
97
+ 1. Explicit `cluster_address:` parameter (highest priority)
98
+ 2. `Busybee.cluster_address` configuration value
99
+ 3. `CLUSTER_ADDRESS` environment variable
100
+ 4. Default: `"localhost:26500"` (lowest priority)
101
+
102
+ This allows you to set a default cluster address once and override it selectively when needed.
103
+
104
+ ### Environment Variables
105
+
106
+ For convenience, many of the credential parameters may be read implicitly from the following env vars:
107
+
108
+ | Environment Variable | Purpose | Used By |
109
+ |---------------------|---------|---------|
110
+ | `CLUSTER_ADDRESS` | Zeebe cluster address (host:port) | All credential types |
111
+ | `BUSYBEE_CREDENTIAL_TYPE` | Credential type (insecure, tls, oauth, camunda_cloud) | Auto-detection |
112
+ | `ZEEBE_TOKEN_URL` | OAuth token endpoint | OAuth |
113
+ | `ZEEBE_AUDIENCE` | OAuth audience | OAuth |
114
+ | `ZEEBE_SCOPE` | OAuth scope (optional) | OAuth |
115
+ | `ZEEBE_CERTIFICATE_FILE` | Path to CA certificate | TLS, OAuth |
116
+ | `CAMUNDA_CLIENT_ID` | OAuth client ID | OAuth, Camunda Cloud |
117
+ | `CAMUNDA_CLIENT_SECRET` | OAuth client secret | OAuth, Camunda Cloud |
118
+ | `CAMUNDA_CLUSTER_ID` | Cluster UUID | Camunda Cloud |
119
+ | `CAMUNDA_CLUSTER_REGION` | Cluster region (e.g., "bru-2") | Camunda Cloud |
120
+
121
+ ## Error Handling
122
+
123
+ Busybee wraps low-level GRPC errors in Ruby exceptions that are easier to work with. The goal is to let you rescue errors by type without needing to understand GRPC status codes, while still giving you access to the underlying details when you need them.
124
+
125
+ ### Error Hierarchy
126
+
127
+ All Busybee errors inherit from `Busybee::Error`, so you can rescue broadly or narrowly:
128
+
129
+ | Error Class | When Raised |
130
+ |-------------|-------------|
131
+ | `Busybee::Error` | Base class for all Busybee errors. Never raised directly; exists for `rescue Busybee::Error`. |
132
+ | `Busybee::GRPC::Error` | Any GRPC operation failure (network issues, invalid requests, server errors). See below. |
133
+ | `Busybee::InvalidOAuthResponse` | OAuth token endpoint returned invalid JSON. |
134
+ | `Busybee::InvalidJobJson` | Job variables or headers contain malformed JSON. |
135
+ | `Busybee::JobAlreadyHandled` | Attempted to complete, fail, or throw error on a job that has already been handled. |
136
+ | `Busybee::OAuthTokenRefreshFailed` | HTTP error received from OAuth refresh token endpoint. |
137
+ | `Busybee::StreamAlreadyClosed` | Attempted to iterate a job stream that was already closed. |
138
+
139
+ ### GRPC::Error Wrapper Class
140
+
141
+ `Busybee::GRPC::Error` is the error you're likely to encounter most often. It wraps the underlying `::GRPC::BadStatus` exception and provides convenient accessors for GRPC-specific information:
142
+
143
+ ```ruby
144
+ begin
145
+ client.start_instance("missing-process", vars: { orderId: 123 })
146
+ rescue Busybee::GRPC::Error => e
147
+ e.message # => "GRPC call failed (NOT_FOUND: no process found with ID 'missing-process')"
148
+ e.grpc_status # => :not_found
149
+ e.grpc_code # => 5 (the numeric GRPC status code)
150
+ e.grpc_details # => "no process found with ID 'missing-process'"
151
+ e.cause # => #<GRPC::NotFound: ...> (the original GRPC exception)
152
+ end
153
+ ```
154
+
155
+ The original GRPC exception is preserved in `#cause` through Ruby's automatic exception chaining, so you can always dig into the raw error if needed.
156
+
157
+ ### Automatic Retry
158
+
159
+ Busybee can automatically retry GRPC calls that fail due to transient errors. This is disabled by default to minimize surprise, because retries can mask problems during development.
160
+
161
+ ```ruby
162
+ Busybee.configure do |config|
163
+ config.grpc_retry_enabled = true # Enable retry (default: false)
164
+ config.grpc_retry_delay_ms = 500 # Delay between attempts (default: 500ms)
165
+ config.grpc_retry_errors = [ # Which errors trigger retry (default below)
166
+ GRPC::Unavailable,
167
+ GRPC::DeadlineExceeded,
168
+ GRPC::ResourceExhausted
169
+ ]
170
+ end
171
+ ```
172
+
173
+ When retry is enabled, Busybee makes up to 2 attempts before raising. A warning is logged on retry:
174
+
175
+ ```
176
+ [busybee] GRPC call failed, retrying in 500ms (error_class: GRPC::Unavailable)
177
+ ```
178
+
179
+ If both attempts fail, the error message indicates this: `"GRPC call failed after retry"`.
180
+
181
+ ## Working with Jobs
182
+
183
+ Jobs represent units of work that your application performs as part of a workflow. When a BPMN process instance reaches a service task, Zeebe creates a job that workers can claim and process. This section covers the conceptual model; see [Job Operations](#job-operations) in the API Reference for method details.
184
+
185
+ ### Polling vs Streaming
186
+
187
+ Busybee provides two ways to receive jobs:
188
+
189
+ | Approach | Method | Model | Best For |
190
+ |----------|--------|-------|----------|
191
+ | Long-Polling | `with_each_job` | Request/Response | Batch processing, cron jobs, serverless functions |
192
+ | Streaming | `open_job_stream` | Push | Long-running workers, real-time processing |
193
+
194
+ **`with_each_job` (long-polling)** makes a request and waits. If jobs are available, they're returned immediately. If not, the request waits until jobs become available or the request timeout expires, then returns whatever it has. This is like checking your inbox: you see everything that's there at the moment you look, no matter when it arrived.
195
+
196
+ **`open_job_stream` (streaming)** opens a persistent connection and receives jobs as they become available. Jobs that existed before you opened the stream are NOT delivered. This is like subscribing to a magazine: you get new issues as they're published, but you don't get back issues.
197
+
198
+ This means if you start a streaming worker after jobs have already been created, those jobs won't be delivered to your stream. For workers that need to process both existing and new jobs, either:
199
+ - Use `with_each_job` in a polling loop, or
200
+ - Call `with_each_job` at startup to drain existing jobs, then switch to streaming
201
+
202
+ > The [Worker pattern framework](workers.md) makes it easy to select between these behaviors automatically via [worker modes](workers.md#worker-modes).
203
+
204
+ ### The Job Object
205
+
206
+ When you receive jobs through `with_each_job` or `open_job_stream`, each job is wrapped in a `Busybee::Job` object. This is the primary way you'll interact with jobs.
207
+
208
+ **Attributes:**
209
+
210
+ | Method | Returns | Description |
211
+ |--------|---------|-------------|
212
+ | `key` | `Integer` | Unique job identifier |
213
+ | `type` | `String` | Job type (from BPMN task definition) |
214
+ | `process_instance_key` | `Integer` | The process instance this job belongs to |
215
+ | `bpmn_process_id` | `String` | The BPMN process ID |
216
+ | `retries` | `Integer` | Remaining retry attempts |
217
+ | `deadline` | `Time` | When the job lock expires (frozen Time object) |
218
+ | `variables` | `Hash` | Job input variables (with indifferent access) |
219
+ | `headers` | `Hash` | Custom headers from the BPMN task definition |
220
+ | `status` | `Symbol` | Current status: `:ready`, `:complete`, `:failed`, or `:error` |
221
+
222
+ **Variables: Reading and Writing**
223
+
224
+ When you **read** variables and headers from a job, they're returned as `ActiveSupport::HashWithIndifferentAccess`. You can access keys with strings or symbols, and they support method-style access with automatic camelCase conversion:
225
+
226
+ ```ruby
227
+ client.with_each_job("process-order") do |job|
228
+ # All of these work:
229
+ job.variables[:orderId] # Symbol access
230
+ job.variables["orderId"] # String access
231
+ job.variables.orderId # Method access (camelCase)
232
+ job.variables.order_id # Method access (snake_case → camelCase)
233
+
234
+ # Nested hashes also support method access
235
+ job.variables.customer.email
236
+ end
237
+ ```
238
+
239
+ Variables and headers are frozen to prevent accidental mutation.
240
+
241
+ When you **write** variables (via `complete!`, `start_instance`, `publish_message`, etc.), Busybee calls `as_json` on your data before JSON-encoding it. This means objects with custom serialization—like ActiveRecord models—work sensibly:
242
+
243
+ ```ruby
244
+ user = User.find(user_id)
245
+ order = Order.find(order_id)
246
+
247
+ # ActiveRecord models serialize via as_json, not inspect
248
+ job.complete!(user: user, order: order)
249
+ # => {"user": {"id": 1, "name": "Alice", ...}, "order": {"id": 42, ...}}
250
+
251
+ # Plain hashes, arrays, and primitives work as expected
252
+ job.complete!(items: ["a", "b"], count: 3, metadata: { source: "api" })
253
+ ```
254
+
255
+ If you need custom serialization for your own classes, implement `as_json`.
256
+
257
+ **Actions:**
258
+
259
+ The Job object provides convenience methods that are the preferred way to complete, fail, or error jobs:
260
+
261
+ **`complete!(vars = {})`** — Complete the job with optional output variables.
262
+
263
+ ```ruby
264
+ job.complete!
265
+ job.complete!(result: "success", processedAt: Time.now.iso8601)
266
+ ```
267
+
268
+ **`fail!(error_message_or_exception, retries: nil, backoff: nil)`** — Fail the job. You can pass a string or an exception:
269
+
270
+ ```ruby
271
+ job.fail!("Payment gateway timeout")
272
+ job.fail!("Rate limited", retries: 3, backoff: 30.seconds)
273
+
274
+ # Pass an exception directly—Busybee formats it nicely
275
+ begin
276
+ risky_operation
277
+ rescue => e
278
+ job.fail!(e) # Message: "[ExceptionClass] message (caused by: ...)"
279
+ end
280
+ ```
281
+
282
+ **`throw_bpmn_error!(code_or_exception, message = "")`** — Throw a BPMN error. You can pass a string, symbol, or exception:
283
+
284
+ ```ruby
285
+ job.throw_bpmn_error!("ORDER_NOT_FOUND", "Order #{order_id} not found")
286
+ job.throw_bpmn_error!(:order_not_found) # Symbol converted to "ORDER_NOT_FOUND"
287
+
288
+ # Pass an exception—class name becomes the error code
289
+ begin
290
+ order = Order.find!(order_id)
291
+ rescue OrderNotFoundError => e
292
+ job.throw_bpmn_error!(e) # Code: "ORDER_NOT_FOUND_ERROR"
293
+ end
294
+ ```
295
+
296
+ **Status Tracking:**
297
+
298
+ The Job tracks its status to prevent double-handling bugs:
299
+
300
+ ```ruby
301
+ job.ready? # => true (job can be completed/failed)
302
+ job.complete? # => true after calling complete!
303
+ job.failed? # => true after calling fail!
304
+ job.error? # => true after calling throw_bpmn_error!
305
+
306
+ # Attempting to handle a job twice raises an error
307
+ job.complete!
308
+ job.fail!("oops") # => raises Busybee::JobAlreadyHandled
309
+ ```
310
+
311
+ ### JobStream
312
+
313
+ `Busybee::JobStream` wraps a gRPC server stream and provides a Ruby-idiomatic interface. It includes `Enumerable`, so you can use `each`, `map`, `select`, and other collection methods.
314
+
315
+ | Method | Description |
316
+ |--------|-------------|
317
+ | `each { \|job\| ... }` | Iterate over jobs (blocks until stream closes) |
318
+ | `close` | Close the stream (idempotent) |
319
+ | `closed?` | Check if the stream has been closed |
320
+
321
+ **Gotchas:**
322
+
323
+ 1. **Blocking behavior:** Calling `stream.each` blocks the calling thread indefinitely. The iteration only ends when another thread calls `stream.close`, or when the server closes the connection. Plan for this—use signal handlers or a separate management thread.
324
+
325
+ 2. **Single-pass:** Streams are single-pass. Once consumed via `each` or other Enumerable methods, the stream is exhausted. If you need to process the same jobs multiple times, collect them into an array first.
326
+
327
+ 3. **No back issues:** As noted in [Polling vs Streaming](#polling-vs-streaming), streams only receive jobs created *after* the stream opens.
328
+
329
+ ```ruby
330
+ stream = client.open_job_stream("send-email")
331
+
332
+ # Stream blocks on each—close from another thread or signal handler
333
+ trap("INT") { stream.close }
334
+
335
+ stream.each do |job|
336
+ puts "Processing job #{job.key}"
337
+ job.complete!
338
+ end
339
+ # This line only runs after the stream is closed
340
+ puts "Stream closed, shutting down"
341
+ ```
342
+
343
+ ---
344
+
345
+ ## API Reference
346
+
347
+ ### Overview
348
+
349
+ | Category | Method | Description |
350
+ |----------|--------|-------------|
351
+ | [Process](#process-operations) | [`deploy_process`](#deploy_process) | Deploy BPMN files |
352
+ | | [`start_instance`](#start_instance) | Start a process instance |
353
+ | | [`cancel_instance`](#cancel_instance) | Cancel a running instance |
354
+ | [Job](#job-operations) | [`with_each_job`](#with_each_job) | Poll for jobs (bounded) |
355
+ | | [`open_job_stream`](#open_job_stream) | Stream jobs (push-based) |
356
+ | | [`complete_job`](#complete_job) | Complete a job |
357
+ | | [`fail_job`](#fail_job) | Fail a job with retry |
358
+ | | [`throw_bpmn_error`](#throw_bpmn_error) | Throw a BPMN error |
359
+ | | [`update_job_retries`](#update_job_retries) | Update job retry count |
360
+ | | [`update_job_timeout`](#update_job_timeout) | Extend job deadline |
361
+ | | [`resolve_incident`](#resolve_incident) | Resolve a failed-job incident |
362
+ | [Message](#message-operations) | [`publish_message`](#publish_message) | Publish a correlated message |
363
+ | | [`broadcast_signal`](#broadcast_signal) | Broadcast a signal to all listeners |
364
+ | [Variable](#variable-operations) | [`set_variables`](#set_variables) | Set variables on an instance |
365
+
366
+ ---
367
+
368
+ ### Process Operations
369
+
370
+ Process operations manage BPMN workflow deployments and instances.
371
+
372
+ #### deploy_process
373
+
374
+ Deploy one or more BPMN files to the Zeebe cluster.
375
+
376
+ ```ruby
377
+ deploy_process(*paths, tenant_id: nil) → Hash{String => Integer}
378
+ ```
379
+
380
+ | Parameter | Type | Description |
381
+ |-----------|------|-------------|
382
+ | `*paths` | `String` | One or more paths to BPMN files |
383
+ | `tenant_id:` | `String`, `nil` | Tenant ID for multi-tenancy (optional) |
384
+
385
+ **Returns:** A hash mapping each BPMN process ID to its process definition key.
386
+
387
+ **Raises:** `Errno::ENOENT` if a file doesn't exist; `Busybee::GRPC::Error` if deployment fails (e.g., invalid BPMN syntax).
388
+
389
+ ```ruby
390
+ # Deploy a single workflow
391
+ result = client.deploy_process("workflows/order-fulfillment.bpmn")
392
+ # => { "order-fulfillment" => 2251799813685249 }
393
+
394
+ # Deploy multiple workflows at once
395
+ result = client.deploy_process("order.bpmn", "payment.bpmn", "shipping.bpmn")
396
+ # => { "order-fulfillment" => 123, "payment-process" => 456, "shipping-process" => 789 }
397
+ ```
398
+
399
+ #### start_instance
400
+
401
+ Start a new process instance from a deployed workflow.
402
+
403
+ ```ruby
404
+ start_instance(bpmn_process_id, vars: {}, version: :latest, tenant_id: nil) → Integer
405
+ ```
406
+
407
+ | Parameter | Type | Description |
408
+ |-----------|------|-------------|
409
+ | `bpmn_process_id` | `String` | The BPMN process ID (from the workflow definition) |
410
+ | `vars:` | `Hash` | Variables to initialize the process with (default: `{}`) |
411
+ | `version:` | `Integer`, `:latest`, `nil` | Process version to start (default: `:latest`) |
412
+ | `tenant_id:` | `String`, `nil` | Tenant ID for multi-tenancy (optional) |
413
+
414
+ **Returns:** The process instance key (an integer uniquely identifying this instance).
415
+
416
+ **Raises:** `ArgumentError` if `vars` is not a Hash; `Busybee::GRPC::Error` if the process doesn't exist or starting fails.
417
+
418
+ ```ruby
419
+ # Start with variables
420
+ instance_key = client.start_instance("order-fulfillment", vars: {
421
+ orderId: "ORD-123",
422
+ customer: { name: "Alice", email: "alice@example.com" }
423
+ })
424
+ # => 2251799813685300
425
+
426
+ # Start a specific version
427
+ instance_key = client.start_instance("order-fulfillment", version: 3)
428
+
429
+ # Alias: start_process_instance
430
+ instance_key = client.start_process_instance("order-fulfillment", vars: { orderId: "ORD-456" })
431
+ ```
432
+
433
+ #### cancel_instance
434
+
435
+ Cancel a running process instance.
436
+
437
+ ```ruby
438
+ cancel_instance(process_instance_key, ignore_missing: false) → Boolean
439
+ ```
440
+
441
+ | Parameter | Type | Description |
442
+ |-----------|------|-------------|
443
+ | `process_instance_key` | `Integer` | The process instance key to cancel |
444
+ | `ignore_missing:` | `Boolean` | Return `false` instead of raising if instance not found (default: `false`) |
445
+
446
+ **Returns:** `true` if cancelled, `false` if not found and `ignore_missing: true`.
447
+
448
+ **Raises:** `Busybee::GRPC::Error` if cancellation fails (unless instance not found and `ignore_missing: true`).
449
+
450
+ ```ruby
451
+ # Cancel an instance (raises if not found)
452
+ client.cancel_instance(2251799813685300)
453
+ # => true
454
+
455
+ # Cancel without raising if already completed/cancelled
456
+ cancelled = client.cancel_instance(2251799813685300, ignore_missing: true)
457
+ # => false (if instance was already gone)
458
+
459
+ # Alias: cancel_process_instance
460
+ client.cancel_process_instance(instance_key, ignore_missing: true)
461
+ ```
462
+
463
+ ---
464
+
465
+ ### Job Operations
466
+
467
+ For conceptual background on jobs, the Job object, and JobStream, see [Working with Jobs](#working-with-jobs) above.
468
+
469
+ #### with_each_job
470
+
471
+ Poll for available jobs and process them with a block.
472
+
473
+ ```ruby
474
+ with_each_job(job_type, max_jobs: 25, job_timeout: 60_000, request_timeout: 60_000) { |job| ... } → Integer
475
+ ```
476
+
477
+ | Parameter | Type | Description |
478
+ |-----------|------|-------------|
479
+ | `job_type` | `String` | The job type to activate (from the BPMN task definition) |
480
+ | `max_jobs:` | `Integer` | Maximum jobs to return (default: 25) |
481
+ | `job_timeout:` | `Integer`, `Duration` | How long a job stays locked to this worker (default: 60s) |
482
+ | `request_timeout:` | `Integer`, `Duration` | How long to wait for jobs before returning (default: 60s) |
483
+
484
+ **Yields:** Each activated job as a `Busybee::Job` object.
485
+
486
+ **Returns:** The number of jobs processed.
487
+
488
+ **Raises:** `ArgumentError` if no block is given; `Busybee::GRPC::Error` if activation fails.
489
+
490
+ **Long-polling behavior:** If no jobs are immediately available, the request waits up to `request_timeout` for jobs to become available. When jobs arrive (or the timeout expires), the method returns with whatever jobs it collected.
491
+
492
+ **Configuration:** The worker name sent to Zeebe comes from `Busybee.worker_name`, which defaults to the machine hostname. You can override it:
493
+
494
+ ```ruby
495
+ Busybee.configure do |config|
496
+ config.worker_name = "order-processor-1"
497
+ end
498
+ ```
499
+
500
+ ```ruby
501
+ # Process jobs in a loop
502
+ loop do
503
+ count = client.with_each_job("send-email", max_jobs: 10) do |job|
504
+ EmailService.deliver(to: job.variables.email, subject: job.variables.subject)
505
+ job.complete!(sentAt: Time.now.iso8601)
506
+ rescue EmailService::DeliveryError => e
507
+ job.fail!(e, backoff: 1.minute)
508
+ end
509
+
510
+ puts "Processed #{count} jobs"
511
+ end
512
+ ```
513
+
514
+ #### open_job_stream
515
+
516
+ Open a long-lived stream for job activation. Jobs are pushed to your code as they become available.
517
+
518
+ ```ruby
519
+ open_job_stream(job_type, job_timeout: 60_000) → Busybee::JobStream
520
+ ```
521
+
522
+ | Parameter | Type | Description |
523
+ |-----------|------|-------------|
524
+ | `job_type` | `String` | The job type to activate |
525
+ | `job_timeout:` | `Integer`, `Duration` | How long a job stays locked (default: 60s) |
526
+
527
+ **Returns:** A `Busybee::JobStream` object.
528
+
529
+ **Raises:** `Busybee::GRPC::Error` if stream creation fails.
530
+
531
+ **Remember:** Streams only receive jobs created *after* the stream opens. See [Polling vs Streaming](#polling-vs-streaming).
532
+
533
+ ```ruby
534
+ stream = client.open_job_stream("send-email", job_timeout: 2.minutes)
535
+
536
+ # Handle graceful shutdown
537
+ trap("INT") { stream.close }
538
+ trap("TERM") { stream.close }
539
+
540
+ stream.each do |job|
541
+ process_email(job)
542
+ job.complete!
543
+ rescue StandardError => e
544
+ job.fail!(e)
545
+ end
546
+
547
+ puts "Stream closed, shutting down"
548
+ ```
549
+
550
+ #### complete_job
551
+
552
+ Mark a job as successfully completed, optionally returning variables to the workflow.
553
+
554
+ ```ruby
555
+ complete_job(job_key, vars: {}) → Object
556
+ ```
557
+
558
+ | Parameter | Type | Description |
559
+ |-----------|------|-------------|
560
+ | `job_key` | `Integer` | The job key |
561
+ | `vars:` | `Hash` | Variables to return to the workflow (default: `{}`) |
562
+
563
+ **Returns:** A truthy response from the gateway.
564
+
565
+ **Raises:** `Busybee::GRPC::Error` if completion fails.
566
+
567
+ **Prefer `Job#complete!`** when processing jobs through `with_each_job` or `open_job_stream`.
568
+
569
+ ```ruby
570
+ # Direct client call (when you only have the job key)
571
+ client.complete_job(job_key, vars: { result: "success" })
572
+
573
+ # Preferred: use the Job object
574
+ job.complete!(result: "success")
575
+ ```
576
+
577
+ #### fail_job
578
+
579
+ Mark a job as failed. The workflow engine will retry the job (if retries remain) after the backoff period.
580
+
581
+ ```ruby
582
+ fail_job(job_key, error_message, retries: nil, backoff: nil) → Object
583
+ ```
584
+
585
+ | Parameter | Type | Description |
586
+ |-----------|------|-------------|
587
+ | `job_key` | `Integer` | The job key |
588
+ | `error_message` | `String` | Error message describing the failure |
589
+ | `retries:` | `Integer`, `nil` | Override the remaining retry count (default: decrement by 1) |
590
+ | `backoff:` | `Integer`, `Duration`, `nil` | Delay before retry (see below) |
591
+
592
+ **Returns:** A truthy response from the gateway.
593
+
594
+ **Raises:** `Busybee::GRPC::Error` if the fail operation fails.
595
+
596
+ **Default backoff:** When `backoff:` is omitted, Busybee uses `Busybee.default_fail_job_backoff` (default: 5 seconds). You can change this globally:
597
+
598
+ ```ruby
599
+ Busybee.configure do |config|
600
+ config.default_fail_job_backoff = 30_000 # 30 seconds
601
+ end
602
+ ```
603
+
604
+ **Prefer `Job#fail!`** when processing jobs through `with_each_job` or `open_job_stream`.
605
+
606
+ ```ruby
607
+ # Direct client call
608
+ client.fail_job(job_key, "Payment gateway timeout", backoff: 30.seconds)
609
+
610
+ # Preferred: use the Job object
611
+ job.fail!("Payment gateway timeout", backoff: 30.seconds)
612
+ ```
613
+
614
+ #### throw_bpmn_error
615
+
616
+ Throw a BPMN error that can be caught by an error boundary event in the workflow. Use this for business-level errors that the workflow is designed to handle (as opposed to technical failures, which should use `fail_job`).
617
+
618
+ ```ruby
619
+ throw_bpmn_error(job_key, error_code, message: "") → Object
620
+ ```
621
+
622
+ | Parameter | Type | Description |
623
+ |-----------|------|-------------|
624
+ | `job_key` | `Integer` | The job key |
625
+ | `error_code` | `String` | BPMN error code (must match the error catch event) |
626
+ | `message:` | `String` | Optional error message for context (default: `""`) |
627
+
628
+ **Returns:** A truthy response from the gateway.
629
+
630
+ **Raises:** `Busybee::GRPC::Error` if the operation fails.
631
+
632
+ **Prefer `Job#throw_bpmn_error!`** when processing jobs through `with_each_job` or `open_job_stream`.
633
+
634
+ ```ruby
635
+ # Direct client call
636
+ client.throw_bpmn_error(job_key, "ORDER_NOT_FOUND", message: "Order ORD-123 does not exist")
637
+
638
+ # Preferred: use the Job object (also accepts symbols and exceptions)
639
+ job.throw_bpmn_error!(:order_not_found, "Order ORD-123 does not exist")
640
+ ```
641
+
642
+ #### update_job_retries
643
+
644
+ Update the retry count for a job. Useful for giving a job more attempts after fixing an underlying issue.
645
+
646
+ ```ruby
647
+ update_job_retries(job_key, retries) → Object
648
+ ```
649
+
650
+ | Parameter | Type | Description |
651
+ |-----------|------|-------------|
652
+ | `job_key` | `Integer` | The job key |
653
+ | `retries` | `Integer` | The new retry count |
654
+
655
+ **Returns:** A truthy response from the gateway.
656
+
657
+ **Raises:** `Busybee::GRPC::Error` if the update fails.
658
+
659
+ ```ruby
660
+ client.update_job_retries(job_key, 5)
661
+ ```
662
+
663
+ #### update_job_timeout
664
+
665
+ Extend the deadline for a job that's taking longer than expected. Call this before the current deadline expires to prevent the job from timing out and being reassigned to another worker.
666
+
667
+ ```ruby
668
+ update_job_timeout(job_key, timeout) → Object
669
+ ```
670
+
671
+ | Parameter | Type | Description |
672
+ |-----------|------|-------------|
673
+ | `job_key` | `Integer` | The job key |
674
+ | `timeout` | `Integer`, `Duration` | New timeout in milliseconds or as a Duration |
675
+
676
+ **Returns:** A truthy response from the gateway.
677
+
678
+ **Raises:** `Busybee::GRPC::Error` if the update fails.
679
+
680
+ ```ruby
681
+ client.update_job_timeout(job_key, 30.seconds)
682
+ ```
683
+
684
+ #### resolve_incident
685
+
686
+ Resolve an incident so the workflow can continue. Incidents occur when a job fails with no retries remaining, or when an expression evaluation fails.
687
+
688
+ ```ruby
689
+ resolve_incident(incident_key) → true
690
+ ```
691
+
692
+ | Parameter | Type | Description |
693
+ |-----------|------|-------------|
694
+ | `incident_key` | `Integer` | The incident key to resolve |
695
+
696
+ **Returns:** `true` if resolved.
697
+
698
+ **Raises:** `Busybee::GRPC::Error` if the incident doesn't exist or resolution fails.
699
+
700
+ Before resolving, you typically need to fix the underlying problem (e.g., set missing variables, fix external services, or update job retries):
701
+
702
+ ```ruby
703
+ # Fix the problem first
704
+ client.set_variables(process_instance_key, vars: { missingField: "now provided" })
705
+
706
+ # Or give the job more retries
707
+ client.update_job_retries(job_key, 3)
708
+
709
+ # Then resolve the incident
710
+ client.resolve_incident(incident_key)
711
+ ```
712
+
713
+ ---
714
+
715
+ ### Message Operations
716
+
717
+ Messages enable communication with waiting process instances. A message correlates to instances by matching a correlation key against process variables.
718
+
719
+ #### publish_message
720
+
721
+ Publish a message that correlates to waiting process instances.
722
+
723
+ ```ruby
724
+ publish_message(name, correlation_key:, vars: {}, ttl: nil, tenant_id: nil) → Integer
725
+ ```
726
+
727
+ | Parameter | Type | Description |
728
+ |-----------|------|-------------|
729
+ | `name` | `String` | The message name (must match the message catch event in BPMN) |
730
+ | `correlation_key:` | `String` | Key to match against process instance variables |
731
+ | `vars:` | `Hash` | Variables to pass with the message (default: `{}`) |
732
+ | `ttl:` | `Integer`, `ActiveSupport::Duration`, `nil` | Time-to-live (see below) |
733
+ | `tenant_id:` | `String`, `nil` | Tenant ID for multi-tenancy (optional) |
734
+
735
+ **Returns:** The message key.
736
+
737
+ **Raises:** `ArgumentError` if `vars` is not a Hash; `Busybee::GRPC::Error` if publishing fails.
738
+
739
+ **Default TTL:** When `ttl:` is omitted, Busybee uses `Busybee.default_message_ttl` (default: 10 seconds). You can change this globally:
740
+
741
+ ```ruby
742
+ Busybee.configure do |config|
743
+ config.default_message_ttl = 60_000 # 1 minute, in milliseconds
744
+ end
745
+ ```
746
+
747
+ The TTL determines how long the message remains buffered if no matching instance is found. If an instance starts waiting before the TTL expires, the message is delivered. After the TTL expires, the message is discarded.
748
+
749
+ ```ruby
750
+ # Publish with default TTL
751
+ client.publish_message("order-confirmed", correlation_key: "ORD-123")
752
+
753
+ # Publish with variables and custom TTL using ActiveSupport::Duration
754
+ client.publish_message("payment-received",
755
+ correlation_key: order.id.to_s,
756
+ vars: { paymentId: payment.id, amount: payment.amount },
757
+ ttl: 5.minutes
758
+ )
759
+
760
+ # Publish with TTL in milliseconds
761
+ client.publish_message("order-shipped", correlation_key: "ORD-123", ttl: 60_000)
762
+ ```
763
+
764
+ #### broadcast_signal
765
+
766
+ Broadcast a signal to all process instances with matching signal catch events. Unlike messages, signals don't use correlation—they're delivered to all waiting instances.
767
+
768
+ ```ruby
769
+ broadcast_signal(signal_name, vars: {}, tenant_id: nil) → Integer
770
+ ```
771
+
772
+ | Parameter | Type | Description |
773
+ |-----------|------|-------------|
774
+ | `signal_name` | `String` | The signal name (must match signal catch events in BPMN) |
775
+ | `vars:` | `Hash` | Variables to pass with the signal (default: `{}`) |
776
+ | `tenant_id:` | `String`, `nil` | Tenant ID for multi-tenancy (optional) |
777
+
778
+ **Returns:** The signal key.
779
+
780
+ **Raises:** `ArgumentError` if `vars` is not a Hash; `Busybee::GRPC::Error` if broadcasting fails.
781
+
782
+ ```ruby
783
+ # Broadcast a simple signal
784
+ client.broadcast_signal("system-shutdown")
785
+
786
+ # Broadcast with variables
787
+ client.broadcast_signal("price-updated", vars: {
788
+ productId: "PROD-789",
789
+ newPrice: 29.99
790
+ })
791
+ ```
792
+
793
+ ---
794
+
795
+ ### Variable Operations
796
+
797
+ Variable operations let you modify process instance state from outside the workflow.
798
+
799
+ #### set_variables
800
+
801
+ Set variables on a process instance or element (e.g., a service task).
802
+
803
+ ```ruby
804
+ set_variables(element_instance_key, vars: {}, local: false) → Integer
805
+ ```
806
+
807
+ | Parameter | Type | Description |
808
+ |-----------|------|-------------|
809
+ | `element_instance_key` | `Integer` | The process instance key or element instance key |
810
+ | `vars:` | `Hash` | Variables to set (default: `{}`) |
811
+ | `local:` | `Boolean` | If `true`, variables are scoped locally (default: `false`) |
812
+
813
+ **Returns:** The set variables operation key.
814
+
815
+ **Raises:** `ArgumentError` if `vars` is not a Hash; `Busybee::GRPC::Error` if setting variables fails.
816
+
817
+ When `local: false` (the default), variables propagate up to the process instance scope and are visible everywhere. When `local: true`, variables are scoped to the specific element and won't be visible to parent or sibling elements.
818
+
819
+ ```ruby
820
+ # Set variables on a process instance (propagates globally)
821
+ client.set_variables(process_instance_key, vars: { status: "approved", approvedBy: "manager@example.com" })
822
+
823
+ # Set local variables on a specific element (won't propagate)
824
+ client.set_variables(element_instance_key, vars: { tempCalculation: 42 }, local: true)
825
+ ```