bemi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.1] - 2023-05-17
4
+
5
+ - Initial release
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in bemi.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
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
@@ -0,0 +1,8 @@
1
+ install:
2
+ bundle install
3
+
4
+ test:
5
+ bundle exec rspec
6
+
7
+ release:
8
+ bundle exec rake release
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Bemi
4
+ VERSION = "0.0.1"
5
+ end
data/lib/bemi.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bemi/version"
4
+
5
+ class Bemi
6
+ end
data/sig/bemi.rbs ADDED
@@ -0,0 +1,4 @@
1
+ class Bemi
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/spec/bemi_spec.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Bemi do
4
+ it "has a version number" do
5
+ expect(Bemi::VERSION).not_to be nil
6
+ end
7
+ end
@@ -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: []