dipa 0.1.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +126 -0
- data/Rakefile +10 -0
- data/app/jobs/dipa/application_job.rb +6 -0
- data/app/jobs/dipa/service_job.rb +9 -0
- data/app/models/concerns/models/dipa/dumpable.rb +16 -0
- data/app/models/concerns/models/dipa/loadable.rb +17 -0
- data/app/models/concerns/models/dipa/state_attribute_handling.rb +38 -0
- data/app/models/dipa/agent.rb +67 -0
- data/app/models/dipa/application_record.rb +7 -0
- data/app/models/dipa/coordinator.rb +81 -0
- data/app/models/modules/models/dipa/status_constants.rb +24 -0
- data/app/services/dipa/agent_services/coordinator_state_service.rb +14 -0
- data/app/services/dipa/agent_services/post_processing_service.rb +16 -0
- data/app/services/dipa/agent_services/processing_service.rb +13 -0
- data/app/services/dipa/agent_services/start_processing_service.rb +18 -0
- data/app/services/dipa/application_service.rb +9 -0
- data/app/services/dipa/coordinator_services/create_agents_service.rb +23 -0
- data/app/services/dipa/coordinator_services/start_processing_service.rb +18 -0
- data/app/validators/dipa/date_validator.rb +26 -0
- data/db/migrate/20220102132652_create_dipa_coordinators.rb +20 -0
- data/db/migrate/20220106183616_create_dipa_agents.rb +19 -0
- data/lib/dipa/engine.rb +49 -0
- data/lib/dipa/errors.rb +12 -0
- data/lib/dipa/processor/base.rb +141 -0
- data/lib/dipa/processor/each.rb +11 -0
- data/lib/dipa/processor/map.rb +8 -0
- data/lib/dipa/version.rb +5 -0
- data/lib/dipa.rb +37 -0
- data/lib/tasks/auto_annotate_models.rake +62 -0
- data/lib/tasks/dipa_tasks.rake +5 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6403004fa28c6c0ae9f9cc40f9da8f5b812ed5ea4feb94bd00210785dd4bb950
|
4
|
+
data.tar.gz: 5d485e3096f6b835520844bf6564354eb07640159f4fc7579566df78c8c7c999
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4903b379f5772adf30f2791c74e6355a3a753bb8808a3d8bf655790e7d601e925806d110ec3561993979b3f3aee9a19c5fade0a7d2b17afc7d856e3b5709fecf
|
7
|
+
data.tar.gz: 4022abee161ffd77df8a8a11f9712285689287b369d994334c0bc82ca0f6615f087d1f9aeafa184536b09e399a74921e26d15e670274e98ae836d7aff033c1fd
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Merten Falk
|
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/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
![status-badge](https://ci.codeberg.org/api/badges/empunkt/dipa/status.svg)
|
2
|
+
|
3
|
+
# Dipa
|
4
|
+
|
5
|
+
This gem provides an API for parallel processing like the [parallel
|
6
|
+
gem](https://github.com/grosser/parallel) but distributed and scalable over
|
7
|
+
different machines. All this with minimum configuration and minimum dependencies
|
8
|
+
to specific technologies and using the rails ecosystem.
|
9
|
+
|
10
|
+
Dipa provides a rails engine which depends on
|
11
|
+
[ActiveJob](https://guides.rubyonrails.org/active_job_basics.html) and
|
12
|
+
[ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html).
|
13
|
+
You can use whatever backend you like for any of this components and configure
|
14
|
+
them for your specific usecase.
|
15
|
+
|
16
|
+
The purpose of this gem is to distribute load heavy and long running processing
|
17
|
+
of large datasets over multiple processes or machines using
|
18
|
+
[ActiveJob](https://guides.rubyonrails.org/active_job_basics.html).
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Before you install Dipa make sure
|
23
|
+
[ActiveJob](https://guides.rubyonrails.org/active_job_basics.html) and
|
24
|
+
[ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) are
|
25
|
+
installed and configured properly.
|
26
|
+
|
27
|
+
Add this line to your application's Gemfile:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
gem 'dipa'
|
31
|
+
```
|
32
|
+
|
33
|
+
And then execute:
|
34
|
+
```bash
|
35
|
+
$ bundle install
|
36
|
+
```
|
37
|
+
|
38
|
+
Or install it yourself as:
|
39
|
+
```bash
|
40
|
+
$ gem install dipa
|
41
|
+
```
|
42
|
+
|
43
|
+
Install Dipa migrations
|
44
|
+
```bash
|
45
|
+
bundle exec rake app:dipa:install:migrations
|
46
|
+
bundle exec rake db:migrate
|
47
|
+
```
|
48
|
+
|
49
|
+
## Configuration
|
50
|
+
|
51
|
+
Dipa can be configured in the application config.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
config.dipa.agent_queue = :default_queue_for_dipa_agent_jobs
|
55
|
+
config.dipa.coordinator_queue = :default_queue_for_coordinator_queue_jobs
|
56
|
+
```
|
57
|
+
|
58
|
+
If not configured `config.active_job.default_queue_name` or `:default` will be
|
59
|
+
used.
|
60
|
+
|
61
|
+
## Usage
|
62
|
+
|
63
|
+
Minimum example:
|
64
|
+
```ruby
|
65
|
+
Dipa.map(1..100).with('Integer', :sqrt)
|
66
|
+
```
|
67
|
+
|
68
|
+
More realistic examples:
|
69
|
+
```ruby
|
70
|
+
Dipa.map(large_dataset, options).with('ProcessorClassName', :processor_class_method)
|
71
|
+
Dipa.each(large_dataset, options).with('ProcessorClassName', :processor_class_method)
|
72
|
+
```
|
73
|
+
|
74
|
+
`Dipa.map` returns an `Array` of the processed items. The result is in the same order as the input (`large_dataset`).
|
75
|
+
|
76
|
+
`Dipa.each` returns `large_dataset.to_a`.
|
77
|
+
|
78
|
+
`large_dataset` must be an `Enumerable`.
|
79
|
+
|
80
|
+
`options` is a hash. Following keys are allowed:
|
81
|
+
|
82
|
+
- `agent_queue:` [Symbol] Defaults to `config.dipa.agent_queue`.
|
83
|
+
- `coordinator_queue:` [Symbol] Defaults to `config.dipa.coordinator_queue`.
|
84
|
+
- `async:` [true|false] Defaults to `false`. Usually no need to change, but
|
85
|
+
could be useful for `Dipa.each` if you have a alternative way to monitor your
|
86
|
+
jobs.
|
87
|
+
- `keep_data:` [true|false] Defaults to `false`. Useful for debugging. After
|
88
|
+
processing all `Dipa::Agent` and `Dipa::Coordinator` records and the
|
89
|
+
associated ActiveStorage data will be removed. If you don't want that to
|
90
|
+
happen, set this to `true`.
|
91
|
+
|
92
|
+
`ProcessorClassName` must be a `Class` or a `String`. Defines the class which
|
93
|
+
provides the processor method.
|
94
|
+
|
95
|
+
`:processor_class_method` must be a `Symbol` or a `String`. Defines the method
|
96
|
+
which is used to process each single element of `large_dataset`. MUST be a class
|
97
|
+
method. MUST except just one element as argument.
|
98
|
+
|
99
|
+
## TODO
|
100
|
+
|
101
|
+
[TODO.md](TODO.md)
|
102
|
+
|
103
|
+
## Development
|
104
|
+
|
105
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
106
|
+
`bundle exec rspec` to run the tests. You can also run `bin/console` for an
|
107
|
+
interactive prompt that will allow you to experiment.
|
108
|
+
|
109
|
+
## Contributing
|
110
|
+
|
111
|
+
Bug reports and pull requests are welcome on Codeberg at
|
112
|
+
https://codeberg.org/empunkt/dipa. This project is intended to be a safe,
|
113
|
+
welcoming space for collaboration, and contributors are expected to adhere to
|
114
|
+
the [code of
|
115
|
+
conduct](https://codeberg.org/empunkt/dipa/src/branch/main/CODE_OF_CONDUCT.md).
|
116
|
+
|
117
|
+
## License
|
118
|
+
|
119
|
+
The gem is available as open source under the terms of the
|
120
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
121
|
+
|
122
|
+
## Code of Conduct
|
123
|
+
|
124
|
+
Everyone interacting in the Dipa project's codebases, issue trackers, chat rooms
|
125
|
+
and mailing lists is expected to follow the
|
126
|
+
[code of conduct](https://codeberg.org/empunkt/dipa/src/branch/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Models
|
4
|
+
module Dipa
|
5
|
+
module Dumpable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def dump_to_file(data:, attacher:)
|
9
|
+
io = StringIO.new(Marshal.dump(data), 'rb')
|
10
|
+
filename = "#{attacher}.dat"
|
11
|
+
|
12
|
+
public_send(attacher).attach(io: io, filename: filename)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Models
|
4
|
+
module Dipa
|
5
|
+
module Loadable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def load_from_file(attacher:)
|
9
|
+
return unless public_send(attacher).attached?
|
10
|
+
|
11
|
+
Marshal.load( # rubocop:disable Security/MarshalLoad
|
12
|
+
public_send(attacher).download
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Models
|
4
|
+
module Dipa
|
5
|
+
module StateAttributeHandling
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
include Modules::Models::Dipa::StatusConstants
|
9
|
+
|
10
|
+
included do
|
11
|
+
attribute :state, :string, default: INITIALIZED_STATE
|
12
|
+
|
13
|
+
validates :state, presence: true, inclusion: { in: STATES }
|
14
|
+
if Rails.version >= '7'
|
15
|
+
validates :state, comparison: { equal_to: INITIALIZED_STATE },
|
16
|
+
on: :create
|
17
|
+
else
|
18
|
+
validates :state, inclusion: { in: [INITIALIZED_STATE] },
|
19
|
+
on: :create
|
20
|
+
end
|
21
|
+
|
22
|
+
STATES.each do |state_value|
|
23
|
+
define_method("#{state_value}?".to_sym) do
|
24
|
+
state == state_value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def started!
|
30
|
+
update!(started_at: Time.zone.now, state: PROCESSING_STATE)
|
31
|
+
end
|
32
|
+
|
33
|
+
def finished!
|
34
|
+
update!(finished_at: Time.zone.now, state: PROCESSED_STATE)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: dipa_agents
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# finished_at :datetime
|
9
|
+
# index :integer not null
|
10
|
+
# started_at :datetime
|
11
|
+
# state :string(255) not null
|
12
|
+
# created_at :datetime not null
|
13
|
+
# updated_at :datetime not null
|
14
|
+
# dipa_coordinator_id :bigint not null
|
15
|
+
#
|
16
|
+
# Indexes
|
17
|
+
#
|
18
|
+
# index_dipa_agents_on_dipa_coordinator_id (dipa_coordinator_id)
|
19
|
+
#
|
20
|
+
# Foreign Keys
|
21
|
+
#
|
22
|
+
# fk_rails_... (dipa_coordinator_id => dipa_coordinators.id)
|
23
|
+
#
|
24
|
+
|
25
|
+
module Dipa
|
26
|
+
class Agent < ApplicationRecord
|
27
|
+
include Models::Dipa::Dumpable
|
28
|
+
include Models::Dipa::Loadable
|
29
|
+
include Models::Dipa::StateAttributeHandling
|
30
|
+
|
31
|
+
# validation and default for `state` attribute is included by
|
32
|
+
# Models::Dipa::StateAttributeHandling
|
33
|
+
|
34
|
+
validates :index, numericality: { only_integer: true }
|
35
|
+
|
36
|
+
validates :started_at, 'dipa/date' => true, allow_nil: true
|
37
|
+
validates :finished_at, 'dipa/date' => true, allow_nil: true
|
38
|
+
|
39
|
+
belongs_to :coordinator, inverse_of: :agents,
|
40
|
+
foreign_key: :dipa_coordinator_id
|
41
|
+
|
42
|
+
has_one_attached :source_dump
|
43
|
+
has_one_attached :result_dump
|
44
|
+
|
45
|
+
def result
|
46
|
+
return unless processed?
|
47
|
+
|
48
|
+
load_from_file(attacher: :result_dump)
|
49
|
+
end
|
50
|
+
|
51
|
+
def source
|
52
|
+
load_from_file(attacher: :source_dump)
|
53
|
+
end
|
54
|
+
|
55
|
+
def process!
|
56
|
+
processor_result = coordinator.processor_class_name.constantize
|
57
|
+
.public_send(
|
58
|
+
coordinator.processor_method_name,
|
59
|
+
source
|
60
|
+
)
|
61
|
+
|
62
|
+
dump_to_file(data: processor_result, attacher: :result_dump)
|
63
|
+
|
64
|
+
finished!
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: dipa_coordinators
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# agent_queue :string(255) not null
|
9
|
+
# coordinator_queue :string(255) not null
|
10
|
+
# finished_at :datetime
|
11
|
+
# keep_data :boolean default(FALSE), not null
|
12
|
+
# processor_class_name :string(255) not null
|
13
|
+
# processor_method_name :string(255) not null
|
14
|
+
# size :integer not null
|
15
|
+
# started_at :datetime
|
16
|
+
# state :string(255) not null
|
17
|
+
# want_result :boolean default(TRUE), not null
|
18
|
+
# created_at :datetime not null
|
19
|
+
# updated_at :datetime not null
|
20
|
+
#
|
21
|
+
|
22
|
+
module Dipa
|
23
|
+
class Coordinator < ApplicationRecord
|
24
|
+
include Models::Dipa::Dumpable
|
25
|
+
include Models::Dipa::Loadable
|
26
|
+
include Models::Dipa::StateAttributeHandling
|
27
|
+
|
28
|
+
# validation and default for `state` attribute is included by
|
29
|
+
# Models::Dipa::StateAttributeHandling
|
30
|
+
|
31
|
+
attribute :keep_data, :boolean, default: false
|
32
|
+
attribute :want_result, :boolean, default: true
|
33
|
+
|
34
|
+
validates :agent_queue, presence: true
|
35
|
+
validates :coordinator_queue, presence: true
|
36
|
+
|
37
|
+
validates :size, numericality: { only_integer: true }
|
38
|
+
|
39
|
+
validates :started_at, 'dipa/date' => true, allow_nil: true
|
40
|
+
validates :finished_at, 'dipa/date' => true, allow_nil: true
|
41
|
+
|
42
|
+
validates :processor_class_name, presence: true
|
43
|
+
validates :processor_method_name, presence: true
|
44
|
+
|
45
|
+
validates :keep_data, inclusion: [true, false]
|
46
|
+
validates :want_result, inclusion: [true, false]
|
47
|
+
|
48
|
+
has_many :agents, dependent: :destroy, inverse_of: :coordinator,
|
49
|
+
foreign_key: :dipa_coordinator_id
|
50
|
+
|
51
|
+
has_one_attached :source_dump
|
52
|
+
|
53
|
+
def result
|
54
|
+
return unless processed?
|
55
|
+
|
56
|
+
_result_from_agents
|
57
|
+
end
|
58
|
+
|
59
|
+
def source
|
60
|
+
load_from_file(attacher: :source_dump)
|
61
|
+
end
|
62
|
+
|
63
|
+
def all_agents_created_and_processed?
|
64
|
+
_all_agents_created? && _all_agents_processed?
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def _result_from_agents
|
70
|
+
agents.with_attached_result_dump.order(:index).map(&:result)
|
71
|
+
end
|
72
|
+
|
73
|
+
def _all_agents_processed?
|
74
|
+
agents.all?(&:processed?)
|
75
|
+
end
|
76
|
+
|
77
|
+
def _all_agents_created?
|
78
|
+
agents.length == size
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modules
|
4
|
+
module Models
|
5
|
+
module Dipa
|
6
|
+
module StatusConstants
|
7
|
+
ABORTED_STATE = 'aborted'
|
8
|
+
INITIALIZED_STATE = 'initialized'
|
9
|
+
PROCESSED_STATE = 'processed'
|
10
|
+
PROCESSING_STATE = 'processing'
|
11
|
+
PROCESSING_FAILED_STATE = 'processing_failed'
|
12
|
+
TIMEOUT_STATE = 'timed_out'
|
13
|
+
STATES = [
|
14
|
+
ABORTED_STATE,
|
15
|
+
INITIALIZED_STATE,
|
16
|
+
PROCESSED_STATE,
|
17
|
+
PROCESSING_STATE,
|
18
|
+
PROCESSING_FAILED_STATE,
|
19
|
+
TIMEOUT_STATE
|
20
|
+
].freeze
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
module AgentServices
|
5
|
+
class CoordinatorStateService < ApplicationService
|
6
|
+
def call(agent:)
|
7
|
+
return if agent.coordinator.processed?
|
8
|
+
return unless agent.coordinator.all_agents_created_and_processed?
|
9
|
+
|
10
|
+
agent.coordinator.finished!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
module AgentServices
|
5
|
+
class PostProcessingService < ApplicationService
|
6
|
+
def call(agent:)
|
7
|
+
Dipa::ServiceJob.set(queue_as: agent.coordinator.agent_queue)
|
8
|
+
.perform_later(
|
9
|
+
service_class_name:
|
10
|
+
'Dipa::AgentServices::CoordinatorStateService',
|
11
|
+
kwargs: { agent: agent }
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
module AgentServices
|
5
|
+
class StartProcessingService < ApplicationService
|
6
|
+
def call(agent:)
|
7
|
+
agent.started!
|
8
|
+
|
9
|
+
Dipa::ServiceJob.set(queue_as: agent.coordinator.agent_queue)
|
10
|
+
.perform_later(
|
11
|
+
service_class_name:
|
12
|
+
'Dipa::AgentServices::ProcessingService',
|
13
|
+
kwargs: { agent: agent }
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
module CoordinatorServices
|
5
|
+
class CreateAgentsService < ApplicationService
|
6
|
+
def call(coordinator:)
|
7
|
+
coordinator.source.each_with_index do |item, i|
|
8
|
+
_create_agent(coordinator: coordinator, item: item, index: i)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def _create_agent(coordinator:, item:, index:)
|
15
|
+
agent = coordinator.agents.create!(index: index)
|
16
|
+
|
17
|
+
agent.dump_to_file(data: item, attacher: :source_dump)
|
18
|
+
|
19
|
+
Dipa::AgentServices::StartProcessingService.call(agent: agent)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
module CoordinatorServices
|
5
|
+
class StartProcessingService < ApplicationService
|
6
|
+
def call(coordinator:)
|
7
|
+
coordinator.started!
|
8
|
+
|
9
|
+
ServiceJob.set(queue_as: coordinator.coordinator_queue)
|
10
|
+
.perform_later(
|
11
|
+
service_class_name:
|
12
|
+
'Dipa::CoordinatorServices::CreateAgentsService',
|
13
|
+
kwargs: { coordinator: coordinator }
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
class DateValidator < ActiveModel::EachValidator
|
5
|
+
def validate_each(record, attribute, value)
|
6
|
+
return if self.class.valid?(value: value)
|
7
|
+
|
8
|
+
record.errors.add(
|
9
|
+
attribute,
|
10
|
+
:invalid,
|
11
|
+
message: (options[:message] || 'is not valid Date')
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.valid?(value:)
|
16
|
+
return false if value.blank?
|
17
|
+
|
18
|
+
begin
|
19
|
+
Date.parse(value.to_s)
|
20
|
+
true
|
21
|
+
rescue ArgumentError
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateDipaCoordinators < ActiveRecord::Migration[6.0]
|
4
|
+
def change # rubocop:disable Metrics/MethodLength
|
5
|
+
create_table :dipa_coordinators do |t|
|
6
|
+
t.boolean :keep_data, default: false, null: false
|
7
|
+
t.boolean :want_result, default: true, null: false
|
8
|
+
t.datetime :finished_at
|
9
|
+
t.datetime :started_at
|
10
|
+
t.integer :size, null: false
|
11
|
+
t.string :agent_queue, null: false
|
12
|
+
t.string :coordinator_queue, null: false
|
13
|
+
t.string :processor_class_name, null: false
|
14
|
+
t.string :processor_method_name, null: false
|
15
|
+
t.string :state, null: false
|
16
|
+
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateDipaAgents < ActiveRecord::Migration[6.0]
|
4
|
+
def change # rubocop:disable Metrics/MethodLength
|
5
|
+
create_table :dipa_agents do |t|
|
6
|
+
t.datetime :finished_at
|
7
|
+
t.datetime :started_at
|
8
|
+
t.integer :index, null: false
|
9
|
+
t.string :state, null: false
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_belongs_to(
|
15
|
+
:dipa_agents, :dipa_coordinator,
|
16
|
+
foreign_key: true, index: true, null: false
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
data/lib/dipa/engine.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'rails'
|
5
|
+
require 'active_job/railtie'
|
6
|
+
require 'active_record/railtie'
|
7
|
+
require 'active_storage/engine'
|
8
|
+
|
9
|
+
module Dipa
|
10
|
+
class Engine < ::Rails::Engine
|
11
|
+
isolate_namespace Dipa
|
12
|
+
|
13
|
+
config.eager_load_namespaces << Dipa
|
14
|
+
|
15
|
+
config.generators do |g|
|
16
|
+
g.test_framework :rspec
|
17
|
+
g.api_only = true
|
18
|
+
end
|
19
|
+
|
20
|
+
config.dipa = ActiveSupport::OrderedOptions.new
|
21
|
+
|
22
|
+
initializer 'dipa.queue_names' do
|
23
|
+
config.after_initialize do |app|
|
24
|
+
Dipa.agent_queue = (
|
25
|
+
app.config.dipa.agent_queue ||
|
26
|
+
app.config.active_job.default_queue_name ||
|
27
|
+
:default
|
28
|
+
).to_sym
|
29
|
+
Dipa.coordinator_queue = (
|
30
|
+
app.config.dipa.coordinator_queue ||
|
31
|
+
app.config.active_job.default_queue_name ||
|
32
|
+
:default
|
33
|
+
).to_sym
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
initializer 'dipa.timeouts' do
|
38
|
+
config.after_initialize do |app|
|
39
|
+
Dipa.agent_timeout = (
|
40
|
+
app.config.dipa.agent_timeout || Dipa::DEFAULT_AGENT_TIMEOUT
|
41
|
+
).to_i
|
42
|
+
Dipa.coordinator_timeout = (
|
43
|
+
app.config.dipa.coordinator_timeout ||
|
44
|
+
Dipa::DEFAULT_COORDINATOR_TIMEOUT
|
45
|
+
).to_i
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/dipa/errors.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class AbortedError < Error; end
|
7
|
+
class ProcessingFailedError < Error; end
|
8
|
+
class TimeoutError < Error; end
|
9
|
+
class UnknownProcessingStateError < Error; end
|
10
|
+
class UnknownProcessorClassError < Error; end
|
11
|
+
class UnknownProcessorMethodError < Error; end
|
12
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dipa
|
4
|
+
module Processor
|
5
|
+
class Base
|
6
|
+
SYNC_MODE_WAIT_CYCLE_SECONDS = 2
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
# queue names
|
10
|
+
agent_queue: Dipa.agent_queue,
|
11
|
+
coordinator_queue: Dipa.coordinator_queue,
|
12
|
+
# timeouts
|
13
|
+
agent_timeout: Dipa.agent_timeout,
|
14
|
+
coordinator_timeout: Dipa.coordinator_timeout,
|
15
|
+
# misc
|
16
|
+
async: false,
|
17
|
+
keep_data: false,
|
18
|
+
want_result: true
|
19
|
+
}.freeze
|
20
|
+
OVERRIDE_OPTIONS = {}.freeze
|
21
|
+
|
22
|
+
def with(processor_class, processor_method)
|
23
|
+
_validate_processor_arguments(processor_class: processor_class.to_s,
|
24
|
+
processor_method: processor_method.to_s)
|
25
|
+
|
26
|
+
_prepare_coordinator(processor_class: processor_class.to_s,
|
27
|
+
processor_method: processor_method.to_s)
|
28
|
+
|
29
|
+
_start_process
|
30
|
+
|
31
|
+
return if _async?
|
32
|
+
|
33
|
+
_wait_for_it
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :_source, :_raw_opts, :_coordinator
|
39
|
+
|
40
|
+
def initialize(source, options: {})
|
41
|
+
options.assert_valid_keys(*DEFAULT_OPTIONS.keys)
|
42
|
+
|
43
|
+
@_source = source
|
44
|
+
@_raw_opts = options
|
45
|
+
end
|
46
|
+
|
47
|
+
def _validate_processor_arguments(processor_class:, processor_method:)
|
48
|
+
return if processor_class.constantize.respond_to?(processor_method)
|
49
|
+
|
50
|
+
raise Dipa::UnknownProcessorMethodError,
|
51
|
+
"Method .#{processor_method} does not exist on processor class " \
|
52
|
+
"#{processor_class}"
|
53
|
+
rescue NameError => e
|
54
|
+
raise Dipa::UnknownProcessorClassError, e.original_message
|
55
|
+
end
|
56
|
+
|
57
|
+
def _wait_for_it
|
58
|
+
sleep(SYNC_MODE_WAIT_CYCLE_SECONDS) while _wait_for_it?
|
59
|
+
|
60
|
+
return _result if _coordinator.processed?
|
61
|
+
|
62
|
+
# must be an error then
|
63
|
+
_raise_error
|
64
|
+
end
|
65
|
+
|
66
|
+
def _wait_for_it?
|
67
|
+
_coordinator.reload
|
68
|
+
|
69
|
+
_coordinator.initialized? || _coordinator.processing?
|
70
|
+
end
|
71
|
+
|
72
|
+
def _raise_error
|
73
|
+
raise Dipa::AbortedError if _coordinator.aborted?
|
74
|
+
raise Dipa::ProcessingFailedError if _coordinator.processing_failed?
|
75
|
+
raise Dipa::TimeoutError if _coordinator.timed_out?
|
76
|
+
|
77
|
+
raise Dipa::UnknownProcessingStateError
|
78
|
+
end
|
79
|
+
|
80
|
+
def _result
|
81
|
+
result = _fetch_result
|
82
|
+
|
83
|
+
_maybe_cleanup
|
84
|
+
|
85
|
+
result
|
86
|
+
end
|
87
|
+
|
88
|
+
def _maybe_cleanup
|
89
|
+
_coordinator.destroy! unless _keep_data?
|
90
|
+
end
|
91
|
+
|
92
|
+
def _fetch_result
|
93
|
+
_want_result? ? _coordinator.result : _coordinator.source
|
94
|
+
end
|
95
|
+
|
96
|
+
def _start_process
|
97
|
+
Dipa::CoordinatorServices::StartProcessingService.call(
|
98
|
+
coordinator: _coordinator
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def _prepare_coordinator(processor_class:, processor_method:)
|
103
|
+
@_coordinator = Dipa::Coordinator.create!(
|
104
|
+
agent_queue: _agent_queue,
|
105
|
+
coordinator_queue: _coordinator_queue,
|
106
|
+
keep_data: _keep_data?,
|
107
|
+
processor_class_name: processor_class,
|
108
|
+
processor_method_name: processor_method,
|
109
|
+
size: _source.to_a.size,
|
110
|
+
want_result: _want_result?
|
111
|
+
)
|
112
|
+
|
113
|
+
_coordinator.dump_to_file(data: _source.to_a, attacher: :source_dump)
|
114
|
+
end
|
115
|
+
|
116
|
+
def _agent_queue
|
117
|
+
_option(option: :agent_queue)
|
118
|
+
end
|
119
|
+
|
120
|
+
def _coordinator_queue
|
121
|
+
_option(option: :coordinator_queue)
|
122
|
+
end
|
123
|
+
|
124
|
+
def _async?
|
125
|
+
_option(option: :async)
|
126
|
+
end
|
127
|
+
|
128
|
+
def _keep_data?
|
129
|
+
_option(option: :keep_data)
|
130
|
+
end
|
131
|
+
|
132
|
+
def _want_result?
|
133
|
+
_option(option: :want_result)
|
134
|
+
end
|
135
|
+
|
136
|
+
def _option(option:)
|
137
|
+
OVERRIDE_OPTIONS[option] || _raw_opts[option] || DEFAULT_OPTIONS[option]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/dipa/version.rb
ADDED
data/lib/dipa.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
require 'dipa/version'
|
6
|
+
require 'dipa/engine'
|
7
|
+
require 'dipa/errors'
|
8
|
+
|
9
|
+
module Dipa
|
10
|
+
extend ActiveSupport::Autoload
|
11
|
+
|
12
|
+
DEFAULT_AGENT_TIMEOUT = 0
|
13
|
+
DEFAULT_COORDINATOR_TIMEOUT = 0
|
14
|
+
|
15
|
+
# rubocop:disable ThreadSafety/ClassAndModuleAttributes
|
16
|
+
mattr_accessor :agent_queue
|
17
|
+
mattr_accessor :agent_timeout, default: DEFAULT_AGENT_TIMEOUT
|
18
|
+
mattr_accessor :coordinator_queue
|
19
|
+
mattr_accessor :coordinator_timeout, default: DEFAULT_COORDINATOR_TIMEOUT
|
20
|
+
# rubocop:enable ThreadSafety/ClassAndModuleAttributes
|
21
|
+
|
22
|
+
def self.map(source, options: {})
|
23
|
+
Dipa::Processor::Map.new(source, options: options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.each(source, options: {})
|
27
|
+
Dipa::Processor::Each.new(source, options: options)
|
28
|
+
end
|
29
|
+
|
30
|
+
module Processor
|
31
|
+
extend ActiveSupport::Autoload
|
32
|
+
|
33
|
+
autoload :Base
|
34
|
+
autoload :Each
|
35
|
+
autoload :Map
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# NOTE: only doing this in development as some production environments (Heroku)
|
4
|
+
# NOTE: are sensitive to local FS writes, and besides -- it's just not proper
|
5
|
+
# NOTE: to have a dev-mode tool do its thing in production.
|
6
|
+
if Rails.env.development?
|
7
|
+
require 'annotate'
|
8
|
+
desc 'annotate'
|
9
|
+
task set_annotation_options: :environment do # rubocop:disable Metrics/BlockLength
|
10
|
+
# You can override any of these by setting an environment variable of the
|
11
|
+
# same name.
|
12
|
+
Annotate.set_defaults(
|
13
|
+
'active_admin' => 'false',
|
14
|
+
'additional_file_patterns' => [],
|
15
|
+
'routes' => 'false',
|
16
|
+
'models' => 'true',
|
17
|
+
'position_in_routes' => 'before',
|
18
|
+
'position_in_class' => 'before',
|
19
|
+
'position_in_test' => 'before',
|
20
|
+
'position_in_fixture' => 'before',
|
21
|
+
'position_in_factory' => 'before',
|
22
|
+
'position_in_serializer' => 'before',
|
23
|
+
'show_foreign_keys' => 'true',
|
24
|
+
'show_complete_foreign_keys' => 'false',
|
25
|
+
'show_indexes' => 'true',
|
26
|
+
'simple_indexes' => 'false',
|
27
|
+
'model_dir' => 'app/models',
|
28
|
+
'root_dir' => '',
|
29
|
+
'include_version' => 'false',
|
30
|
+
'require' => '',
|
31
|
+
'exclude_tests' => 'true',
|
32
|
+
'exclude_fixtures' => 'true',
|
33
|
+
'exclude_factories' => 'false',
|
34
|
+
'exclude_serializers' => 'false',
|
35
|
+
'exclude_scaffolds' => 'true',
|
36
|
+
'exclude_controllers' => 'true',
|
37
|
+
'exclude_helpers' => 'true',
|
38
|
+
'exclude_sti_subclasses' => 'false',
|
39
|
+
'ignore_model_sub_dir' => 'false',
|
40
|
+
'ignore_columns' => nil,
|
41
|
+
'ignore_routes' => nil,
|
42
|
+
'ignore_unknown_models' => 'false',
|
43
|
+
'hide_limit_column_types' => 'integer,bigint,boolean',
|
44
|
+
'hide_default_column_types' => 'json,jsonb,hstore',
|
45
|
+
'skip_on_db_migrate' => 'false',
|
46
|
+
'format_bare' => 'true',
|
47
|
+
'format_rdoc' => 'false',
|
48
|
+
'format_yard' => 'false',
|
49
|
+
'format_markdown' => 'false',
|
50
|
+
'sort' => 'true',
|
51
|
+
'force' => 'false',
|
52
|
+
'frozen' => 'false',
|
53
|
+
'classified_sort' => 'true',
|
54
|
+
'trace' => 'false',
|
55
|
+
'wrapper_open' => nil,
|
56
|
+
'wrapper_close' => nil,
|
57
|
+
'with_comment' => 'true'
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
Annotate.load_tasks
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dipa
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.pre.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Merten Falk
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.0.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 8.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 6.0.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 8.0.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activerecord
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 6.0.0
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 8.0.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 6.0.0
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 8.0.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: activestorage
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 6.0.0
|
60
|
+
- - "<"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 8.0.0
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 6.0.0
|
70
|
+
- - "<"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 8.0.0
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: activesupport
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 6.0.0
|
80
|
+
- - "<"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 8.0.0
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 6.0.0
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 8.0.0
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rake
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '13.0'
|
100
|
+
type: :runtime
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - "~>"
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '13.0'
|
107
|
+
description: |
|
108
|
+
This gem provides an API for parallel processing like the [parallel
|
109
|
+
gem](https://github.com/grosser/parallel) but distributed and scalable over
|
110
|
+
different machines. All this with minimum configuration and minimum
|
111
|
+
dependencies to specific technologies and using the rails ecosystem.
|
112
|
+
|
113
|
+
Dipa provides a rails engine which depends on
|
114
|
+
[ActiveJob](https://guides.rubyonrails.org/active_job_basics.html) and
|
115
|
+
[ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html).
|
116
|
+
You can use whatever backend you like for any of this components and
|
117
|
+
configure them for your specific usecase.
|
118
|
+
|
119
|
+
The purpose of this gem is to distribute load heavy and long running
|
120
|
+
processing of large datasets over multiple processes or machines using
|
121
|
+
[ActiveJob](https://guides.rubyonrails.org/active_job_basics.html).
|
122
|
+
email:
|
123
|
+
- empunkt@mailbox.org
|
124
|
+
executables: []
|
125
|
+
extensions: []
|
126
|
+
extra_rdoc_files: []
|
127
|
+
files:
|
128
|
+
- LICENSE.txt
|
129
|
+
- README.md
|
130
|
+
- Rakefile
|
131
|
+
- app/jobs/dipa/application_job.rb
|
132
|
+
- app/jobs/dipa/service_job.rb
|
133
|
+
- app/models/concerns/models/dipa/dumpable.rb
|
134
|
+
- app/models/concerns/models/dipa/loadable.rb
|
135
|
+
- app/models/concerns/models/dipa/state_attribute_handling.rb
|
136
|
+
- app/models/dipa/agent.rb
|
137
|
+
- app/models/dipa/application_record.rb
|
138
|
+
- app/models/dipa/coordinator.rb
|
139
|
+
- app/models/modules/models/dipa/status_constants.rb
|
140
|
+
- app/services/dipa/agent_services/coordinator_state_service.rb
|
141
|
+
- app/services/dipa/agent_services/post_processing_service.rb
|
142
|
+
- app/services/dipa/agent_services/processing_service.rb
|
143
|
+
- app/services/dipa/agent_services/start_processing_service.rb
|
144
|
+
- app/services/dipa/application_service.rb
|
145
|
+
- app/services/dipa/coordinator_services/create_agents_service.rb
|
146
|
+
- app/services/dipa/coordinator_services/start_processing_service.rb
|
147
|
+
- app/validators/dipa/date_validator.rb
|
148
|
+
- db/migrate/20220102132652_create_dipa_coordinators.rb
|
149
|
+
- db/migrate/20220106183616_create_dipa_agents.rb
|
150
|
+
- lib/dipa.rb
|
151
|
+
- lib/dipa/engine.rb
|
152
|
+
- lib/dipa/errors.rb
|
153
|
+
- lib/dipa/processor/base.rb
|
154
|
+
- lib/dipa/processor/each.rb
|
155
|
+
- lib/dipa/processor/map.rb
|
156
|
+
- lib/dipa/version.rb
|
157
|
+
- lib/tasks/auto_annotate_models.rake
|
158
|
+
- lib/tasks/dipa_tasks.rake
|
159
|
+
homepage: https://codeberg.org/empunkt/dipa
|
160
|
+
licenses:
|
161
|
+
- MIT
|
162
|
+
metadata:
|
163
|
+
rubygems_mfa_required: 'true'
|
164
|
+
post_install_message:
|
165
|
+
rdoc_options: []
|
166
|
+
require_paths:
|
167
|
+
- lib
|
168
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: 2.7.0
|
173
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">"
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: 1.3.1
|
178
|
+
requirements: []
|
179
|
+
rubygems_version: 3.3.7
|
180
|
+
signing_key:
|
181
|
+
specification_version: 4
|
182
|
+
summary: Rails engine that provides an API to execute code in parallel and distributed
|
183
|
+
using the rails ecosystem.
|
184
|
+
test_files: []
|