bemi 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/Makefile +8 -0
- data/README.md +489 -0
- data/Rakefile +8 -0
- data/bemi.gemspec +37 -0
- data/lib/bemi/version.rb +5 -0
- data/lib/bemi.rb +6 -0
- data/sig/bemi.rbs +4 -0
- data/spec/bemi_spec.rb +7 -0
- data/spec/spec_helper.rb +15 -0
- metadata +63 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 98fa40708b6bfd92280161b572fc052aa2f8aefb6f9676976c02f1c270547228
|
4
|
+
data.tar.gz: 745f8075714453a0e72aca4d82910c9c847be3b12f9d961b1a353c5c0616b30f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 12dd253ff5038f98970cd08a3c55dba31e289049b6761d12a3eb90d47dff5eb14957d1c6428f612f17eac95573b81ba639812f0b9609b726aea59af9d069f98a
|
7
|
+
data.tar.gz: b61d9ae6a853a9cb290ac4b637635cfb23ee4bcbc6d89596c9b7dd1781862bfc4f1a3c1cb2daddb0a8cc3272e72f23a40035c74914e534fbecc7cb708228fd25
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
8
|
+
|
9
|
+
## Our Standards
|
10
|
+
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
12
|
+
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
18
|
+
|
19
|
+
Examples of unacceptable behavior include:
|
20
|
+
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
22
|
+
advances of any kind
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
24
|
+
* Public or private harassment
|
25
|
+
* Publishing others' private information, such as a physical or email
|
26
|
+
address, without their explicit permission
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
28
|
+
professional setting
|
29
|
+
|
30
|
+
## Enforcement Responsibilities
|
31
|
+
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
33
|
+
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
35
|
+
|
36
|
+
## Scope
|
37
|
+
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
39
|
+
|
40
|
+
## Enforcement
|
41
|
+
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at exaspark@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
|
43
|
+
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
45
|
+
|
46
|
+
## Enforcement Guidelines
|
47
|
+
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
49
|
+
|
50
|
+
### 1. Correction
|
51
|
+
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
53
|
+
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
55
|
+
|
56
|
+
### 2. Warning
|
57
|
+
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
59
|
+
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
61
|
+
|
62
|
+
### 3. Temporary Ban
|
63
|
+
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
65
|
+
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
67
|
+
|
68
|
+
### 4. Permanent Ban
|
69
|
+
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
71
|
+
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
73
|
+
|
74
|
+
## Attribution
|
75
|
+
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
78
|
+
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
80
|
+
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
82
|
+
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 exAspArk
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/Makefile
ADDED
data/README.md
ADDED
@@ -0,0 +1,489 @@
|
|
1
|
+
# Bemi
|
2
|
+
|
3
|
+
A Ruby framework for managing code workflows. Bemi allows to describe and chain multiple actions similarly to function pipelines, have the execution reliability of a background job framework, unlock full visibility into business and infrastructure processes, distribute workload and implementation across multiple services as simply as running everything in the monolith.
|
4
|
+
|
5
|
+
Bemi stands for "beginner mindset" and is pronounced as [ˈbɛmɪ].
|
6
|
+
|
7
|
+
## Contents
|
8
|
+
|
9
|
+
* [Overview](#overview)
|
10
|
+
* [Code example](#code-example)
|
11
|
+
* [Architecture](#architecture)
|
12
|
+
* [Usage](#usage)
|
13
|
+
* [Workflows](#workflows)
|
14
|
+
* [Workflow definition](#workflow-definition)
|
15
|
+
* [Workflow validation](#workflow-validation)
|
16
|
+
* [Workflow concurrency](#workflow-concurrency)
|
17
|
+
* [Workflow querying](#workflow-querying)
|
18
|
+
* [Actions](#actions)
|
19
|
+
* [Action validation](#action-validation)
|
20
|
+
* [Action error handling](#action-error-handling)
|
21
|
+
* [Action rollback](#action-rollback)
|
22
|
+
* [Action querying](#action-querying)
|
23
|
+
* [Action concurrency](#action-concurrency)
|
24
|
+
* [Installation](#installation)
|
25
|
+
* [Alternatives](#alternatives)
|
26
|
+
* [License](#license)
|
27
|
+
* [Code of Conduct](#code-of-conduct)
|
28
|
+
|
29
|
+
## Overview
|
30
|
+
|
31
|
+
* Explicitly defined and orchestrated workflows instead of implicit execution sequences and spaghetti code
|
32
|
+
* Synchronous, scheduled, and background execution of workflows
|
33
|
+
* Improved reliability with transactions, queues, retries, timeouts, rate limiting, and priorities
|
34
|
+
* Implemented patterns like sagas, distributed tracing, transactional outbox, and railway-oriented programming
|
35
|
+
* Full visibility into the system, event logging for debugging and auditing, and monitoring with the web UI
|
36
|
+
* Simple distributed workflow execution across services, applications, and programming languages (soon)
|
37
|
+
|
38
|
+
## Code example
|
39
|
+
|
40
|
+
Here is an example of a multi-step workflow:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# app/workflows/order_workflow.rb
|
44
|
+
class OrderWorkflow < Bemi::Workflow
|
45
|
+
name :order
|
46
|
+
|
47
|
+
def perform
|
48
|
+
action :process_payment, sync: true
|
49
|
+
action :send_confirmation, wait_for: [:process_payment], async: true
|
50
|
+
action :ship_package, wait_for: [:process_payment], async: { queue: 'warehouse' }
|
51
|
+
action :request_feedback, wait_for: [:ship_package], async: { delay: 7.days.to_i }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
To run an instance of this workflow:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# Init a workflow, it will stop at the first action and wait until it is executed synchronously
|
60
|
+
workflow = Bemi.perform_workflow(:order, context: { order_id: params[:order_id], user_id: current_user.id })
|
61
|
+
|
62
|
+
# Process payment by running the first workflow action synchronously
|
63
|
+
Bemi.perform_action(:process_payment, workflow_id: workflow.id, input: { payment_token: params[:payment_token] })
|
64
|
+
|
65
|
+
# Once the payment is processed, the next actions in the workflow
|
66
|
+
# will be executed automatically through background workers
|
67
|
+
```
|
68
|
+
|
69
|
+
Each action can be implemented in a separate class that can be called "action", "service", "use case", "interactor", "mutation"... you name it:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# app/actions/order/process_payment_action.rb
|
73
|
+
class Order::ProcessPaymentAction < Bemi::Action
|
74
|
+
name :process_payment
|
75
|
+
|
76
|
+
def perform
|
77
|
+
payment = PaymentProcessor.pay_for!(workflow.context[:order_id], input[:payment_token])
|
78
|
+
{ payment_id: payment.id }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
# app/actions/order/send_confirmation_action.rb
|
85
|
+
class Order::SendConfirmationAction < Bemi::Action
|
86
|
+
name :send_confirmation
|
87
|
+
|
88
|
+
def perform
|
89
|
+
payment_output = wait_for(:process_payment).output
|
90
|
+
mail = OrderMailer.send_confirmation(payment_output[:payment_id])
|
91
|
+
{ delivered: mail.delivered? }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# ../warehouse/app/actions/order/ship_package_action.rb
|
98
|
+
class Order::ShipPackageAction < Bemi::Action
|
99
|
+
name :ship_package
|
100
|
+
|
101
|
+
def perform
|
102
|
+
# Run a separate "shipment" workflow
|
103
|
+
shipment_workflow = Bemi.perform_workflow(:shipment, context: { order_id: workflow.context[:order_id] })
|
104
|
+
{ shipment_workflow_id: shipment_workflow.id }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# app/actions/order/request_feedback_action.rb
|
111
|
+
class Order::RequestFeedbackAction < Bemi::Action
|
112
|
+
name :request_feedback
|
113
|
+
|
114
|
+
def perform
|
115
|
+
mail = OrderMailer.request_feedback(workflow.context[:user_id])
|
116
|
+
{ delivered: mail.delivered? }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
## Architecture
|
122
|
+
|
123
|
+
Bemi is designed to be lightweight and simple to use by default. As a system dependency, all you need is PostgreSQL.
|
124
|
+
|
125
|
+
```
|
126
|
+
/‾‾‾\
|
127
|
+
\___/
|
128
|
+
__/ \__
|
129
|
+
/ User \
|
130
|
+
│
|
131
|
+
- - - - - │ - - - - - - - - - - - - - - - -
|
132
|
+
╵ │ Start "order" workflow ╵
|
133
|
+
╵ ∨ ╵
|
134
|
+
╵ ________________ ╵ [‾‾‾‾‾‾‾‾‾‾‾‾]
|
135
|
+
╵ ┆ [Rails Server] ┆ Run "process_payment" ╵ [------------]
|
136
|
+
╵ ┆ with ┆⸺⸺⸺⸺⸺⸺⸺⸺⸺> [ PostgreSQL ]
|
137
|
+
╵ ┆ Bemi gem ┆ action synchronously ╵ [------------]
|
138
|
+
╵ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ╵ [____________]
|
139
|
+
╵ ╵ │
|
140
|
+
╵ ╵ │
|
141
|
+
╵ _______________ ╵ │
|
142
|
+
╵ | [Bemi Worker] | Run "send_confirmation" ╵ │
|
143
|
+
╵ | "default" | <⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺│
|
144
|
+
╵ | queue | action async ╵ │ - - - - - - - - - - - - - - - - - - -
|
145
|
+
╵ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ╵ │ ╵ _______________ ╵
|
146
|
+
╵ ╵ │ ╵ Run "ship_package" | [Bemi Worker] | ╵
|
147
|
+
╵ ╵ │⸺⸺⸺⸺⸺⸺⸺⸺⸺> | "warehouse" | ╵
|
148
|
+
╵ ╵ │ ╵ action async | queue | ╵
|
149
|
+
╵ _______________ ╵ │ ╵ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ╵
|
150
|
+
╵ | [Bemi Worker] | Run "request_feedback" ╵ │ ╵ ╵
|
151
|
+
╵ | "default" | <⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺╵ ╵ ╵
|
152
|
+
╵ | queue | action by schedule ╵ ╵ ╵
|
153
|
+
╵ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ╵ ╵ ╵
|
154
|
+
╵ ╵ ╵ ╵
|
155
|
+
╵ Store service ╵ ╵ Warehouse service ╵
|
156
|
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
157
|
+
```
|
158
|
+
|
159
|
+
* Workflows
|
160
|
+
|
161
|
+
Bemi orchestrates workflows by persisting their execution state into PostgreSQL. When connecting to PostgreSQL, Bemi first scans the codebase and registers all workflows uniquely identified by `name`. Workflows describe a sequence of actions by using the DSL written in Ruby that can be run synchronously in the same process or asynchronously and by schedule in workers.
|
162
|
+
|
163
|
+
* Actions
|
164
|
+
|
165
|
+
Actions are also uniquely identified by `name`. They can receive data from an input if ran synchronously, previously executed actions if they depend on them, and the shared workflow execution context. They can be implemented and executed in any service or application as long as it is connected to the same PostgreSQL instance. So, there is no need to deal with message passing by implementing APIs, callbacks, message buses, data serialization, etc.
|
166
|
+
|
167
|
+
* Workers
|
168
|
+
|
169
|
+
Bemi workers allow running actions that are executed asynchronously or by schedule. One worker represents a process with multiple threads to enable concurrency. Workers can process one or more `queues` and execute different actions across different workflows simultaneously if they are assigned to the same workers' queues.
|
170
|
+
|
171
|
+
See the [Alternatives](#alternatives) section that describes how Bemi is different from other tools you might be familiar with.
|
172
|
+
|
173
|
+
## Usage
|
174
|
+
|
175
|
+
### Workflows
|
176
|
+
|
177
|
+
#### Workflow definition
|
178
|
+
|
179
|
+
Workflows declaratively describe actions that can be executed
|
180
|
+
|
181
|
+
* Synchronously
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
class RegistrationWorkflow < Bemi::Workflow
|
185
|
+
name :registration
|
186
|
+
|
187
|
+
def perform
|
188
|
+
action :create_user, sync: true
|
189
|
+
action :send_confirmation_email, sync: true
|
190
|
+
action :confirm_email_address, sync: true
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
workflow = Bemi.perform_workflow(:registration, context: { email: params[:email] })
|
195
|
+
Bemi.perform_action(:create_user, workflow_id: workflow.id, input: { password: params[:password] })
|
196
|
+
Bemi.perform_action(:send_confirmation_email, workflow_id: workflow.id)
|
197
|
+
Bemi.perform_action(:confirm_email_address, workflow_id: workflow.id, input: { token: params[:token] })
|
198
|
+
```
|
199
|
+
|
200
|
+
* Asynchronously
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class RegistrationWorkflow < Bemi::Workflow
|
204
|
+
name :registration
|
205
|
+
|
206
|
+
def perform
|
207
|
+
action :create_user, async: true
|
208
|
+
action :send_welcome_email, wait_for: [:create_user], async: true
|
209
|
+
action :run_background_check, wait_for: [:send_welcome_email], async: { queue: 'kyc' }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
Bemi.perform_workflow(:registration, context: { email: params[:email] })
|
214
|
+
```
|
215
|
+
|
216
|
+
* By schedule
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class RegistrationWorkflow < Bemi::Workflow
|
220
|
+
name :registration
|
221
|
+
|
222
|
+
def perform
|
223
|
+
action :create_user, async: { cron: '0 2 * * *' } # daily at 2am
|
224
|
+
action :send_welcome_email, async: { cron: '0 3 * * *', queue: 'emails', priority: 10 }
|
225
|
+
action :run_background_check, async: { delay: 24.hours.to_i },
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
Bemi.perform_workflow(:registration, context: { email: params[:email] })
|
230
|
+
```
|
231
|
+
|
232
|
+
#### Workflow validation
|
233
|
+
|
234
|
+
Workflow can define the shape of the `context` and validate it against a JSON Schema
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
class RegistrationWorkflow < Bemi::Workflow
|
238
|
+
name :registration
|
239
|
+
context_schema {
|
240
|
+
type: :object,
|
241
|
+
properties: { email: { type: :string } },
|
242
|
+
required: [:email],
|
243
|
+
}
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
#### Workflow concurrency
|
248
|
+
|
249
|
+
It is possible to set workflow concurrency options if you want to guarantee uniqueness
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
class RegistrationWorkflow < Bemi::Workflow
|
253
|
+
name :registration
|
254
|
+
concurrency limit: 1, on_conflict: :raise # or :reject
|
255
|
+
end
|
256
|
+
|
257
|
+
Bemi.perform_workflow(:registration, context: { email: 'email@example.com' })
|
258
|
+
Bemi.perform_workflow(:registration, context: { email: 'email@example.com' })
|
259
|
+
# => Bemi::ConcurrencyError: cannot run more than 1 'registration' workflow at the same time
|
260
|
+
```
|
261
|
+
|
262
|
+
#### Workflow querying
|
263
|
+
|
264
|
+
You can query workflows if, for example, one of the actions in the middle of the workflow execution needs to be triggered manually
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
workflow = Bemi.find_workflow(:registration, context: { email: 'email@example.com' })
|
268
|
+
|
269
|
+
workflow.canceled?
|
270
|
+
workflow.completed?
|
271
|
+
workflow.failed?
|
272
|
+
workflow.running?
|
273
|
+
workflow.timed_out?
|
274
|
+
|
275
|
+
# Persisted and deserialized from JSON
|
276
|
+
workflow.context
|
277
|
+
|
278
|
+
Bemi.perform_action(:confirm_email_address, workflow_id: workflow.id)
|
279
|
+
```
|
280
|
+
|
281
|
+
### Actions
|
282
|
+
|
283
|
+
#### Action validation
|
284
|
+
|
285
|
+
Bemi allows to define the shape of actions' inputs and outputs and validate it against a JSON Schema
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
class RegistrationWorkflow < Bemi::Workflow
|
289
|
+
name :registration
|
290
|
+
|
291
|
+
def perform
|
292
|
+
action :create_user,
|
293
|
+
sync: true,
|
294
|
+
input_schema: {
|
295
|
+
type: :object, properties: { password: { type: :string } }, required: [:password],
|
296
|
+
},
|
297
|
+
output_schema: {
|
298
|
+
type: :object, properties: { user_id: { type: :integer } }, required: [:user_id],
|
299
|
+
}
|
300
|
+
end
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
304
|
+
#### Action error handling
|
305
|
+
|
306
|
+
Custom `retry` count
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
class RegistrationWorkflow < Bemi::Workflow
|
310
|
+
name :registration
|
311
|
+
|
312
|
+
def perform
|
313
|
+
action :create_user, async: true, on_error: { retry: :exponential_backoff } # default retry option
|
314
|
+
action :send_welcome_email, async: true, on_error: { retry: 1 }
|
315
|
+
end
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
Custom error handler with `around_perform`
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
class Registration::SendWelcomeEmailAction < Bemi::Action
|
323
|
+
name :send_welcome_email
|
324
|
+
around_perform :error_handler
|
325
|
+
|
326
|
+
def perform
|
327
|
+
user = User.find(workflow.context[:user_id])
|
328
|
+
context[:email] = user.email
|
329
|
+
mail = UserMailer.welcome(user.id)
|
330
|
+
{ delivered: mail.delivered? }
|
331
|
+
end
|
332
|
+
|
333
|
+
private
|
334
|
+
|
335
|
+
def error_handler(&block)
|
336
|
+
block.call
|
337
|
+
rescue User::InvalidEmail => e
|
338
|
+
add_error!(:email, "Invalid email: #{context[:email]}")
|
339
|
+
fail! # don't retry if there is an application-level error
|
340
|
+
rescue Errno::ECONNRESET => e
|
341
|
+
raise e # retry by raising an exception if there is a temporary system-level error
|
342
|
+
end
|
343
|
+
end
|
344
|
+
```
|
345
|
+
|
346
|
+
#### Action rollback
|
347
|
+
|
348
|
+
If one of the actions in a workflow fails, all previously executed actions can be rolled back by defining a method called `rollback`
|
349
|
+
|
350
|
+
```ruby
|
351
|
+
class Order::ProcessPaymentAction < Bemi::Action
|
352
|
+
name :process_payment
|
353
|
+
around_rollback :rollback_notifier
|
354
|
+
|
355
|
+
def perform
|
356
|
+
payment = PaymentProcessor.pay_for!(workflow.context[:order_id], input[:payment_token])
|
357
|
+
{ payment_id: payment.id }
|
358
|
+
end
|
359
|
+
|
360
|
+
def rollback
|
361
|
+
refund = PaymentProcessor.issue_refund!(output[:payment_id], input[:payment_token])
|
362
|
+
{ refund_id: refund.id }
|
363
|
+
end
|
364
|
+
|
365
|
+
def rollback_notifier(&block)
|
366
|
+
OrderMailer.notify_cancelation(output[:payment_id])
|
367
|
+
block.call
|
368
|
+
end
|
369
|
+
end
|
370
|
+
```
|
371
|
+
|
372
|
+
#### Action querying
|
373
|
+
|
374
|
+
```ruby
|
375
|
+
workflow = Bemi.find_workflow(:registration, context: { email: 'email@example.com' })
|
376
|
+
action = Bemi.find_action(:create_user, workflow_id: workflow.id)
|
377
|
+
|
378
|
+
action.canceled?
|
379
|
+
action.completed?
|
380
|
+
action.failed?
|
381
|
+
action.running?
|
382
|
+
action.timed_out?
|
383
|
+
|
384
|
+
action.workflow
|
385
|
+
action.options
|
386
|
+
|
387
|
+
# Persisted and deserialized from JSON
|
388
|
+
action.input
|
389
|
+
action.output
|
390
|
+
action.errors
|
391
|
+
action.rollback_output
|
392
|
+
action.context
|
393
|
+
```
|
394
|
+
|
395
|
+
#### Action concurrency
|
396
|
+
|
397
|
+
Custom concurrency `limit`
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
class RegistrationWorkflow < Bemi::Workflow
|
401
|
+
name :registration
|
402
|
+
|
403
|
+
def perform
|
404
|
+
action :create_user, async: true, concurrency: { limit: 1, on_conflict: :reschedule } # or :raise
|
405
|
+
end
|
406
|
+
end
|
407
|
+
```
|
408
|
+
|
409
|
+
Custom uniqueness key defined in `concurrency_key`
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
class Registration::SendWelcomeEmailAction < Bemi::Action
|
413
|
+
name :send_welcome_email
|
414
|
+
|
415
|
+
def perform
|
416
|
+
mail = UserMailer.welcome(workflow.context[:user_id])
|
417
|
+
{ delivered: mail.delivered? }
|
418
|
+
end
|
419
|
+
|
420
|
+
def concurrency_key
|
421
|
+
"#{options[:async][:queue]}-#{input[:user_id]}"
|
422
|
+
end
|
423
|
+
end
|
424
|
+
```
|
425
|
+
|
426
|
+
## Installation
|
427
|
+
|
428
|
+
Add this line to your application's Gemfile:
|
429
|
+
|
430
|
+
```
|
431
|
+
gem 'bemi'
|
432
|
+
```
|
433
|
+
|
434
|
+
And then execute:
|
435
|
+
|
436
|
+
```
|
437
|
+
$ bundle install
|
438
|
+
```
|
439
|
+
|
440
|
+
Or install it yourself as:
|
441
|
+
|
442
|
+
```
|
443
|
+
$ gem install bemi
|
444
|
+
```
|
445
|
+
|
446
|
+
## Alternatives
|
447
|
+
|
448
|
+
#### Background jobs with persistent state
|
449
|
+
|
450
|
+
Tools like Sidekiq, Que, and GoodJob are similar since they execute jobs in background, persist the execution state, retry, etc. These tools, however, focus on executing a single job as a unit of work. Bemi can be used in a similar way to perform single actions. But it shines when it comes to managing chains of actions defined in workflows without a need to use complex callbacks.
|
451
|
+
|
452
|
+
Bemi orchestrates workflows instead of trying to choreograph them. This makes it easy to implement and maintain the code, reduce coordination overhead by having a central coordinator, improve observability, and simplify troubleshooting issues.
|
453
|
+
|
454
|
+
<details>
|
455
|
+
<summary>Orchestration</summary>
|
456
|
+
|
457
|
+
![Orchestration](images/orchestration.jpg)
|
458
|
+
</details>
|
459
|
+
|
460
|
+
<details>
|
461
|
+
<summary>Choreography</summary>
|
462
|
+
|
463
|
+
![Choreography](images/choreography.jpg)
|
464
|
+
</details>
|
465
|
+
|
466
|
+
#### Workflow orchestration tools and services
|
467
|
+
|
468
|
+
Tools like Temporal, AWS Step Functions, Argo Workflows, and Airflow allow orchestrating workflows, although they use quite different approaches.
|
469
|
+
|
470
|
+
Temporal was born based on challenges faced by big-tech and enterprise companies. As a result, it has a complex architecture with deployed clusters, different databases like Cassandra and optional Elasticsearch, and multiple services for frontend, matching, history, etc. It was initially designed for programming languages like Java and Go. Some would argue that the development and user experience are quite rough. Plus, at the time of this writing, it doesn't have an official stable SDK for our favorite programming language (Ruby).
|
471
|
+
|
472
|
+
AWS Step Functions rely on using AWS Lambda to execute each action in a workflow. For various reasons, not everyone can use AWS and their serverless solution. Additionally, workflows should be defined in JSON by using Amazon States Language instead of using a regular programming language.
|
473
|
+
|
474
|
+
Argo Workflows relies on using Kubernetes. It is closer to infrastructure-level workflows since it relies on running a container for each workflow action and doesn't provide code-level features. Additionally, it requires defining workflows in YAML.
|
475
|
+
|
476
|
+
Airflow is a popular tool for data engineering pipelines. Unfortunately, it can work only with Python.
|
477
|
+
|
478
|
+
#### Ruby frameworks for writing better code
|
479
|
+
|
480
|
+
There are many libraries that also implement useful patterns and allow better organize the code. For example, Interactor, ActiveInteraction, Mutations, Dry-Rb, and Trailblazer. They, however, don't help with asynchronous and distributed execution with better reliability guarantees that many of us rely on to execute code "out-of-band" to avoid running long-running workflows in a request/response lifecycle. For example, when sending emails, sending requests to other services, running multiple actions in parallel, etc.
|
481
|
+
|
482
|
+
|
483
|
+
## License
|
484
|
+
|
485
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
486
|
+
|
487
|
+
## Code of Conduct
|
488
|
+
|
489
|
+
Everyone interacting in the Bemi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/exAspArk/bemi/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bemi.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/bemi/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "bemi"
|
7
|
+
spec.version = Bemi::VERSION
|
8
|
+
spec.authors = ["exAspArk"]
|
9
|
+
spec.email = ["exaspark@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Ruby framework for managing code workflows."
|
12
|
+
spec.description = "Bemi allows to describe and chain multiple actions similarly to function pipelines, have the execution reliability of a background job framework, unlock full visibility into business and infrastructure processes, distribute workload and implementation across multiple services as simply as running everything in the monolith."
|
13
|
+
spec.homepage = "https://github.com/exAspArk/bemi"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/exAspArk/bemi"
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/exAspArk/bemi/blob/main/CHANGELOG.md"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(f == __FILE__) || f.match(%r{\A(bin|images|\.git|\.github)\/|(\.rspec|\.gitignore)})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
# Uncomment to register a new dependency of your gem
|
33
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
34
|
+
|
35
|
+
# For more information and examples about making a new gem, check out our
|
36
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
37
|
+
end
|
data/lib/bemi/version.rb
ADDED
data/lib/bemi.rb
ADDED
data/sig/bemi.rbs
ADDED
data/spec/bemi_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bemi"
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
# Enable flags like --only-failures and --next-failure
|
7
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
8
|
+
|
9
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
10
|
+
config.disable_monkey_patching!
|
11
|
+
|
12
|
+
config.expect_with :rspec do |c|
|
13
|
+
c.syntax = :expect
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bemi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- exAspArk
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Bemi allows to describe and chain multiple actions similarly to function
|
14
|
+
pipelines, have the execution reliability of a background job framework, unlock
|
15
|
+
full visibility into business and infrastructure processes, distribute workload
|
16
|
+
and implementation across multiple services as simply as running everything in the
|
17
|
+
monolith.
|
18
|
+
email:
|
19
|
+
- exaspark@gmail.com
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- CHANGELOG.md
|
25
|
+
- CODE_OF_CONDUCT.md
|
26
|
+
- Gemfile
|
27
|
+
- LICENSE.txt
|
28
|
+
- Makefile
|
29
|
+
- README.md
|
30
|
+
- Rakefile
|
31
|
+
- bemi.gemspec
|
32
|
+
- lib/bemi.rb
|
33
|
+
- lib/bemi/version.rb
|
34
|
+
- sig/bemi.rbs
|
35
|
+
- spec/bemi_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
homepage: https://github.com/exAspArk/bemi
|
38
|
+
licenses:
|
39
|
+
- MIT
|
40
|
+
metadata:
|
41
|
+
homepage_uri: https://github.com/exAspArk/bemi
|
42
|
+
source_code_uri: https://github.com/exAspArk/bemi
|
43
|
+
changelog_uri: https://github.com/exAspArk/bemi/blob/main/CHANGELOG.md
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 2.6.0
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubygems_version: 3.4.6
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Ruby framework for managing code workflows.
|
63
|
+
test_files: []
|