jongleur 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +111 -0
- data/.gitlab-ci.yml +25 -0
- data/.rspec +3 -0
- data/.rubocop.yml +45 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +269 -0
- data/Rakefile +8 -0
- data/bin/console +11 -0
- data/bin/img/DAG_graph_1.png +0 -0
- data/bin/img/ETL_DAG.png +0 -0
- data/bin/img/jongleur_m-2015.jpg +0 -0
- data/bin/img/transactional_DAG.png +0 -0
- data/bin/setup +8 -0
- data/jongleur.gemspec +34 -0
- data/lib/jongleur.rb +41 -0
- data/lib/jongleur/api.rb +217 -0
- data/lib/jongleur/helpers.rb +9 -0
- data/lib/jongleur/implementation.rb +216 -0
- data/lib/jongleur/version.rb +5 -0
- data/lib/jongleur/worker_task.rb +20 -0
- metadata +193 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 749c9b43f797c10a6bbba43ab6fc74fbf9d5b430
|
4
|
+
data.tar.gz: 41077a42d773e8bd23d8446963beec566e118199
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6abc365ad553864cfaf5d8113a24a981abd5676e58789268fe2e95ee3a246038c083c893fa9977b87a0c4b08c04f2ba0fd16ff5c3d8aaa2842baef156cdc51de
|
7
|
+
data.tar.gz: 12dba9ec3a9f5d888b04c80234bf7a1becff58145fff7d2544ba027118b02803ea15672cef44594146c8842968c765e03d8563cecae5de93dd23f1fe90730ad3
|
data/.gitignore
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
|
2
|
+
# Created by https://www.gitignore.io/api/ruby,linux,macos,sublimetext
|
3
|
+
|
4
|
+
### Linux ###
|
5
|
+
*~
|
6
|
+
|
7
|
+
# temporary files which can be created if a process still has a handle open of a deleted file
|
8
|
+
.fuse_hidden*
|
9
|
+
|
10
|
+
# KDE directory preferences
|
11
|
+
.directory
|
12
|
+
|
13
|
+
# Linux trash folder which might appear on any partition or disk
|
14
|
+
.Trash-*
|
15
|
+
|
16
|
+
# .nfs files are created when an open file is removed but is still being accessed
|
17
|
+
.nfs*
|
18
|
+
|
19
|
+
### macOS ###
|
20
|
+
# General
|
21
|
+
.DS_Store
|
22
|
+
.AppleDouble
|
23
|
+
.LSOverride
|
24
|
+
|
25
|
+
# Icon must end with two \r
|
26
|
+
Icon
|
27
|
+
|
28
|
+
# Thumbnails
|
29
|
+
._*
|
30
|
+
|
31
|
+
# Files that might appear in the root of a volume
|
32
|
+
.DocumentRevisions-V100
|
33
|
+
.fseventsd
|
34
|
+
.Spotlight-V100
|
35
|
+
.TemporaryItems
|
36
|
+
.Trashes
|
37
|
+
.VolumeIcon.icns
|
38
|
+
.com.apple.timemachine.donotpresent
|
39
|
+
|
40
|
+
# Directories potentially created on remote AFP share
|
41
|
+
.AppleDB
|
42
|
+
.AppleDesktop
|
43
|
+
Network Trash Folder
|
44
|
+
Temporary Items
|
45
|
+
.apdisk
|
46
|
+
|
47
|
+
### Ruby ###
|
48
|
+
*.gem
|
49
|
+
*.rbc
|
50
|
+
/.config
|
51
|
+
/coverage/
|
52
|
+
/InstalledFiles
|
53
|
+
/pkg/
|
54
|
+
/spec/reports/
|
55
|
+
/spec/examples.txt
|
56
|
+
/test/tmp/
|
57
|
+
/test/version_tmp/
|
58
|
+
/tmp/
|
59
|
+
|
60
|
+
# Used by dotenv library to load environment variables.
|
61
|
+
# .env
|
62
|
+
|
63
|
+
|
64
|
+
## Documentation cache and generated files:
|
65
|
+
/.yardoc/
|
66
|
+
/_yardoc/
|
67
|
+
/doc/
|
68
|
+
/rdoc/
|
69
|
+
|
70
|
+
## Environment normalization:
|
71
|
+
/.bundle/
|
72
|
+
/vendor/bundle
|
73
|
+
/lib/bundler/man/
|
74
|
+
|
75
|
+
|
76
|
+
Gemfile.lock
|
77
|
+
.ruby-version
|
78
|
+
.ruby-gemset
|
79
|
+
|
80
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
81
|
+
.rvmrc
|
82
|
+
|
83
|
+
### SublimeText ###
|
84
|
+
# Cache files for Sublime Text
|
85
|
+
*.tmlanguage.cache
|
86
|
+
*.tmPreferences.cache
|
87
|
+
*.stTheme.cache
|
88
|
+
|
89
|
+
# Workspace files are user-specific
|
90
|
+
*.sublime-workspace
|
91
|
+
|
92
|
+
|
93
|
+
# SFTP configuration file
|
94
|
+
sftp-config.json
|
95
|
+
|
96
|
+
# Package control specific files
|
97
|
+
Package Control.last-run
|
98
|
+
Package Control.ca-list
|
99
|
+
Package Control.ca-bundle
|
100
|
+
Package Control.system-ca-bundle
|
101
|
+
Package Control.cache/
|
102
|
+
Package Control.ca-certs/
|
103
|
+
Package Control.merged-ca-bundle
|
104
|
+
Package Control.user-ca-bundle
|
105
|
+
oscrypto-ca-bundle.crt
|
106
|
+
bh_unicode_properties.cache
|
107
|
+
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
# End of https://www.gitignore.io/api/ruby,linux,macos,sublimetext
|
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
before_script:
|
2
|
+
- ruby -v
|
3
|
+
- which ruby
|
4
|
+
- gem install bundler --no-ri --no-rdoc
|
5
|
+
- bundle install --jobs $(nproc) "${FLAGS[@]}"
|
6
|
+
|
7
|
+
test:2.4.3:
|
8
|
+
image: ruby:2.4.3
|
9
|
+
script:
|
10
|
+
- bundle exec rspec
|
11
|
+
|
12
|
+
test:2.4.4:
|
13
|
+
image: ruby:2.4.4
|
14
|
+
script:
|
15
|
+
- bundle exec rspec
|
16
|
+
|
17
|
+
test:2.5.0:
|
18
|
+
image: ruby:2.5.0
|
19
|
+
script:
|
20
|
+
- bundle exec rspec
|
21
|
+
|
22
|
+
test:2.5.1:
|
23
|
+
image: ruby:2.5.1
|
24
|
+
script:
|
25
|
+
- bundle exec rspec
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
Style/Semicolon:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
Style/ClassVars:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Metrics/BlockLength:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Style/CommentedKeyword:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/FormatStringToken:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Metrics/CyclomaticComplexity:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Metrics/MethodLength:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Metrics/PerceivedComplexity:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Metrics/ModuleLength:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
Metrics/LineLength:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Metrics/AbcSize:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
Style/FormatString:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Layout/AlignArray:
|
38
|
+
Enabled: false
|
39
|
+
|
40
|
+
Style/StringLiterals:
|
41
|
+
Enabled: false
|
42
|
+
|
43
|
+
AllCops:
|
44
|
+
Exclude:
|
45
|
+
- test.rb
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Fred Heath
|
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,269 @@
|
|
1
|
+
# Jongleur
|
2
|
+
<img src="./bin/img/jongleur_m-2015.jpg" width="150" height="150">
|
3
|
+
|
4
|
+
Jongleur is a process scheduler and manager. It allows its users to declare a number of executable tasks as Ruby classes, define precedence between those tasks and run each task as a separate process.
|
5
|
+
|
6
|
+
Jongleur is particularly useful for implementing workflows modeled as a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph)
|
7
|
+
(Directed Acyclic Graph), but can be also used to run multiple tasks in parallel or even sequential workflows where each task needs to run as a separate OS process.
|
8
|
+
|
9
|
+
## Environment
|
10
|
+
|
11
|
+
This gem has been built using the [POSIX/UNIX process model](https://support.sas.com/documentation/onlinedoc/sasc/doc750/html/lr2/zid-6574.htm).
|
12
|
+
It will work on Linux and Mac OS but not on Windows.
|
13
|
+
|
14
|
+
Jongleur has been tested with MRuby 2.4.3, 2.4.4, 2.5.0 and 2.5.1. I would also expect it to work with other Ruby implementations too, such as JRuby or Rubinius though it hasn't yet been tested on those.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'jongleur'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install jongleur
|
31
|
+
|
32
|
+
|
33
|
+
## What does it do?
|
34
|
+
|
35
|
+
In a nutshell, Jongleur keeps track of a number of tasks and executes them as separate OS processes according to their precedence criteria. For instance, if there are 3 tasks A, B and C, and task C depends on A and B, Jongleur will start executing A and B in separate processes (i.e. in parallel) and will wait until they are both finished before it executes C in a separate process.
|
36
|
+
|
37
|
+
Jongleur is ideal for running workflows represented as DAGs, but is also useful for simply running tasks in parallel or for whenever you need some multi-processing capability.
|
38
|
+
|
39
|
+
## Concepts
|
40
|
+
|
41
|
+
### Task Graph
|
42
|
+
|
43
|
+
To run Jongleur, you will need to define the tasks to run and their precedence. A _Task Graph_ is a
|
44
|
+
representation of the tasks to be run by Jongleur and it usually (but not exclusively) represents a DAG, as in the examples below:
|
45
|
+
|
46
|
+
![DAG examples](https://upload.wikimedia.org/wikipedia/commons/f/fa/Dag_graf.JPG)
|
47
|
+
|
48
|
+
A _Task Graph_ is defined as a Hash in the following format:
|
49
|
+
|
50
|
+
`{task-name => list[names-of-dependent-tasks]}`
|
51
|
+
|
52
|
+
|
53
|
+
So the first graph would be defined as:
|
54
|
+
|
55
|
+
```
|
56
|
+
my_graph = {
|
57
|
+
s: [:q, :r, :t],
|
58
|
+
q: [:r],
|
59
|
+
r: [],
|
60
|
+
t: []
|
61
|
+
}
|
62
|
+
|
63
|
+
```
|
64
|
+
|
65
|
+
where they Hash key is the class name of a Task and the Hash value is an Array of other Tasks that can be
|
66
|
+
run only after this Task is finished. So in the above example:
|
67
|
+
|
68
|
+
* Tasks Q, R and T can only start after task S has finished.
|
69
|
+
* Task R can only start after Q has finished.
|
70
|
+
* Tasks T and T have no dependents. No other task need wait for them.
|
71
|
+
|
72
|
+
__N.B:__ Since the _Task Graph_ is a Hash, any duplicate key entries will be overriden. For instance, if this Task Graph
|
73
|
+
|
74
|
+
```
|
75
|
+
my_task_graph = { A: [:B, :C], B: [:D] }
|
76
|
+
```
|
77
|
+
is re-defined as
|
78
|
+
|
79
|
+
```
|
80
|
+
my_task_graph = { A: [:B], A: [:C], B: [:D] }
|
81
|
+
```
|
82
|
+
The 2nd assignment of `A` will override the first one so your graph will be:
|
83
|
+
|
84
|
+
`{:A=>[:C], :B=>[:D]}`
|
85
|
+
|
86
|
+
Always assign all dependent tasks together in a single list.
|
87
|
+
|
88
|
+
### Task Matrix
|
89
|
+
|
90
|
+
It's a tabular real-time representation of the state of task execution. It can be invoked at any time with
|
91
|
+
|
92
|
+
```
|
93
|
+
Jongleur::API.task_matrix
|
94
|
+
```
|
95
|
+
|
96
|
+
After defining your Task Graph and before running Jongleur, your _Task Matrix_ should look like this:
|
97
|
+
|
98
|
+
```
|
99
|
+
#<Jongleur::Task name=:A, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
100
|
+
#<Jongleur::Task name=:B, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
101
|
+
#<Jongleur::Task name=:C, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
102
|
+
#<Jongleur::Task name=:D, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
103
|
+
#<Jongleur::Task name=:E, pid=-1, running=false, exit_status=nil, success_status=nil>
|
104
|
+
```
|
105
|
+
After Jongleur finishes, your _Task Matrix_ will look something like this:
|
106
|
+
|
107
|
+
```
|
108
|
+
#<Jongleur::Task name=:A, pid=95117, running=false, exit_status=0, success_status=true>
|
109
|
+
#<Jongleur::Task name=:B, pid=95118, running=false, exit_status=0, success_status=true>
|
110
|
+
#<Jongleur::Task name=:C, pid=95120, running=false, exit_status=0, success_status=true>
|
111
|
+
#<Jongleur::Task name=:D, pid=95122, running=false, exit_status=0, success_status=true>
|
112
|
+
#<Jongleur::Task name=:E, pid=95123, running=false, exit_status=0, success_status=true>
|
113
|
+
```
|
114
|
+
|
115
|
+
The `Jongleur::Task` attribute values are as follows
|
116
|
+
|
117
|
+
* name : the Task name
|
118
|
+
* pid : the Task process id (`nil` if the task hasn't yet ran)
|
119
|
+
* running : `true` if task is currently running
|
120
|
+
* exit_status : usually 0 if process finished without errors, <>0 or `nil` otherwise
|
121
|
+
* success_status : `true` if process finished successfully, `false` if it didn't or `nil` if process didn't exit at all
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
### WorkerTask
|
126
|
+
|
127
|
+
This is the implementation template for a Task. For each Task in your Task Graph you must provide a class that derives from `WorkerTask` and implements the `execute` method. This method is what will be called by Jongleur when the Task is ready to run.
|
128
|
+
|
129
|
+
## Usage
|
130
|
+
|
131
|
+
Using Jongleur is easy:
|
132
|
+
|
133
|
+
1. (Optional) You may want to head your code with `require Jongleur` so that you won't have to namespace every api call.
|
134
|
+
|
135
|
+
2. Define your Task Graph
|
136
|
+
|
137
|
+
```
|
138
|
+
test_graph = {
|
139
|
+
A: [:B, :C],
|
140
|
+
B: [:D],
|
141
|
+
C: [:D],
|
142
|
+
D: [:E],
|
143
|
+
E: []
|
144
|
+
}
|
145
|
+
```
|
146
|
+
|
147
|
+
3. Add your Task Graph to Jongleur
|
148
|
+
|
149
|
+
```
|
150
|
+
API.add_task_graph test_graph
|
151
|
+
|
152
|
+
=> [#<struct Jongleur::Task name=:A, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
153
|
+
#<struct Jongleur::Task name=:B, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
154
|
+
#<struct Jongleur::Task name=:C, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
155
|
+
#<struct Jongleur::Task name=:D, pid=-1, running=false, exit_status=nil, success_status=nil>,
|
156
|
+
#<struct Jongleur::Task name=:E, pid=-1, running=false, exit_status=nil, success_status=nil>]
|
157
|
+
```
|
158
|
+
Jongleur will show you the Task Matrix for your Task Graph with all attributes set at their initial values, obviously, since the Tasks haven't ran yet.
|
159
|
+
|
160
|
+
4. (Optional) You may want to see a graphical representation of your Task Graph
|
161
|
+
|
162
|
+
```
|
163
|
+
API.print_graph('/tmp')
|
164
|
+
|
165
|
+
=> "/tmp/jongleur_graph_08252018_194828.pdf"
|
166
|
+
```
|
167
|
+
Opening the PDF file will display this:
|
168
|
+
|
169
|
+
<img src="./bin/img/DAG_graph_1.png" width="225" height="450" alt="ETL DAG">
|
170
|
+
|
171
|
+
5. Implement your tasks. To do that you have to (i) create a new class, based on `WorkerTask` and (ii) define and `#execute` method in your class. This is the method hat Jongleur will call to run the Task. For instance task A from your Task Graph may look something like that:
|
172
|
+
|
173
|
+
```
|
174
|
+
class A < Jongleur::WorkerTask
|
175
|
+
@desc = 'this is task A'
|
176
|
+
def execute
|
177
|
+
sleep 1
|
178
|
+
'A is running... '
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
You'll have to do the same for Tasks B, C, D and E, as these ae the tasks declared in the Task Graph.
|
183
|
+
|
184
|
+
6. Run the tasks. Ok, pay attention now because this is the complex bit. Nah, only joking - it's simply:
|
185
|
+
|
186
|
+
```
|
187
|
+
API.run
|
188
|
+
|
189
|
+
|
190
|
+
=> Starting workflow...
|
191
|
+
=> starting task A
|
192
|
+
=> finished task: A, process: 2501, exit_status: 0, success: true
|
193
|
+
=> starting task B
|
194
|
+
=> starting task C
|
195
|
+
=> finished task: C, process: 2503, exit_status: 0, success: true
|
196
|
+
=> finished task: B, process: 2502, exit_status: 0, success: true
|
197
|
+
=> starting task D
|
198
|
+
=> finished task: D, process: 2505, exit_status: 0, success: true
|
199
|
+
=> starting task E
|
200
|
+
=> finished task: E, process: 2506, exit_status: 0, success: true
|
201
|
+
=> Workflow finished
|
202
|
+
```
|
203
|
+
|
204
|
+
A __simple example__ of a client app fro Jongleur can be found [on GitLab](https://gitlab.com/RedFred7/jongleur-client)
|
205
|
+
|
206
|
+
## Use-Cases
|
207
|
+
### Extract-Transform-Load
|
208
|
+
The ETL workflow is ideally suited to Jongleur. You can define many Extraction tasks -maybe separate Tasks for different data sources- and have them ran in parallel to each other. At the same time Transformation and Loading Tasks wait in turn for the previous task to finish before they start, as in this DAG illustration:
|
209
|
+
|
210
|
+
<img src="./bin/img/ETL_DAG.png" width="450" height="450" alt="ETL DAG">
|
211
|
+
|
212
|
+
### Transactions
|
213
|
+
Transactional workflows can be greatly sped up by Jongleur by parallelising parts of the transaction that are usually performed sequentially, i.e:
|
214
|
+
|
215
|
+
<img src="./bin/img/transactional_DAG.png" width="550" height="450" alt="Transaction DAG">
|
216
|
+
|
217
|
+
## Development
|
218
|
+
|
219
|
+
After checking out the repo, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
220
|
+
|
221
|
+
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).
|
222
|
+
|
223
|
+
|
224
|
+
## F.A.Q
|
225
|
+
|
226
|
+
### Does Jongleur allow me to pass messages between Tasks?
|
227
|
+
No it doesn't. Each task is run competely independently from the other Tasks. There is no Inter-Process Communication, no common data contexts, no shared memory.
|
228
|
+
|
229
|
+
### How can I share data created by a predecessor Task?
|
230
|
+
This is something that I wouldl ike to build into Jongleur. For now, you can save a Task's data in a detabase or KV Store and using the Tasks process id as part of the key. Subsequent Tasks can retrieve their predecessor's process ids with
|
231
|
+
|
232
|
+
```
|
233
|
+
API.get_predecessor_pids
|
234
|
+
```
|
235
|
+
|
236
|
+
and therefore retrieve the data created by those Tasks.
|
237
|
+
|
238
|
+
### What's the difference between Jongleur::Task's _success\_status_ and _exit\_status_ attributes?
|
239
|
+
According to [the official docs](https://ruby-doc.org/core-2.4.3/Process/Status.html) `exit_status` returns the least significant eight bits of the return code of the `stat` call while `success_status` returns true if `stat` is successful.
|
240
|
+
|
241
|
+
### What happens when Jongleur finishes running?
|
242
|
+
When Jongleur finishes running all tasks in its Task Graph -and regardless of whether the Tasks themselves have failed ot not- it will exit the parent process with an exit code of 0.
|
243
|
+
|
244
|
+
### What happens if a Task fails
|
245
|
+
If a Task fails to run or to finish its run, Jongleur will simply go on running any other tasks it can. It will not run any Tasks which depend on the failed Task. The status of the failed Task will be indicated via an appropriate output message and also on the Task Matrix.
|
246
|
+
|
247
|
+
### How can I examine the Task Matrix after Jongleur has finished?
|
248
|
+
Jongleur serializes each run's Task Matrix as a JSON file in the `/tmp` directory. You can either view this in an editor or load it and manipulate it in Ruby with
|
249
|
+
|
250
|
+
```
|
251
|
+
JSON.parse( File.read('/tmp/jongleur_task_matrix_08272018_103406.json') )
|
252
|
+
```
|
253
|
+
|
254
|
+
|
255
|
+
## Roadmap
|
256
|
+
|
257
|
+
These are the things I'd like Jongleur to support in future releases:
|
258
|
+
|
259
|
+
* Task storage mechanism, i.e. the ability for each Task to save data in a uniquely identifiable and safe way so that data can be shared between
|
260
|
+
sequential tasks in a transparent and easy manner.
|
261
|
+
* Rails integration. Pretty self-explanatory really.
|
262
|
+
|
263
|
+
## Contributing
|
264
|
+
|
265
|
+
Any suggestions for new features or improvements are very welcome. Please raise bug reports and pull requests on [GitLab](https://gitlab.com/RedFred7/Jongleur).
|
266
|
+
|
267
|
+
## License
|
268
|
+
|
269
|
+
The gem is available as open source under the terms of the [MIT License](./License.txt)
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'jongleur'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require 'pry'
|
11
|
+
Pry.start
|
Binary file
|
data/bin/img/ETL_DAG.png
ADDED
Binary file
|
Binary file
|
Binary file
|
data/bin/setup
ADDED
data/jongleur.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'jongleur/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'jongleur'
|
9
|
+
spec.version = Jongleur::VERSION
|
10
|
+
spec.authors = ['Fred Heath']
|
11
|
+
spec.email = ['fred@bootstrap.me.uk']
|
12
|
+
|
13
|
+
spec.summary = 'A task scheduler manager for DAG-style task groups.'
|
14
|
+
spec.description = 'Acceps a number of inter-dependent tasks and runs them as separate processes, parallelising where possible.'
|
15
|
+
spec.homepage = 'http://www.bootstrap.me.uk'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'graphviz', '~> 1.1'
|
26
|
+
spec.add_dependency 'os', '~> 1.0'
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
28
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.4'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
|
+
spec.add_development_dependency 'rubocop', '~> 0.58'
|
32
|
+
spec.add_development_dependency 'simplecov', '~> 0.9'
|
33
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
34
|
+
end
|
data/lib/jongleur.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'graphviz'
|
3
|
+
require 'json'
|
4
|
+
require_relative 'jongleur/version'
|
5
|
+
require_relative 'jongleur/helpers'
|
6
|
+
require_relative 'jongleur/worker_task'
|
7
|
+
require_relative 'jongleur/implementation'
|
8
|
+
require_relative 'jongleur/api'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
# this is the gem's main module
|
12
|
+
module Jongleur
|
13
|
+
# a Task is a representation of the status of an executable Jongleur class,
|
14
|
+
# i.e. a class derived from WorkerTask and the process that's executing that class
|
15
|
+
# @see https://ruby-doc.org/core-2.4.3/Process/Status.html
|
16
|
+
#
|
17
|
+
# @!attribute name
|
18
|
+
# @return [String] the class (WorkerTask) name that's executing this process
|
19
|
+
# @!attribute pid
|
20
|
+
# @return [Integer] the process id accoding to the OS
|
21
|
+
# @!attribute running
|
22
|
+
# @return [Boolean] true if the process is running
|
23
|
+
# @!attribute exit_status
|
24
|
+
# @return [Integer, Nil] the process's return code when the process is exited
|
25
|
+
# Usually 0 for success, 1 for error or Nil otherwise
|
26
|
+
# @!attribute success_status
|
27
|
+
# @return [Boolean, Nil] true if process finished successfully, false if it didn't
|
28
|
+
# or nil if process didn't exit properly.
|
29
|
+
Task = Struct.new(:name, :pid, :running, :exit_status, :success_status)
|
30
|
+
|
31
|
+
$stdout.sync = true
|
32
|
+
|
33
|
+
module StatusCodes
|
34
|
+
PROCESS_NOT_YET_RAN = -1
|
35
|
+
TASK_NOT_IN_TASK_MATRIX = -8
|
36
|
+
TASK_NOT_IN_TASK_GRAPH = -9
|
37
|
+
SUCCESS_STATUS_UNDETERMINED = -2
|
38
|
+
end
|
39
|
+
|
40
|
+
end # module
|
41
|
+
|
data/lib/jongleur/api.rb
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Lint/HandleExceptions
|
4
|
+
|
5
|
+
require_relative './implementation'
|
6
|
+
module Jongleur
|
7
|
+
# Here be methods to be accessed by the gem's client, i.e. the public API
|
8
|
+
module API
|
9
|
+
# @!scope class
|
10
|
+
|
11
|
+
# Accepts a task_graph and does some initialisation, namely the assigning
|
12
|
+
# of class variables and creation of the inital task matrix
|
13
|
+
#
|
14
|
+
# @param [Hash<Symbol, Array>] task_graph_hash
|
15
|
+
# @raise [ArgumentError] if the task_matrix argument is not structured correctly
|
16
|
+
# @return [void]
|
17
|
+
def self.add_task_graph(task_graph_hash)
|
18
|
+
@@task_matrix = Array.new
|
19
|
+
raise ArgumentError, 'Value should be Hash {task_name, [descendants]}' unless task_graph_hash.is_a?(Hash)
|
20
|
+
# this task_graph will raise the error below , { A: [:B], B: :C, C: []}
|
21
|
+
task_graph_hash.values.each do |val|
|
22
|
+
raise ArgumentError, 'Dependent Tasks should be wrapped in an Array {task_name, [dependents]}' unless val.is_a?(Array)
|
23
|
+
end
|
24
|
+
# this task_graph will raise the error below , { A: [:B], B: [:C, :D], C: []}
|
25
|
+
if (task_graph_hash.keys.size - task_graph_hash.values.flatten.uniq.size).negative?
|
26
|
+
raise ArgumentError, 'Each dependent Task should also be defined with a separate key entry'
|
27
|
+
end
|
28
|
+
@@task_graph = task_graph_hash
|
29
|
+
@@task_matrix = Implementation.build_task_matrix(task_graph_hash)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Prints the TaskGraph to a PDF file
|
33
|
+
#
|
34
|
+
# @param [String] the directory name to print the file to
|
35
|
+
# @return [String] the PDF file name
|
36
|
+
def self.print_graph(dir="")
|
37
|
+
graph = Graphviz::Graph.new
|
38
|
+
dir = Dir.pwd if (!dir || dir.empty?)
|
39
|
+
file_name = File.expand_path("jongleur_graph_#{Time.now.strftime('%m%d%Y_%H%M%S')}.pdf", dir)
|
40
|
+
task_graph.each do |parent_node, child_nodes|
|
41
|
+
new_node = unless graph.node_exists?(parent_node)
|
42
|
+
graph.add_node( parent_node )
|
43
|
+
else
|
44
|
+
graph.get_node( parent_node ).first
|
45
|
+
end
|
46
|
+
|
47
|
+
child_nodes.each { |child_node| new_node.add_node(child_node) }
|
48
|
+
end
|
49
|
+
Graphviz::output(graph, path: file_name)
|
50
|
+
file_name
|
51
|
+
end
|
52
|
+
|
53
|
+
# @!attribute task_matrix
|
54
|
+
# @return [Array<Jongleur::Task>] a list of Tasks and their current state
|
55
|
+
# @see Jongleur::Task
|
56
|
+
def self.task_matrix
|
57
|
+
@@task_matrix
|
58
|
+
end
|
59
|
+
|
60
|
+
# @!attribute task_graph
|
61
|
+
# @return [Hash<Symbol, Array<Symbol>>] where the Hash key is the Task
|
62
|
+
# name and the value is an array of dependent Tasks
|
63
|
+
# @example
|
64
|
+
# a_task_graph = {:A=>[:B, :C], :B=>[:D], :C=>[:D], :D=>[:E], :E=>[]}
|
65
|
+
def self.task_graph
|
66
|
+
@@task_graph ||= {}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Analyses the Task Matrix for all Tasks that ran successfully
|
70
|
+
#
|
71
|
+
# @param [Array<Jongleur::Task>] the task matrix to analyse
|
72
|
+
# @return [Array<Jongleur::Task>] the successful Tasks
|
73
|
+
def self.successful_tasks(my_task_matrix)
|
74
|
+
my_task_matrix.select { |x| x.success_status == true &&
|
75
|
+
x.exit_status == 0
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Analyses the Task Matrix for all Tasks that failed to finish successfully
|
80
|
+
#
|
81
|
+
# @param [Array<Jongleur::Task>] the task matrix to analyse
|
82
|
+
# @return [Array<Jongleur::Task>] the failed Tasks
|
83
|
+
def self.failed_tasks(my_task_matrix)
|
84
|
+
my_task_matrix.select { |x| x.success_status == false }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Analyses the Task Matrix for all Tasks that haven't been ran
|
88
|
+
#
|
89
|
+
# @param [Array<Jongleur::Task>] the task matrix to analyse
|
90
|
+
# @return [Array<Jongleur::Task>] the Tasks that haven't been ran
|
91
|
+
def self.not_ran_tasks(my_task_matrix)
|
92
|
+
my_task_matrix.select { |x| x.success_status == nil &&
|
93
|
+
x.exit_status == nil &&
|
94
|
+
x.pid == StatusCodes::PROCESS_NOT_YET_RAN
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Analyses the Task Matrix for all Tasks that started but failed to finish
|
99
|
+
#
|
100
|
+
# @param [Array<Jongleur::Task>] the task matrix to analyse
|
101
|
+
# @return [Array<Jongleur::Task>] the Tasks that started but failed to finish
|
102
|
+
def self.hung_tasks(my_task_matrix)
|
103
|
+
my_task_matrix.select { |x| x.success_status == nil &&
|
104
|
+
x.pid != StatusCodes::PROCESS_NOT_YET_RAN
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.get_predecessor_pids(a_task)
|
109
|
+
pids = Array.new
|
110
|
+
Implementation.get_predecessors(a_task).each do |task|
|
111
|
+
pids << Implementation.get_process_id(task)
|
112
|
+
end
|
113
|
+
pids
|
114
|
+
end
|
115
|
+
|
116
|
+
# The main method. It starts the tasks as separate processes, according to
|
117
|
+
# their precedence, traps and handles signals, processes messages. On exit
|
118
|
+
# it will also print the Task Matrix in the /tmp directory in JSON format
|
119
|
+
#
|
120
|
+
# @note This method launches processes without precedence constraints,
|
121
|
+
# traps child process signals and starts new processes when their
|
122
|
+
# antecedents have finished. The method will exit its own process when
|
123
|
+
# all children processes have finished.
|
124
|
+
# @raise [RuntimeError] if there are no implementations for Tasks in the Task Graph
|
125
|
+
# @return [void]
|
126
|
+
def self.run
|
127
|
+
unless Implementation.valid_tasks?(task_graph.keys)
|
128
|
+
raise RuntimeError, 'Not all the tasks in the Task Graph are implemented as WorkerTask classes'
|
129
|
+
end
|
130
|
+
|
131
|
+
Implementation.process_message 'Starting workflow...'
|
132
|
+
trap_quit_signals
|
133
|
+
start_processes
|
134
|
+
|
135
|
+
trap(:CHLD) do
|
136
|
+
begin
|
137
|
+
# with WNOHANG flag we make sure Process.wait is not blocking
|
138
|
+
while (res = Process.wait2(-1, Process::WNOHANG))
|
139
|
+
dead_pid = res[0]
|
140
|
+
status = res[1]
|
141
|
+
dead_task_name = ''
|
142
|
+
Implementation.find_task_by(:pid, dead_pid) do |t|
|
143
|
+
t.running = false
|
144
|
+
t.exit_status = status.exitstatus
|
145
|
+
t.success_status = status.success?
|
146
|
+
dead_task_name = t.name
|
147
|
+
end
|
148
|
+
msg = "finished task: %s, process: %i, exit_status: %i, success: %s"
|
149
|
+
Implementation.process_message msg % [dead_task_name,
|
150
|
+
dead_pid,
|
151
|
+
status.exitstatus,
|
152
|
+
status.success?]
|
153
|
+
|
154
|
+
if status.success?
|
155
|
+
Implementation.run_descendants(dead_task_name)
|
156
|
+
else
|
157
|
+
msg = "Task #{dead_task_name} with process id #{dead_pid} was not succesfully completed."
|
158
|
+
Implementation.process_message(msg)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# it's possible for the last CHLD signal to arrive after our trap
|
163
|
+
# handler has already called Process.wait twice and reaped the
|
164
|
+
# available status. In such a case we must handle (and ignore)
|
165
|
+
# the oncoming exception so we don't get a crash.
|
166
|
+
rescue Errno::ECHILD
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
loop do
|
171
|
+
# We exit once all the child processes and their descendants are
|
172
|
+
# accounted for
|
173
|
+
if Implementation.running_tasks.empty?
|
174
|
+
Implementation.process_message 'Workflow finished'
|
175
|
+
file_name = File.expand_path("jongleur_task_matrix_#{Time.now.strftime('%m%d%Y_%H%M%S')}.json", '/tmp')
|
176
|
+
File.open(file_name, 'w') {|f| f.write(task_matrix.to_json) }
|
177
|
+
exit 0
|
178
|
+
end
|
179
|
+
sleep 1
|
180
|
+
end
|
181
|
+
end #method
|
182
|
+
|
183
|
+
|
184
|
+
# Starts all tasks without dependencies as separate processes
|
185
|
+
#
|
186
|
+
# @return [void]
|
187
|
+
def self.start_processes
|
188
|
+
Implementation.tasks_without_predecessors.each do |t|
|
189
|
+
t.running = true
|
190
|
+
Implementation.process_message "starting task #{t.name}"
|
191
|
+
t.pid = fork do
|
192
|
+
Jongleur.const_get(t.name).new(predecessors: Implementation.get_predecessors(t.name)).execute
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Forwards any quit signals to all working processes so that quitting the
|
198
|
+
# gem (Ctrl+C) kills all processes
|
199
|
+
#
|
200
|
+
# @return [void]
|
201
|
+
def self.trap_quit_signals
|
202
|
+
%i[INT QUIT].each do |signal|
|
203
|
+
Signal.trap(signal) do
|
204
|
+
Implementation.process_message " #{signal} sent to master process!"
|
205
|
+
Implementation.running_tasks.each do |t|
|
206
|
+
Implementation.process_message "....killing #{t.pid}"
|
207
|
+
Process.kill(:KILL, t.pid)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
end #module
|
215
|
+
end #module
|
216
|
+
|
217
|
+
# rubocop:enable Lint/HandleExceptions
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Lint/AssignmentInCondition
|
4
|
+
|
5
|
+
module Jongleur
|
6
|
+
# this module encapsulates methods that are not meant to be accessed by the gem's client callers
|
7
|
+
# and are used by the API module to implement functionality
|
8
|
+
# @see API
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
module Implementation
|
12
|
+
|
13
|
+
# Creates a list of tasks and their current state
|
14
|
+
#
|
15
|
+
# @param [Hash] task_graph
|
16
|
+
# @see API.task_graph
|
17
|
+
# @return [Array] task_matrix a list of Tasks
|
18
|
+
def self.build_task_matrix(task_graph)
|
19
|
+
return [] if task_graph.empty?
|
20
|
+
# create it as a Set so we can easily ensure unique entries
|
21
|
+
task_matrix = Set.new
|
22
|
+
task_graph.keys.each { |t| task_matrix << Task.new(t, StatusCodes::PROCESS_NOT_YET_RAN, false) }
|
23
|
+
task_graph.values.each do |val|
|
24
|
+
val.each { |t| task_matrix << Task.new(t, StatusCodes::PROCESS_NOT_YET_RAN, false) }
|
25
|
+
end
|
26
|
+
task_matrix.to_a
|
27
|
+
end
|
28
|
+
|
29
|
+
# Lists a task's dependent tasks
|
30
|
+
#
|
31
|
+
# @param [Symbol] task
|
32
|
+
# @return [Array] a list of the dependent task names for the given task
|
33
|
+
def self.get_predecessors(task)
|
34
|
+
return [] if API.task_graph.empty?
|
35
|
+
API.task_graph.select { |_k, v| v.include?(task) }.keys
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ensures a task, or list of tasks, are defined in the task_diagram and are loaded in Ruby.
|
39
|
+
# If #const_get can't find the class it raises NameError. The method catches it and returns false
|
40
|
+
#
|
41
|
+
# @note this method exists for the scenario where the user adds a task X to the Task Diagram but fails
|
42
|
+
# to provide an implementation of the Task's class, i.e. class X < WorkerTask
|
43
|
+
# @param [Array<Symbol>] tasks to be validated
|
44
|
+
# @return [Boolean] true if all tasks are valid, and false if one task or more are invalid
|
45
|
+
def self.valid_tasks?(task_list)
|
46
|
+
task_list.each { |task| API.const_get(task.to_s) }
|
47
|
+
true
|
48
|
+
rescue NameError
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
# Gets the process id of a task.
|
53
|
+
#
|
54
|
+
# @param [Symbol] task_name
|
55
|
+
# @return [Integer] the pid of the task or Jongleur::StatusCodes::PROCESS_NOT_YET_RAN if the task
|
56
|
+
# hasn't been ran yet
|
57
|
+
def self.get_process_id(task_name)
|
58
|
+
if valid_tasks?([].push(task_name))
|
59
|
+
idx = API.task_matrix.index { |t| t.name == task_name }
|
60
|
+
# STDOUT.puts ">>>>> #{task_name} >>>>>> #{API.task_matrix[idx].pid}", "\n"
|
61
|
+
API.task_matrix[idx].pid
|
62
|
+
else
|
63
|
+
StatusCodes::TASK_NOT_IN_TASK_GRAPH
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Gets a task's exit status
|
68
|
+
# @see https://ruby-doc.org/core-2.4.3/Process/Status.html
|
69
|
+
#
|
70
|
+
# @param [Symbol] task_name
|
71
|
+
# @return [Integer] the task's exit status or StatusCodes::TASK_NOT_IN_TASK_MATRIX
|
72
|
+
def self.get_exit_status(task_name)
|
73
|
+
idx = API.task_matrix.index { |t| t.name == task_name }
|
74
|
+
idx ? API.task_matrix[idx].exit_status : StatusCodes::TASK_NOT_IN_TASK_MATRIX
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.are_predecessors_running?(task_name)
|
78
|
+
!get_predecessors(task_name).select(&:running).empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.all_predecessors_finished_successfully?(task_name)
|
82
|
+
get_predecessors(task_name).reduce(0) { |sum, t| sum + get_exit_status(t) }.zero?
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.predecessors_which_failed(task_name)
|
86
|
+
get_predecessors(task_name).select { |t| task_failed?(t) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.predecessors_which_havent_finished(task_name)
|
90
|
+
get_predecessors(task_name).reject { |t| task_finished?(t) }
|
91
|
+
end
|
92
|
+
|
93
|
+
# Lists all tasks without dependents
|
94
|
+
#
|
95
|
+
# @return [Array] a list of all tasks without dependents
|
96
|
+
def self.tasks_without_predecessors
|
97
|
+
list = API.task_graph.keys - API.task_graph.values.flatten
|
98
|
+
API.task_matrix.select { |t| list.include?(t.name) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if a task has failed status
|
102
|
+
#
|
103
|
+
# @return [Boolean, Integer] true if task has a failed status, false if not,
|
104
|
+
# StatusCodes::TASK_NOT_IN_TASK_MATRIX if task not found
|
105
|
+
def self.task_failed?(task)
|
106
|
+
idx = API.task_matrix.index { |t| t.name == task }
|
107
|
+
idx ? (API.task_matrix[idx].success_status == false) : StatusCodes::TASK_NOT_IN_TASK_MATRIX
|
108
|
+
end
|
109
|
+
|
110
|
+
# Check if a task is still tunning, at the time of checking
|
111
|
+
#
|
112
|
+
# @return [Boolean, Integer] true if task is running, false if not,
|
113
|
+
# StatusCodes::TASK_NOT_IN_TASK_MATRIX if task not found
|
114
|
+
def self.task_running?(task)
|
115
|
+
idx = API.task_matrix.index { |t| t.name == task }
|
116
|
+
idx ? API.task_matrix[idx].running : StatusCodes::TASK_NOT_IN_TASK_MATRIX
|
117
|
+
end
|
118
|
+
|
119
|
+
# Check if a task has finished running
|
120
|
+
#
|
121
|
+
# @return [Boolean, Integer] true if task has finished, false if not,
|
122
|
+
# StatusCodes::TASK_NOT_IN_TASK_MATRIX if task not found
|
123
|
+
def self.task_finished?(task)
|
124
|
+
idx = API.task_matrix.index { |t| t.name == task }
|
125
|
+
idx ? API.task_matrix[idx].exit_status : StatusCodes::TASK_NOT_IN_TASK_MATRIX
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.finished_tasks
|
129
|
+
API.task_matrix.map { |t| t.name if t.running == false }.compact.extend(Helper)
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.running_tasks
|
133
|
+
API.task_matrix.select(&:running)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Find task based on an attribute's value
|
137
|
+
#
|
138
|
+
# @note the methof will find the first matching task. If there are more than one matches,
|
139
|
+
# only the first one -in sequence order- will be returned
|
140
|
+
# @param [Symbol] attr_name
|
141
|
+
# @param [Object] attr_value could be a String, Integer, Boolean, etc.
|
142
|
+
# @yield [Jongleur::Task] the first task that matches the arguments
|
143
|
+
# @return [Jongleur::Task, nil] the first task that matches the arguments, nil if no matches are found
|
144
|
+
def self.find_task_by(attr_name, attr_value)
|
145
|
+
idx = API.task_matrix.index { |t| t.send(attr_name.to_s) == attr_value }
|
146
|
+
yield API.task_matrix[idx] if block_given? && idx
|
147
|
+
idx ? API.task_matrix[idx] : nil
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.each_descendant(task)
|
151
|
+
API.task_graph[task]&.each do |desc_task|
|
152
|
+
# check desc_task isn't already running and that its predecessors are finished
|
153
|
+
yield find_task_by(:name, desc_task) if !task_running?(desc_task) &&
|
154
|
+
finished_tasks.contains_array?(get_predecessors(desc_task))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Parses a line of program output
|
159
|
+
#
|
160
|
+
# @param [String] a line of program output
|
161
|
+
# @return [Hash] the output line in a key-value format
|
162
|
+
def self.parse_line(line)
|
163
|
+
res = {}
|
164
|
+
msg_arr = []
|
165
|
+
msg_arr = line.split(',') if line&.match(/^finished task/)
|
166
|
+
msg_arr.each do |x|
|
167
|
+
h = {}
|
168
|
+
s = x.split(':')
|
169
|
+
h[s.at(0).strip] = s.at(1).strip
|
170
|
+
res.merge!(h)
|
171
|
+
end
|
172
|
+
res
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
# Parses a multi-line string of program output
|
177
|
+
#
|
178
|
+
# @param [StringIO] the standard output as a string
|
179
|
+
# @param [Boolean] print output to stdout
|
180
|
+
# @return [Array<Hash>] a list of hashes representing the std output
|
181
|
+
def self.parse_output(string_io, print_to_stdout = false)
|
182
|
+
parsed = []
|
183
|
+
string_io.each_line do |line|
|
184
|
+
STDOUT.puts ">>> #{line}" if print_to_stdout
|
185
|
+
line_as_hash = parse_line(line)
|
186
|
+
parsed << line_as_hash unless line_as_hash.empty?
|
187
|
+
end
|
188
|
+
parsed
|
189
|
+
end
|
190
|
+
|
191
|
+
# run all descendant tasks of given task
|
192
|
+
def self.run_descendants(task_name)
|
193
|
+
each_descendant(task_name) do |t|
|
194
|
+
waiting = predecessors_which_havent_finished(t.name)
|
195
|
+
failed = predecessors_which_failed(t.name)
|
196
|
+
|
197
|
+
if waiting.empty? && failed.empty?
|
198
|
+
t.running = true
|
199
|
+
Implementation.process_message "starting task #{t.name}"
|
200
|
+
t.pid = fork { API.const_get(t.name).new(predecessors: get_predecessors(t.name)).execute }
|
201
|
+
elsif !failed.empty?
|
202
|
+
process_message "cannot start #{t.name} because its predecessor #{failed.first} failed to finish"
|
203
|
+
elsif !waiting.empty?
|
204
|
+
process_message "cannot start #{t.name} because its predecessor #{waiting.first} hasn't finished yet"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.process_message(a_msg)
|
210
|
+
puts(a_msg)
|
211
|
+
end
|
212
|
+
|
213
|
+
end # module
|
214
|
+
end # module
|
215
|
+
|
216
|
+
# rubocop:enable Lint/AssignmentInCondition
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jongleur
|
4
|
+
# This is a Base class for all task classes executed by Jongleur.
|
5
|
+
# Every class declared and used in Jongleur must inherit from <WorkerTask>
|
6
|
+
class WorkerTask
|
7
|
+
def initialize(**other_args)
|
8
|
+
other_args.each do |key, val|
|
9
|
+
var_name = "@#{key}"
|
10
|
+
instance_variable_set(var_name, val)
|
11
|
+
self.class.send(:attr_accessor, key.to_s)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# returns the task description
|
17
|
+
class << self
|
18
|
+
attr_reader :desc
|
19
|
+
end
|
20
|
+
end # class
|
metadata
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jongleur
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fred Heath
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-08-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graphviz
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: os
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.16'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.16'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry-byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.58'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.58'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.9'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.9'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: yard
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.9'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.9'
|
139
|
+
description: Acceps a number of inter-dependent tasks and runs them as separate processes,
|
140
|
+
parallelising where possible.
|
141
|
+
email:
|
142
|
+
- fred@bootstrap.me.uk
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".gitignore"
|
148
|
+
- ".gitlab-ci.yml"
|
149
|
+
- ".rspec"
|
150
|
+
- ".rubocop.yml"
|
151
|
+
- CHANGELOG.md
|
152
|
+
- Gemfile
|
153
|
+
- LICENSE.txt
|
154
|
+
- README.md
|
155
|
+
- Rakefile
|
156
|
+
- bin/console
|
157
|
+
- bin/img/DAG_graph_1.png
|
158
|
+
- bin/img/ETL_DAG.png
|
159
|
+
- bin/img/jongleur_m-2015.jpg
|
160
|
+
- bin/img/transactional_DAG.png
|
161
|
+
- bin/setup
|
162
|
+
- jongleur.gemspec
|
163
|
+
- lib/jongleur.rb
|
164
|
+
- lib/jongleur/api.rb
|
165
|
+
- lib/jongleur/helpers.rb
|
166
|
+
- lib/jongleur/implementation.rb
|
167
|
+
- lib/jongleur/version.rb
|
168
|
+
- lib/jongleur/worker_task.rb
|
169
|
+
homepage: http://www.bootstrap.me.uk
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.6.14
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: A task scheduler manager for DAG-style task groups.
|
193
|
+
test_files: []
|