simplekiq 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +125 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +16 -0
  6. data/CONTRIBUTING.md +31 -0
  7. data/CONTRIBUTORS.md +22 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.lock +81 -0
  10. data/LICENSE.txt +13 -0
  11. data/README.md +131 -0
  12. data/Rakefile +9 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/lib/simplekiq/batching_job.rb +163 -0
  16. data/lib/simplekiq/orchestration.rb +42 -0
  17. data/lib/simplekiq/orchestration_executor.rb +46 -0
  18. data/lib/simplekiq/orchestration_job.rb +28 -0
  19. data/lib/simplekiq/version.rb +3 -0
  20. data/lib/simplekiq.rb +14 -0
  21. data/simplekiq.gemspec +38 -0
  22. data/tasks/ci.rake +13 -0
  23. data/vendor/cache/ast-2.4.2.gem +0 -0
  24. data/vendor/cache/coderay-1.1.3.gem +0 -0
  25. data/vendor/cache/connection_pool-2.2.5.gem +0 -0
  26. data/vendor/cache/diff-lcs-1.5.0.gem +0 -0
  27. data/vendor/cache/method_source-1.0.0.gem +0 -0
  28. data/vendor/cache/parallel-1.22.1.gem +0 -0
  29. data/vendor/cache/parser-3.1.1.0.gem +0 -0
  30. data/vendor/cache/pry-0.13.1.gem +0 -0
  31. data/vendor/cache/rack-2.2.3.gem +0 -0
  32. data/vendor/cache/rack-protection-2.2.0.gem +0 -0
  33. data/vendor/cache/rainbow-3.1.1.gem +0 -0
  34. data/vendor/cache/rake-12.3.3.gem +0 -0
  35. data/vendor/cache/redis-4.5.1.gem +0 -0
  36. data/vendor/cache/regexp_parser-2.2.1.gem +0 -0
  37. data/vendor/cache/rexml-3.2.5.gem +0 -0
  38. data/vendor/cache/rspec-3.10.0.gem +0 -0
  39. data/vendor/cache/rspec-core-3.10.1.gem +0 -0
  40. data/vendor/cache/rspec-expectations-3.10.1.gem +0 -0
  41. data/vendor/cache/rspec-mocks-3.10.2.gem +0 -0
  42. data/vendor/cache/rspec-support-3.10.3.gem +0 -0
  43. data/vendor/cache/rspec_junit_formatter-0.5.1.gem +0 -0
  44. data/vendor/cache/rubocop-1.26.1.gem +0 -0
  45. data/vendor/cache/rubocop-ast-1.16.0.gem +0 -0
  46. data/vendor/cache/rubocop-performance-1.13.3.gem +0 -0
  47. data/vendor/cache/ruby-progressbar-1.11.0.gem +0 -0
  48. data/vendor/cache/sidekiq-5.2.10.gem +0 -0
  49. data/vendor/cache/standard-1.9.1.gem +0 -0
  50. data/vendor/cache/unicode-display_width-2.1.0.gem +0 -0
  51. metadata +180 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4caaf4c9bfc49507c627f025b8847b4cbfd36805897abc1c5d14117327cb0313
4
+ data.tar.gz: cdff30ac40b315e7e8dca636d013153631b710b8fb65c2bfffdf36632900b293
5
+ SHA512:
6
+ metadata.gz: 43789a7c661f3981035069935eecdefa4c03f8bfc2bd0eaeba8840018f54813f103475a5cd149752cbbef48e30f7c7363c98af548d85277995e0699df58b00dd
7
+ data.tar.gz: 9ab17b4b9d419d2551fd8cfe6070443015e35c1dc9c6adf195302a912aff52caf67fc7a5edea2c9f7733c43e0e799b790e00be07a4fbc4b6d0586501372be20d
@@ -0,0 +1,125 @@
1
+ version: 2.1
2
+
3
+ orbs:
4
+ gem: doximity/gem-publisher@0
5
+
6
+ executors:
7
+ ruby-2-6:
8
+ resource_class: small
9
+ docker:
10
+ - image: cimg/ruby:2.6
11
+ environment:
12
+ BUNDLE_VERSION: "~> 1.17"
13
+
14
+ # yaml anchor filters
15
+ master_only: &master_only
16
+ filters:
17
+ branches:
18
+ only: master
19
+ tags:
20
+ ignore: /.*/
21
+ pr_only: &pr_only
22
+ filters:
23
+ branches:
24
+ ignore: master
25
+ tags:
26
+ ignore: /.*/
27
+ version_tags_only: &version_tags_only
28
+ filters:
29
+ branches:
30
+ ignore: /.*/
31
+ tags:
32
+ only: /^v.*/
33
+
34
+ jobs:
35
+ build:
36
+ executor: ruby-2-6
37
+ steps:
38
+ - checkout
39
+ - run:
40
+ name: Install Bundler specific version
41
+ command: |
42
+ gem install bundler --version "${BUNDLE_VERSION}" --force
43
+ - restore_cache:
44
+ keys:
45
+ - v1-bundle-{{ checksum "Gemfile.lock" }}-
46
+ - run:
47
+ name: Install Ruby Dependencies
48
+ command: |
49
+ bundle config set --local path 'vendor/bundle'
50
+ bundle config set --local frozen 'true'
51
+ bundle install --local --jobs=4 --retry=3
52
+ - save_cache:
53
+ key: v1-bundle-{{ checksum "Gemfile.lock" }}-
54
+ paths:
55
+ - vendor/bundle
56
+ - run:
57
+ name: Run Tests
58
+ command: bundle exec rake ci:specs
59
+ - store_test_results:
60
+ name: Store test results
61
+ path: tmp/test-results
62
+ - run:
63
+ name: Run StandardRB
64
+ command: bundle exec standardrb
65
+ - persist_to_workspace:
66
+ root: .
67
+ paths:
68
+ - vendor/bundle
69
+
70
+ workflows:
71
+ version: 2
72
+
73
+ trunk:
74
+ jobs:
75
+ - build:
76
+ <<: *master_only
77
+ - gem/build:
78
+ <<: *master_only
79
+ executor: ruby-2-6
80
+ name: gem-build
81
+ requires:
82
+ - build
83
+
84
+ pull-requests:
85
+ jobs:
86
+ - build:
87
+ <<: *pr_only
88
+ - gem/build:
89
+ <<: *pr_only
90
+ executor: ruby-2-6
91
+ name: gem-build
92
+ requires:
93
+ - build
94
+ - pre-release-approval:
95
+ <<: *pr_only
96
+ type: approval
97
+ requires:
98
+ - gem-build
99
+ - gem/publish:
100
+ <<: *pr_only
101
+ name: gem-publish
102
+ to_rubygems: true
103
+ pre_release: true
104
+ requires:
105
+ - pre-release-approval
106
+ context: artifact_publishing
107
+
108
+ final-release:
109
+ jobs:
110
+ - build:
111
+ <<: *version_tags_only
112
+ - gem/build:
113
+ <<: *version_tags_only
114
+ executor: ruby-2-6
115
+ name: gem-build
116
+ requires:
117
+ - build
118
+ - gem/publish:
119
+ <<: *version_tags_only
120
+ name: gem-publish
121
+ to_rubygems: true
122
+ pre_release: false
123
+ requires:
124
+ - gem-build
125
+ context: artifact_publishing
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .rspec_status
2
+ /.ruby-version
3
+ /*.gem
4
+ /.bundle/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/bundle/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.0.3]
8
+ * Misc minimal fixes to get the gem building and releasable
9
+ [#2](https://github.com/doximity/rake-ui/pull/3)
10
+ * Copy over library code from prior sources with maintained history
11
+ [#1](https://github.com/doximity/simplekiq/pull/1)
12
+
13
+ ## [0.0.2]
14
+ * Scrubbed version from rubygems, do not use
15
+ ## [0.0.1]
16
+ * Scrubbed version from rubygems, do not use
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,31 @@
1
+ # Contributing
2
+
3
+ We welcome contributions to this repository. Feel free to submit issues for bugs you encounter and pull requests for code and documentation contributions.
4
+
5
+ In order to prevent licensing issues, we require all contributors to sign an individual contributor license agreement, which is reproduced below:
6
+
7
+ ## Individual Contributor License Agreement
8
+
9
+ In order to clarify the intellectual property license granted with Contributions from any person or entity, Doximity Inc. ("Doximity") must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Doximity; it does not change your rights to use your own Contributions for any other purpose.
10
+
11
+ You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Doximity. Except for the license granted herein to Doximity and recipients of software distributed by Doximity, You reserve all right, title, and interest in and to Your Contributions.
12
+
13
+ ### Definitions
14
+
15
+ "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Doximity. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
16
+
17
+ 1. "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Doximity for inclusion in, or documentation of, any of the products owned or managed by Doximity (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Doximity or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Doximity for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
18
+
19
+ 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Doximity and to recipients of software distributed by Doximity a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
20
+
21
+ 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Doximity and to recipients of software distributed by Doximity a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
22
+
23
+ 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Doximity, or that your employer has executed a separate Corporate CLA with Doximity.
24
+
25
+ 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
26
+
27
+ 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
28
+
29
+ 7. Should You wish to submit work that is not Your original creation, You may submit it to Doximity separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [[]named here]".
30
+
31
+ 8. You agree to notify Doximity of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
data/CONTRIBUTORS.md ADDED
@@ -0,0 +1,22 @@
1
+ ## List of All Known Code Contributors to Simplekiq
2
+
3
+ ### Jack Noble
4
+ * Collaborated on initial concept
5
+ * Wrote the majority of the code as of initial release
6
+
7
+ ### John Wilkinson
8
+ * Collaborated on initial concept
9
+ * Conducted the gem extraction and release
10
+
11
+ ### Brian Dillard
12
+ * Added additional comment documentation
13
+ * Added support for `on_complete` batch callback support in `Simplekiq::BatchingJob`
14
+
15
+ ### Austin Madden
16
+ * Fixed bug with batch statuses in callbacks for empty batches
17
+
18
+ ### Tiffany Troha
19
+ * Added support for specifying `sidekiq_options` for the child job in `Simplekiq::BatchingJob`
20
+
21
+ ### [Daniel Pepper](https://github.com/dpep)
22
+ * On request, graciously took down his unused `simplekiq` placeholder from rubygems so we could continue using the name :raised_hands:
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in simplekiq.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,81 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simplekiq (0.0.3)
5
+ sidekiq (~> 5.2.9)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.2)
11
+ coderay (1.1.3)
12
+ connection_pool (2.2.5)
13
+ diff-lcs (1.5.0)
14
+ method_source (1.0.0)
15
+ parallel (1.22.1)
16
+ parser (3.1.1.0)
17
+ ast (~> 2.4.1)
18
+ pry (0.13.1)
19
+ coderay (~> 1.1)
20
+ method_source (~> 1.0)
21
+ rack (2.2.3)
22
+ rack-protection (2.2.0)
23
+ rack
24
+ rainbow (3.1.1)
25
+ rake (12.3.3)
26
+ redis (4.5.1)
27
+ regexp_parser (2.2.1)
28
+ rexml (3.2.5)
29
+ rspec (3.10.0)
30
+ rspec-core (~> 3.10.0)
31
+ rspec-expectations (~> 3.10.0)
32
+ rspec-mocks (~> 3.10.0)
33
+ rspec-core (3.10.1)
34
+ rspec-support (~> 3.10.0)
35
+ rspec-expectations (3.10.1)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.10.0)
38
+ rspec-mocks (3.10.2)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.10.0)
41
+ rspec-support (3.10.3)
42
+ rspec_junit_formatter (0.5.1)
43
+ rspec-core (>= 2, < 4, != 2.12.0)
44
+ rubocop (1.26.1)
45
+ parallel (~> 1.10)
46
+ parser (>= 3.1.0.0)
47
+ rainbow (>= 2.2.2, < 4.0)
48
+ regexp_parser (>= 1.8, < 3.0)
49
+ rexml
50
+ rubocop-ast (>= 1.16.0, < 2.0)
51
+ ruby-progressbar (~> 1.7)
52
+ unicode-display_width (>= 1.4.0, < 3.0)
53
+ rubocop-ast (1.16.0)
54
+ parser (>= 3.1.1.0)
55
+ rubocop-performance (1.13.3)
56
+ rubocop (>= 1.7.0, < 2.0)
57
+ rubocop-ast (>= 0.4.0)
58
+ ruby-progressbar (1.11.0)
59
+ sidekiq (5.2.10)
60
+ connection_pool (~> 2.2, >= 2.2.2)
61
+ rack (~> 2.0)
62
+ rack-protection (>= 1.5.0)
63
+ redis (~> 4.5, < 4.6.0)
64
+ standard (1.9.1)
65
+ rubocop (= 1.26.1)
66
+ rubocop-performance (= 1.13.3)
67
+ unicode-display_width (2.1.0)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ pry
74
+ rake (~> 12.0)
75
+ rspec (~> 3.2)
76
+ rspec_junit_formatter
77
+ simplekiq!
78
+ standard
79
+
80
+ BUNDLED WITH
81
+ 2.1.4
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2022 Doximity, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Simplekiq
2
+
3
+ Any time that you find yourself needing to string together a long chain of jobs, particularly when there are multiple stages of Sidekiq-pro batches and callbacks involved, come home instead to the simple flavor of orchestrated job flow with Simplekiq.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "simplekiq"
11
+ ```
12
+
13
+ Note that this gem requires you be a Sidekiq Pro paid subscriber to be able to use it, so after following the installation docs for getting the private gem configured with your system, ensure you have `sidekiq-pro` at version `~> 5.0.0` or higher and that it's being required:
14
+
15
+ ```ruby
16
+ gem "sidekiq-pro", "~> 5.0.0"
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install simplekiq
26
+
27
+ ## Usage
28
+
29
+ There are currently two primary components of the system which were designed to work in harmony:
30
+
31
+ * [Simplekiq::OrchestrationJob](./lib/simplekiq/orchestration_job.rb) - A mixin for a Sidekiq jobs to be able to orchestrate a flow of jobs in one place. It makes long complicated flows between jobs easier to understand, iterate on, and test. It eliminates the need to hop between dozens of files to determine when, where, and why a particular job gets called.
32
+ * [Simplekiq::BatchingJob](./lib/simplekiq/batching_job.rb) - A mixin designed to make breaking a large job into a batched process dead simple and contained within a single class while still being trivially composable in orchestrations.
33
+
34
+ ## Tool Drilldown
35
+
36
+ ### Simplekiq::OrchestrationJob
37
+
38
+ Mixing in the [Simplekiq::Orchestration](./lib/simplekiq/orchestration_job.rb) module lets you define a human-readable workflow of jobs in a single file with almost* no special requirements or restrictions on how the child jobs are designed. In most cases, Sidekiq jobs not designed for use in orchestrations should be compatible for use in orchestrations. A job implementing `OrchestrationJob` might look like:
39
+
40
+ ```ruby
41
+ class SomeOrchestrationJob < BaseJob
42
+ include Sidekiq::Worker
43
+ include Simplekiq::OrchestrationJob
44
+
45
+ def perform_orchestration(some_id)
46
+ @some_model = SomeModel.find(some_id) # 1.
47
+
48
+ run SomeInitialSetupJob, some_model.id # 2.
49
+
50
+ in_parallel do
51
+ some_related_models.each do |related_model|
52
+ run SomeParallelizableJob, related_model.id # 3.
53
+ end
54
+ end
55
+
56
+ run SomeFinalizationJob, some_model.id # 4.
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :some_model
62
+
63
+ def some_related_models
64
+ @some_related_models ||= some_model.some_relation
65
+ end
66
+ end
67
+ ```
68
+
69
+ Let's use the above example to describe some specifics of how the flow works.
70
+
71
+ 1. `SomeOrchestrationJob` pulls up some instance of parent model `SomeModel`.
72
+ 2. It does some initial work in `SomeInitialSetupJob`, which blocks the rest of the workflow until it completes successfully.
73
+ 3. Then it will run a `SomeParallelizableJob` for each of some number of associated models `some_related_models`. These jobs will all run parallel to each other independently.
74
+ 4. Finally, after all of the parallel jobs from #3 complete successfully, `SomeFinalizationJob` will run and then after it finishes the orchestration will be complete.
75
+
76
+ **Note** - it's fine to add utility methods and `attr_accessor`s to keep the code tidy and maintainable.
77
+
78
+ When `SomeOrchestrationJob` itself gets called though, the first thing it does it turn these directives into a big serialized structure indicating which job will be called under what conditions (eg, serial or in parallel) and with what arguments, and then keeps passing that between the simplekiq-internal jobs that actually conduct the flow.
79
+
80
+ This means when you want to deploy a change to this flow all previous in-flight workflows will continue undisturbed because the workflow is frozen in sidekiq job arguments and will remain frozen until the workflow completes. This is generally a boon, but note that if you remove a job from a workflow you'll need to remember to either keep the job itself (eg, the `SomeFinalizationJob` class file from our above example) in the codebase or replace it with a stub so that any in-flight workflows won't crash due to not being able to pull up the prior-specified workflow.
81
+
82
+ "almost* no special requirements or restrictions on how the child jobs are designed" - The one thing you'll want to keep in mind when feeding arbitrary jobs into orchestrations is that if the job creates any new sidekiq batches then those new sidekiq batches should be added as child sidekiq batches of the parent sidekiq batch of the job. The parent sidekiq batch of the job is the sidekiq batch that drives the orchestration from step to step, so if you don't do this it will move onto the next step in the orchestration once your job finishes even if the new sidekiq batches it started didn't finish. This sounds more complicated than it is, you can see an example of code that does this in [`BatchingJob#perform`](./lib/simplekiq/batching_job.rb):
83
+
84
+ ```ruby
85
+ if batch # is there a parent batch?
86
+ batch.jobs do # open the parent batch back up
87
+ create_a_new_batch_and_add_jobs_to_it_to_run # make our new batch as a child batch of the parent batch
88
+ end # close the parent batch again
89
+ else # there's no parent batches, this job was run directly outside of an orchestration
90
+ create_a_new_batch_and_add_jobs_to_it_to_run # make our new batch without a parent batch
91
+ end
92
+ ```
93
+
94
+ ### Simplekiq::BatchingJob
95
+
96
+ See the [Simplekiq::BatchingJob](./lib/simplekiq/batching_job.rb) module itself for a description and example usage in the header comments. Nutshell is that you should use this if you're planning on making a batched asynchronous process as it shaves off a lot of ceremony and unexpressive structure. eg - Instead of having `BeerBottlerJob` which queues some number of `BeerBottlerBatchJob`s to handle the broken down sub-tasks you can just have `BeerBottlerJob` with a method for batching, executing individual batches, and a callback that gets run after all batches have completed successfully.
97
+
98
+ ## History
99
+
100
+ Simplekiq was initially released for private use within Doximity applications in Oct 2020 where it continued to be iterated on towards stability and general use until Jan 2022 when it was deemed settled enough for public release.
101
+
102
+ The primary driving factor that inspired this work was a series of over a dozen differently defined and structured jobs part of a single workflow of which the logical flow was extraordinarily difficult to cognitively trace. This led to exteme difficulty in debugging and following problematic instances of the workflow in production as well as needlessly high cost to refactoring and iterative adjustments.
103
+
104
+ The crux of the problem was that each job was highly coupled to its position in the overall flow as well as the absence of any central mechanism to indicate what the overall flow was. After building Simplekiq and implementing it into the flow, significant changes to the flow became quick adjustments requiring only a couple lines of code to change and folks unfamiliar with the system could quickly get up to speed by reading through the orchestration job.
105
+
106
+ ## Versioning
107
+
108
+ This project follows semantic versioning. At time of writing it is sitting at 0.0.1 until its integration with the application it was extracted from is confirmed to be stable. Once confirmed it will be started off at 1.0.0 as it has otherwise been used in a production system already for some time.
109
+
110
+ ## Development
111
+
112
+ After checking out the repo, run `bin/setup` to install dependencies. Note that this depends on `sidekiq-pro` which requires a [commercial license](https://sidekiq.org/products/pro.html) to install and use.
113
+
114
+ Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
115
+
116
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
117
+
118
+ TODO: Update this section with more specific/appropriate instructions once this is a public repository.
119
+
120
+ ## Contributing
121
+
122
+ 1. Fork it
123
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
124
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
125
+ 4. Push to the branch (`git push origin my-new-feature`)
126
+ 5. Create a new Pull Request
127
+ 6. Sign the CLA if you haven't yet. See CONTRIBUTING.md
128
+
129
+ ## License
130
+
131
+ The gem is licensed under an Apache 2 license. Contributors are required to sign an contributor license agreement. See LICENSE.txt and CONTRIBUTING.md for more information.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core/rake_task"
4
+
5
+ FileList["tasks/*.rake"].each { |task| load task }
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "simplekiq"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module enables you to break up your work into batches and run those
4
+ # batches as background jobs while keeping all your code in the same file.
5
+ # Including this module *implements perform* you should not override it.
6
+ # It expects you to implement two methods: #perform_batching and
7
+ # #perform_batch.
8
+ #
9
+ # Optionally you may also implement any combination of Sidekiq::Batch
10
+ # callbacks.
11
+ # - #on_complete
12
+ # - #on_success
13
+ # - #on_death
14
+ #
15
+ # #perform_batching should contain your code for breaking up your work into
16
+ # smaller jobs. It handles all the Sidekiq::Batch boilerplate for you. Where
17
+ # you would normally call ExampleBatchJob.perform_async you should use
18
+ # #queue_batch. If you'd like to custommize the sidekiq batch object, you can
19
+ # access it in perform_batching through the `sidekiq_batch` method.
20
+ #
21
+ # #perform_batch should contain the code that would be in your batch job. Under
22
+ # the hood, #queue_batch queues a job which will run #perform_batch.
23
+ #
24
+ # [Sidekiq::Batch documentation](https://github.com/mperham/sidekiq/wiki/Batches)
25
+ # explains batches, their lifecycle, callbacks, etc.
26
+ #
27
+ # class ExampleJob
28
+ # include Simplekiq::BatchingJob
29
+ #
30
+ # def perform_batching(some_id)
31
+ # sidekiq_batch.description = "My custom batch description" # optional
32
+ #
33
+ # Record.find(some_id).other_records.in_batches do |other_records|
34
+ # queue_batch(other_records.ids)
35
+ # end
36
+ # end
37
+ #
38
+ # def perform_batch(other_record_ids)
39
+ # OtherRecord.where(id: other_record_ids).do_work
40
+ # end
41
+ #
42
+ # def on_death(_status, options)
43
+ # same_id_as_before = options["args"].first
44
+ # Record.find(same_id_as_before).death!
45
+ # end
46
+
47
+ # def on_complete(_status, options)
48
+ # same_id_as_before = options["args"].first
49
+ # Record.find(same_id_as_before).complete!
50
+ # end
51
+
52
+ # def on_success(_status, options)
53
+ # same_id_as_before = options["args"].first
54
+ # Record.find(same_id_as_before).success!
55
+ # end
56
+ # end
57
+ #
58
+ # ExampleJob.perform_async(some_id)
59
+ #
60
+ # Come home to the impossible flavor of batch creation
61
+
62
+ module Simplekiq
63
+ module BatchingJob
64
+ include Sidekiq::Worker
65
+
66
+ BATCH_CLASS_NAME = "SimplekiqBatch"
67
+
68
+ class << self
69
+ def included(klass)
70
+ batch_job_class = Class.new(BaseBatch)
71
+ klass.const_set(BATCH_CLASS_NAME, batch_job_class)
72
+
73
+ klass.extend ClassMethods
74
+ end
75
+ end
76
+
77
+ module ClassMethods
78
+ def batch_sidekiq_options(options)
79
+ batch_class = const_get(BATCH_CLASS_NAME)
80
+ batch_class.instance_eval do
81
+ sidekiq_options(options)
82
+ end
83
+ end
84
+ end
85
+
86
+ def perform(*args)
87
+ self.batches = []
88
+
89
+ perform_batching(*args)
90
+
91
+ # If we're part of an existing sidekiq batch make this a child batch
92
+ # This is necessary for it work with orchestration; we could add an option
93
+ # to toggle the behavior on and off.
94
+ if batch
95
+ batch.jobs do
96
+ handle_batches(args)
97
+ end
98
+ else
99
+ handle_batches(args)
100
+ end
101
+ end
102
+
103
+ protected # TODO: should this be private?
104
+
105
+ attr_accessor :batches
106
+
107
+ def handle_batches(args)
108
+ if !batches.empty?
109
+ flush_batches(args)
110
+ else
111
+ # Empty batches with no jobs will never invoke callbacks, so handle
112
+ # that case by immediately manually invoking :complete & :success.
113
+ on_complete(nil, {"args" => args}) if respond_to?(:on_complete)
114
+ on_success(nil, {"args" => args}) if respond_to?(:on_success)
115
+ end
116
+ end
117
+
118
+ def flush_batches(args)
119
+ batch_job_class = self.class.const_get(BATCH_CLASS_NAME)
120
+ sidekiq_batch.description ||= "Simplekiq Batch Jobs for #{self.class.name}, args: #{args}"
121
+
122
+ sidekiq_batch.on("death", self.class, "args" => args) if respond_to?(:on_death)
123
+ sidekiq_batch.on("complete", self.class, "args" => args) if respond_to?(:on_complete)
124
+ sidekiq_batch.on("success", self.class, "args" => args) if respond_to?(:on_success)
125
+
126
+ sidekiq_batch.jobs do
127
+ batches.each do |job_args|
128
+ batch_job_class.perform_async(*job_args)
129
+ end
130
+ end
131
+ end
132
+
133
+ def queue_batch(*args)
134
+ batches << args
135
+ end
136
+
137
+ def batch_description=(description)
138
+ sidekiq_batch.description = description
139
+ end
140
+
141
+ private
142
+
143
+ def sidekiq_batch
144
+ @sidekiq_batch ||= Sidekiq::Batch.new
145
+ end
146
+ end
147
+
148
+ class BaseBatch
149
+ include Sidekiq::Worker
150
+
151
+ def perform(*args)
152
+ module_parent_of_class.new.perform_batch(*args)
153
+ end
154
+
155
+ private
156
+
157
+ def module_parent_of_class
158
+ # Borrowed from https://apidock.com/rails/Module/module_parent_name
159
+ parent_name = self.class.name =~ /::[^:]+\Z/ ? $`.freeze : nil
160
+ parent_name ? Object.const_get(parent_name) : Object
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Simplekiq
4
+ class Orchestration
5
+ attr_accessor :serial_workflow, :parallel_workflow
6
+ def initialize
7
+ @serial_workflow = []
8
+ end
9
+
10
+ def run(*step)
11
+ workflow = parallel_workflow || serial_workflow
12
+ workflow << step
13
+ end
14
+
15
+ def in_parallel
16
+ @parallel_workflow = []
17
+ yield
18
+ serial_workflow << @parallel_workflow if @parallel_workflow.any?
19
+ ensure
20
+ @parallel_workflow = nil
21
+ serial_workflow
22
+ end
23
+
24
+ def execute(parent_batch)
25
+ OrchestrationExecutor.execute(workflow: serialized_workflow, parent_batch: parent_batch)
26
+ end
27
+
28
+ def serialized_workflow
29
+ @serialized_workflow ||= serial_workflow.map do |step|
30
+ case step[0]
31
+ when Array
32
+ step.map do |(job, *args)|
33
+ {"klass" => job.name, "args" => args}
34
+ end
35
+ when Class
36
+ job, *args = step
37
+ {"klass" => job.name, "args" => args}
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Simplekiq
4
+ class OrchestrationExecutor
5
+ def self.execute(workflow:, parent_batch:)
6
+ new.run_step(parent_batch, workflow, 0)
7
+ end
8
+
9
+ def run_step(parent_batch, workflow, step)
10
+ return if workflow.empty?
11
+
12
+ nest_under(parent_batch) do
13
+ *jobs = workflow.at(step)
14
+ sidekiq_batch = Sidekiq::Batch.new
15
+ sidekiq_batch.on(
16
+ :success,
17
+ self.class,
18
+ "orchestration_workflow" => workflow, "step" => step + 1
19
+ )
20
+
21
+ sidekiq_batch.jobs do
22
+ jobs.each do |job|
23
+ Object.const_get(job["klass"]).perform_async(*job["args"])
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def nest_under(parent_batch)
30
+ if parent_batch
31
+ parent_batch.jobs do
32
+ yield
33
+ end
34
+ else
35
+ yield
36
+ end
37
+ end
38
+
39
+ def on_success(status, options)
40
+ return if options["step"] == options["orchestration_workflow"].length
41
+
42
+ parent_batch = Sidekiq::Batch.new(status.parent_bid)
43
+ run_step(parent_batch, options["orchestration_workflow"], options["step"])
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Simplekiq
6
+ module OrchestrationJob
7
+ include Sidekiq::Worker
8
+
9
+ extend Forwardable
10
+ def_delegators :orchestration, :run, :in_parallel
11
+
12
+ def perform(*args)
13
+ perform_orchestration(*args)
14
+ orchestration.execute(batch)
15
+ end
16
+
17
+ def workflow_plan(*args)
18
+ perform_orchestration(*args)
19
+ orchestration.serialized_workflow
20
+ end
21
+
22
+ private
23
+
24
+ def orchestration
25
+ @orchestration ||= Orchestration.new
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Simplekiq
2
+ VERSION = "0.0.3"
3
+ end
data/lib/simplekiq.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ # NB: You must explicitly require sidekiq-ent in your app!
6
+ # require "sidekiq-ent"
7
+
8
+ require "simplekiq/orchestration_executor"
9
+ require "simplekiq/orchestration"
10
+ require "simplekiq/orchestration_job"
11
+ require "simplekiq/batching_job"
12
+
13
+ module Simplekiq
14
+ end
data/simplekiq.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ require_relative "lib/simplekiq/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "simplekiq"
5
+ spec.version = Simplekiq::VERSION
6
+ spec.authors = ["Jack Noble", "John Wilkinson"]
7
+ spec.email = ["jcwilkinson@doximity.com"]
8
+ spec.summary = "Sidekiq-based workflow orchestration library"
9
+ spec.description = "Provides tools for representing long chains of parallel and serial jobs in a flat, simple way."
10
+ spec.homepage = "https://github.com/doximity/simplekiq"
11
+ spec.license = "APACHE-2.0"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = spec.homepage
16
+ # TODO: spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.executables = []
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_development_dependency "rspec", "~> 3.2"
27
+ spec.add_development_dependency "rake", "~> 12.0"
28
+ spec.add_development_dependency "rspec_junit_formatter"
29
+ spec.add_development_dependency "pry"
30
+ spec.add_development_dependency "standard"
31
+
32
+ spec.add_dependency "sidekiq", "~> 5.2.9"
33
+
34
+ # Can't define this explicitly because it would be inappropriate to vendor this
35
+ # pay-to-use library into the gem and it is not available on rubygems and
36
+ # this otherwise interferes with our publishing process.
37
+ # spec.add_dependency "sidekiq-pro", "~> 5.0.0"
38
+ end
data/tasks/ci.rake ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ci do
4
+ desc "Run specs"
5
+ task :specs do
6
+ reports = "tmp/test-results/rspec"
7
+ sh "mkdir -p #{reports}"
8
+ sh "bundle exec rspec ./spec " \
9
+ "--format progress "\
10
+ "--format RspecJunitFormatter " \
11
+ "-o #{reports}/results.xml"
12
+ end
13
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simplekiq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Jack Noble
8
+ - John Wilkinson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-04-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.2'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '12.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '12.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec_junit_formatter
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: pry
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: standard
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: sidekiq
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 5.2.9
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: 5.2.9
98
+ description: Provides tools for representing long chains of parallel and serial jobs
99
+ in a flat, simple way.
100
+ email:
101
+ - jcwilkinson@doximity.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".circleci/config.yml"
107
+ - ".gitignore"
108
+ - ".rspec"
109
+ - CHANGELOG.md
110
+ - CONTRIBUTING.md
111
+ - CONTRIBUTORS.md
112
+ - Gemfile
113
+ - Gemfile.lock
114
+ - LICENSE.txt
115
+ - README.md
116
+ - Rakefile
117
+ - bin/console
118
+ - bin/setup
119
+ - lib/simplekiq.rb
120
+ - lib/simplekiq/batching_job.rb
121
+ - lib/simplekiq/orchestration.rb
122
+ - lib/simplekiq/orchestration_executor.rb
123
+ - lib/simplekiq/orchestration_job.rb
124
+ - lib/simplekiq/version.rb
125
+ - simplekiq.gemspec
126
+ - tasks/ci.rake
127
+ - vendor/cache/ast-2.4.2.gem
128
+ - vendor/cache/coderay-1.1.3.gem
129
+ - vendor/cache/connection_pool-2.2.5.gem
130
+ - vendor/cache/diff-lcs-1.5.0.gem
131
+ - vendor/cache/method_source-1.0.0.gem
132
+ - vendor/cache/parallel-1.22.1.gem
133
+ - vendor/cache/parser-3.1.1.0.gem
134
+ - vendor/cache/pry-0.13.1.gem
135
+ - vendor/cache/rack-2.2.3.gem
136
+ - vendor/cache/rack-protection-2.2.0.gem
137
+ - vendor/cache/rainbow-3.1.1.gem
138
+ - vendor/cache/rake-12.3.3.gem
139
+ - vendor/cache/redis-4.5.1.gem
140
+ - vendor/cache/regexp_parser-2.2.1.gem
141
+ - vendor/cache/rexml-3.2.5.gem
142
+ - vendor/cache/rspec-3.10.0.gem
143
+ - vendor/cache/rspec-core-3.10.1.gem
144
+ - vendor/cache/rspec-expectations-3.10.1.gem
145
+ - vendor/cache/rspec-mocks-3.10.2.gem
146
+ - vendor/cache/rspec-support-3.10.3.gem
147
+ - vendor/cache/rspec_junit_formatter-0.5.1.gem
148
+ - vendor/cache/rubocop-1.26.1.gem
149
+ - vendor/cache/rubocop-ast-1.16.0.gem
150
+ - vendor/cache/rubocop-performance-1.13.3.gem
151
+ - vendor/cache/ruby-progressbar-1.11.0.gem
152
+ - vendor/cache/sidekiq-5.2.10.gem
153
+ - vendor/cache/standard-1.9.1.gem
154
+ - vendor/cache/unicode-display_width-2.1.0.gem
155
+ homepage: https://github.com/doximity/simplekiq
156
+ licenses:
157
+ - APACHE-2.0
158
+ metadata:
159
+ homepage_uri: https://github.com/doximity/simplekiq
160
+ source_code_uri: https://github.com/doximity/simplekiq
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: 2.5.0
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubygems_version: 3.3.11
177
+ signing_key:
178
+ specification_version: 4
179
+ summary: Sidekiq-based workflow orchestration library
180
+ test_files: []