taskflow-ar 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +158 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/generators/taskflow/taskflow_generator.rb +25 -0
- data/lib/generators/templates/create_taskflow_flows.rb +18 -0
- data/lib/generators/templates/create_taskflow_loggers.rb +11 -0
- data/lib/generators/templates/create_taskflow_records.rb +13 -0
- data/lib/generators/templates/create_taskflow_relation.rb +10 -0
- data/lib/generators/templates/create_taskflow_tasks.rb +19 -0
- data/lib/taskflow/custom_hash.rb +16 -0
- data/lib/taskflow/flow.rb +126 -0
- data/lib/taskflow/logger.rb +52 -0
- data/lib/taskflow/record.rb +13 -0
- data/lib/taskflow/task.rb +91 -0
- data/lib/taskflow/version.rb +3 -0
- data/lib/taskflow/worker.rb +92 -0
- data/lib/taskflow.rb +14 -0
- data/taskflow.gemspec +35 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a33ff609e69514be13b1a44c437dc07c326df59b
|
4
|
+
data.tar.gz: 8a24e0a9757af8093448ba9eca9cdf86c27c8206
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dd77850cf8b06856229b1b5d474ebeaf04dd44156fcb911c4604ee4f49377da31dd101cb6df3fe54e89abc2998fe80eb07373440d1329590d285a631d0bc5247
|
7
|
+
data.tar.gz: d34dbd51ae6db1ba05b9b73cc3446e6b317392e42ddf3ec227063ad59a01173ef5ab39ff6a78e001270314f710df033010b66baceec2ddc1b92a1067984c8f69
|
data/.gitignore
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 qujianping
|
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,158 @@
|
|
1
|
+
# Taskflow
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/taskflow`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'taskflow-ar',:require=>'taskflow'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install taskflow-ar
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
example:
|
26
|
+
```ruby
|
27
|
+
class PlayFlow < Taskflow::Flow
|
28
|
+
NAME = "Play FLow"
|
29
|
+
def configure
|
30
|
+
t1 = run PendingTask, params: input, name: 'pending-task'
|
31
|
+
t2 = run OkTask,after: t1,name: 'ok-task'
|
32
|
+
t3 = run AlwaysFailTask,after: t1,name: 'always-fail-task'
|
33
|
+
t4 = run RetryPassTask,after: t1,name: 'retry-pass-task'
|
34
|
+
t5 = run OkTask,after: t4,name: 'just-play-task'
|
35
|
+
run SummaryTask,after: [t2,t3,t5],name: 'summary-task'
|
36
|
+
run OkTask,params: {love: 3},name: 'finished-task'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
class PendingTask < Taskflow::Task
|
40
|
+
def go(logger)
|
41
|
+
logger.info "I got input paramter: #{input}"
|
42
|
+
logger.info 'first step,then suspend'
|
43
|
+
if data[:who]
|
44
|
+
logger.info "cool, #{data[:who]} wake me up!"
|
45
|
+
tflogger.info 'Pending task wake up'
|
46
|
+
set_output :reason=>'you are cool'
|
47
|
+
else
|
48
|
+
logger.info 'I would suspended now, wake for your wakeup.'
|
49
|
+
suspend
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
class OkTask < Taskflow::Task
|
54
|
+
def go(logger)
|
55
|
+
set_output "result"=>(rand 10)
|
56
|
+
logger.info "#{self.name} finished"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
class AlwaysFailTask < Taskflow::Task
|
60
|
+
def go(logger)
|
61
|
+
logger.info 'I would always fail, pls skip me'
|
62
|
+
raise 'Ops, always fail!!!'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
class SummaryTask < Taskflow::Task
|
66
|
+
def go(logger)
|
67
|
+
logger.info 'get upstream output'
|
68
|
+
upstream.each do |task|
|
69
|
+
logger.info "Upstream task[#{task.name}]: #{task.output}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class RetryPassTask < Taskflow::Task
|
75
|
+
def go(logger)
|
76
|
+
if data.empty?
|
77
|
+
set_data :success_next_time=>true
|
78
|
+
raise 'fail, please retry'
|
79
|
+
else
|
80
|
+
logger.info 'second time ok'
|
81
|
+
end
|
82
|
+
logger.info 'retry succeed'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
Then schedule taskflow like below:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
f=Taskflow::Flow.launch 'PlayFlow',:params=>{word: 'hello'},:launched_by=>'Jason',:workflow_description=>'desc'
|
90
|
+
# find PendingTask
|
91
|
+
t=f.tasks.where(state: 'paused',result: 'suspend').first
|
92
|
+
t.wakeup :who=>'Tom'
|
93
|
+
# find AlwaysFailTask
|
94
|
+
t=f.tasks.find_by name: 'always-fail-task'
|
95
|
+
puts t.error
|
96
|
+
# {"class"=>"RuntimeError", "message"=>"Ops, always fail!!!", "backtrace"=>["/U..."]}
|
97
|
+
t.skip
|
98
|
+
t=f.tasks.find_by name: 'retry-pass-task'
|
99
|
+
t.resume
|
100
|
+
# wait for while
|
101
|
+
puts f.state # => stopped
|
102
|
+
# and we can check the log of taskflow itself
|
103
|
+
puts f.logger.records
|
104
|
+
|
105
|
+
# all sidekiq log
|
106
|
+
Taskflow::Worker JID-905f46ac2a14b79329cc2526 INFO: start
|
107
|
+
Taskflow::Worker JID-905f46ac2a14b79329cc2526 INFO: I got input paramter: {"word"=>"hello"}
|
108
|
+
Taskflow::Worker JID-905f46ac2a14b79329cc2526 INFO: first step,then suspend
|
109
|
+
Taskflow::Worker JID-905f46ac2a14b79329cc2526 INFO: I would suspended now, wake for your wakeup.
|
110
|
+
Taskflow::Worker JID-905f46ac2a14b79329cc2526 INFO: done: 0.034 sec
|
111
|
+
Taskflow::Worker JID-cdcba34bc5f4746d0f0b68ad INFO: start
|
112
|
+
Taskflow::Worker JID-cdcba34bc5f4746d0f0b68ad INFO: I got input paramter: {"word"=>"hello"}
|
113
|
+
Taskflow::Worker JID-cdcba34bc5f4746d0f0b68ad INFO: first step,then suspend
|
114
|
+
Taskflow::Worker JID-cdcba34bc5f4746d0f0b68ad INFO: cool, Tom wake me up!
|
115
|
+
Taskflow::Worker JID-cdcba34bc5f4746d0f0b68ad INFO: done: 0.059 sec
|
116
|
+
Taskflow::Worker JID-2167fefe864f5de18ca7341e INFO: start
|
117
|
+
Taskflow::Worker JID-f1131d60a2d7530953f346ec INFO: start
|
118
|
+
Taskflow::Worker JID-259ef2694a65e235cf010b1e INFO: start
|
119
|
+
Taskflow::Worker JID-259ef2694a65e235cf010b1e INFO: I would always fail, pls skip me
|
120
|
+
Taskflow::Worker JID-2167fefe864f5de18ca7341e INFO: ok-task finished
|
121
|
+
Taskflow::Worker JID-259ef2694a65e235cf010b1e INFO: done: 0.077 sec
|
122
|
+
Taskflow::Worker JID-2167fefe864f5de18ca7341e INFO: done: 0.083 sec
|
123
|
+
Taskflow::Worker JID-f1131d60a2d7530953f346ec INFO: done: 0.084 sec
|
124
|
+
Taskflow::Worker JID-2d4f24491b84a68334cebaab INFO: start
|
125
|
+
Taskflow::Worker JID-2d4f24491b84a68334cebaab INFO: done: 0.022 sec
|
126
|
+
Taskflow::Worker JID-6312d5b0e1c66602bf04372e INFO: start
|
127
|
+
Taskflow::Worker JID-6312d5b0e1c66602bf04372e INFO: second time ok
|
128
|
+
Taskflow::Worker JID-6312d5b0e1c66602bf04372e INFO: retry succeed
|
129
|
+
Taskflow::Worker JID-6312d5b0e1c66602bf04372e INFO: done: 0.027 sec
|
130
|
+
Taskflow::Worker JID-dda69c567c7009219f6237b6 INFO: start
|
131
|
+
Taskflow::Worker JID-dda69c567c7009219f6237b6 INFO: just-play-task finished
|
132
|
+
Taskflow::Worker JID-dda69c567c7009219f6237b6 INFO: done: 0.032 sec
|
133
|
+
Taskflow::Worker JID-7163d1fa16a685d016642a6b INFO: start
|
134
|
+
Taskflow::Worker JID-7163d1fa16a685d016642a6b INFO: get upstream output
|
135
|
+
Taskflow::Worker JID-7163d1fa16a685d016642a6b INFO: Upstream task[ok-task]: {"result"=>4}
|
136
|
+
Taskflow::Worker JID-7163d1fa16a685d016642a6b INFO: Upstream task[always-fail-task]: {}
|
137
|
+
Taskflow::Worker JID-7163d1fa16a685d016642a6b INFO: Upstream task[just-play-task]: {"result"=>0}
|
138
|
+
Taskflow::Worker JID-7163d1fa16a685d016642a6b INFO: done: 0.03 sec
|
139
|
+
Taskflow::Worker JID-d7d0c92da5ab820bc1f66651 INFO: start
|
140
|
+
Taskflow::Worker JID-d7d0c92da5ab820bc1f66651 INFO: finished-task finished
|
141
|
+
Taskflow::Worker JID-d7d0c92da5ab820bc1f66651 INFO: done: 0.021 sec
|
142
|
+
```
|
143
|
+
|
144
|
+
## Development
|
145
|
+
|
146
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
147
|
+
|
148
|
+
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).
|
149
|
+
|
150
|
+
## Contributing
|
151
|
+
|
152
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/taskflow. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
153
|
+
|
154
|
+
|
155
|
+
## License
|
156
|
+
|
157
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
158
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "taskflow"
|
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
|
data/bin/setup
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators/named_base'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module Taskflow
|
5
|
+
|
6
|
+
class TaskflowGenerator < ActiveRecord::Generators::Base
|
7
|
+
|
8
|
+
include Rails::Generators::ResourceHelpers
|
9
|
+
|
10
|
+
namespace "taskflow"
|
11
|
+
|
12
|
+
desc "Creates Taskflow Migrations"
|
13
|
+
|
14
|
+
source_root File.expand_path("../../templates", __FILE__)
|
15
|
+
|
16
|
+
def migration
|
17
|
+
migration_template "create_taskflow_flows.rb", "db/migrate/create_taskflow_flows.rb"
|
18
|
+
migration_template "create_taskflow_tasks.rb", "db/migrate/create_taskflow_tasks.rb"
|
19
|
+
migration_template "create_taskflow_loggers.rb", "db/migrate/create_taskflow_loggers.rb"
|
20
|
+
migration_template "create_taskflow_records.rb", "db/migrate/create_taskflow_records.rb"
|
21
|
+
migration_template "create_taskflow_relation.rb", "db/migrate/create_taskflow_relation.rb"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateTaskflowFlows < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :taskflow_flows do |t|
|
4
|
+
t.string :name
|
5
|
+
t.string :klass
|
6
|
+
t.string :state
|
7
|
+
t.string :category
|
8
|
+
t.string :result
|
9
|
+
t.string :launched_by
|
10
|
+
t.string :halt_by
|
11
|
+
t.text :input
|
12
|
+
t.float :progress
|
13
|
+
t.datetime :started_at
|
14
|
+
t.datetime :ended_at
|
15
|
+
t.text :next_config
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateTaskflowRecords < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :taskflow_records do |t|
|
4
|
+
t.integer :step_id
|
5
|
+
t.string :writer
|
6
|
+
t.string :level
|
7
|
+
t.string :content
|
8
|
+
t.text :tags
|
9
|
+
t.datetime :written_at
|
10
|
+
t.integer :logger_id
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateTaskflowTasks < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :taskflow_tasks do |t|
|
4
|
+
t.integer :index, default: 1
|
5
|
+
t.string :name
|
6
|
+
t.string :klass
|
7
|
+
t.string :state
|
8
|
+
t.string :result
|
9
|
+
t.text :input
|
10
|
+
t.text :output
|
11
|
+
t.text :data
|
12
|
+
t.float :progress, default: 0
|
13
|
+
t.datetime :started_at
|
14
|
+
t.datetime :ended_at
|
15
|
+
t.text :error
|
16
|
+
t.integer :flow_id
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Taskflow::CustomHash
|
2
|
+
class << self
|
3
|
+
def load(str)
|
4
|
+
return unless str
|
5
|
+
HashWithIndifferentAccess.new JSON.parse(str)
|
6
|
+
end
|
7
|
+
def dump(obj)
|
8
|
+
return unless obj
|
9
|
+
unless obj.is_a?(HashWithIndifferentAccess) || obj.is_a?(Hash)
|
10
|
+
raise ::ActiveRecord::SerializationTypeMismatch,
|
11
|
+
"Attribute was supposed to be a Hash, but was a #{obj.class}. -- #{obj.inspect}"
|
12
|
+
end
|
13
|
+
JSON.dump obj
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
class Taskflow::Flow < ActiveRecord::Base
|
3
|
+
|
4
|
+
serialize :input, Taskflow::CustomHash
|
5
|
+
serialize :next_config, Taskflow::CustomHash
|
6
|
+
before_save :set_default_property
|
7
|
+
|
8
|
+
after_create :configure_tasks
|
9
|
+
|
10
|
+
has_many :tasks, :class_name=>'Taskflow::Task',:inverse_of=>:flow,:dependent => :destroy
|
11
|
+
has_one :tflogger,:class_name=>'Taskflow::Logger',:inverse_of=>:flow, :dependent => :destroy
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
# opts support :params
|
16
|
+
def can_launch?(klass,opts={})
|
17
|
+
opts = HashWithIndifferentAccess.new opts
|
18
|
+
!Taskflow::Flow.where.not(state: 'stopped').where(klass: klass,input: opts[:params]).exists?
|
19
|
+
end
|
20
|
+
|
21
|
+
def launch(klass,opts={})
|
22
|
+
opts = HashWithIndifferentAccess.new opts
|
23
|
+
flow_klass = Kernel.const_get klass
|
24
|
+
name = flow_klass.const_get 'NAME'
|
25
|
+
opts[:launched_by] ||= 'task-flow-engine'
|
26
|
+
flow = flow_klass.create name: name,input: opts[:params],launched_by: opts[:launched_by]
|
27
|
+
if opts[:next_workflow_config]
|
28
|
+
flow.update next_config: opts[:next_workflow_config]
|
29
|
+
end
|
30
|
+
flow.create_tflogger name: name,description: opts[:workflow_description]
|
31
|
+
flow.schedule
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def running_steps
|
36
|
+
self.tasks.where(state: ['running','paused'])
|
37
|
+
end
|
38
|
+
|
39
|
+
# opts support :name,:params
|
40
|
+
def run(klass,opts={})
|
41
|
+
obj = {
|
42
|
+
klass: klass.to_s,
|
43
|
+
name: opts[:name] || klass.to_s,
|
44
|
+
input: opts[:params],
|
45
|
+
index: self.tasks.size + 1
|
46
|
+
}
|
47
|
+
task = klass.create obj.select{|k,v| v }
|
48
|
+
if opts[:before]
|
49
|
+
task.downstream << opts[:before]
|
50
|
+
if opts[:before].is_a? Array
|
51
|
+
opts[:before].each{|b| b.upstream << task}
|
52
|
+
else
|
53
|
+
opts[:before].upstream << task
|
54
|
+
end
|
55
|
+
end
|
56
|
+
if opts[:after]
|
57
|
+
task.upstream << opts[:after]
|
58
|
+
if opts[:after].is_a? Array
|
59
|
+
opts[:after].each{|d| d.downstream << task }
|
60
|
+
else
|
61
|
+
opts[:after].downstream << task
|
62
|
+
end
|
63
|
+
end
|
64
|
+
if opts[:before].nil? && opts[:after].nil? && self.tasks.last
|
65
|
+
self.tasks.last.downstream << task
|
66
|
+
task.upstream << self.tasks.last
|
67
|
+
end
|
68
|
+
self.tasks << task
|
69
|
+
task
|
70
|
+
end
|
71
|
+
|
72
|
+
def stop!(user_id=nil)
|
73
|
+
percent = self.tasks.map(&:progress).sum / self.tasks.size
|
74
|
+
self.update_attributes! progress: percent,halt_by: user_id,ended_at: Time.now, state: 'stopped',result: 'warning'
|
75
|
+
end
|
76
|
+
|
77
|
+
def resume
|
78
|
+
self.tasks.where(state: 'paused',result: 'error').each do |task|
|
79
|
+
task.resume
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def schedule
|
84
|
+
return if self.halt_by || self.state == 'stopped'
|
85
|
+
self.update_attributes! state: 'running',started_at: Time.now if self.state == 'pending'
|
86
|
+
task_list = []
|
87
|
+
self.reload.tasks.where(state: 'pending').each do |task|
|
88
|
+
# 上游全部完成
|
89
|
+
if task.upstream.empty? || task.upstream.all?{|t| %w(skipped stopped).include? t.state }
|
90
|
+
task_list << task.id.to_s
|
91
|
+
end
|
92
|
+
end
|
93
|
+
task_list.each{|tid| Taskflow::Worker.perform_async self.id.to_s,tid }
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def configure_tasks
|
99
|
+
begin
|
100
|
+
configure
|
101
|
+
sort_index 1,[]
|
102
|
+
rescue=>exception
|
103
|
+
self.destroy
|
104
|
+
raise exception
|
105
|
+
end
|
106
|
+
reload
|
107
|
+
end
|
108
|
+
|
109
|
+
def sort_index(i,scanned)
|
110
|
+
queue = self.tasks.where.not(id: scanned).select{|t| t.upstream.empty? || t.upstream.all?{|upt| scanned.include?(upt.id.to_s)}}
|
111
|
+
return if queue.empty?
|
112
|
+
queue.each do |task|
|
113
|
+
task.update_attributes index: i
|
114
|
+
scanned << task.id.to_s
|
115
|
+
end
|
116
|
+
sort_index i + 1,scanned
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_default_property
|
120
|
+
self.klass ||= self.class.to_s
|
121
|
+
self.state ||= 'pending'
|
122
|
+
self.category ||= 'simple'
|
123
|
+
self.input ||= {}
|
124
|
+
self.progress ||= 0
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Taskflow::Logger < ActiveRecord::Base
|
2
|
+
|
3
|
+
belongs_to :flow, :class_name=>'Taskflow::Flow',:inverse_of=>:tflogger
|
4
|
+
|
5
|
+
has_many :records,:class_name=>'Taskflow::Record',:inverse_of=>:tflogger
|
6
|
+
|
7
|
+
before_save :set_default_property
|
8
|
+
def log(content,options={})
|
9
|
+
raise 'Need step id to write a log' if options[:step_id].nil? && @step_id.nil?
|
10
|
+
options[:step_id] ||= @step_id
|
11
|
+
options[:writer] ||= @writer
|
12
|
+
@step_id ||= options[:step_id]
|
13
|
+
@writer ||= options[:writer]
|
14
|
+
options.merge! :content=>content
|
15
|
+
record = self.records.last
|
16
|
+
if record && options.all?{|k,v| record.send(k) == v }
|
17
|
+
record.update_attributes! written_at: Time.now
|
18
|
+
else
|
19
|
+
self.records.create options
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def info(content,options={})
|
24
|
+
options.merge!(:level=>'INFO')
|
25
|
+
self.log content,options
|
26
|
+
end
|
27
|
+
|
28
|
+
def error(content,options={})
|
29
|
+
options.merge!(:level=>'ERROR')
|
30
|
+
self.log content,options
|
31
|
+
end
|
32
|
+
|
33
|
+
def fatal(content,options={})
|
34
|
+
options.merge!(:level=>'FATAL')
|
35
|
+
self.log content,options
|
36
|
+
end
|
37
|
+
|
38
|
+
def warning(content,options={})
|
39
|
+
options.merge!(:level=>'WARNING')
|
40
|
+
self.log content,options
|
41
|
+
end
|
42
|
+
|
43
|
+
def debug(content,options={})
|
44
|
+
options.merge!(:level=>'DEBUG')
|
45
|
+
self.log content,options
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def set_default_property
|
50
|
+
self.created_at ||= Time.now
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Taskflow::Record < ActiveRecord::Base
|
2
|
+
|
3
|
+
belongs_to :tflogger,:class_name=>'Taskflow::Logger',:inverse_of=>:records
|
4
|
+
serialize :tags, Taskflow::CustomHash
|
5
|
+
|
6
|
+
before_save :set_default_property
|
7
|
+
|
8
|
+
private
|
9
|
+
def set_default_property
|
10
|
+
self.tags ||= {}
|
11
|
+
self.written_at ||= Time.now
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
class Taskflow::Task < ActiveRecord::Base
|
3
|
+
# do not save myself in up or downstream
|
4
|
+
before_save :remove_self_in_stream
|
5
|
+
|
6
|
+
serialize :data, Taskflow::CustomHash
|
7
|
+
serialize :input, Taskflow::CustomHash
|
8
|
+
serialize :output, Taskflow::CustomHash
|
9
|
+
serialize :error, Taskflow::CustomHash
|
10
|
+
|
11
|
+
|
12
|
+
before_save :set_default_property
|
13
|
+
has_and_belongs_to_many :downstream, :class_name=>'Taskflow::Task',:inverse_of=>:upstream,:join_table=> :taskflow_relation,:foreign_key=> 'upstream_id'
|
14
|
+
has_and_belongs_to_many :upstream, :class_name=>'Taskflow::Task',:inverse_of=>:downstream,:join_table=> :taskflow_relation,:foreign_key=> 'downstream_id'
|
15
|
+
|
16
|
+
belongs_to :flow,:class_name=>'Taskflow::Flow',:inverse_of=>:tasks
|
17
|
+
|
18
|
+
def go(sidekiq_logger)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def resume
|
23
|
+
if self.state == 'paused' && self.result == 'error'
|
24
|
+
self.flow.update_attributes! state: 'running'
|
25
|
+
Taskflow::Worker.perform_async self.flow.id.to_s,self.id.to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def wakeup(arguments={})
|
30
|
+
self.reload
|
31
|
+
if self.state == 'paused' && self.result == 'suspend'
|
32
|
+
self.data = self.data.merge arguments
|
33
|
+
self.result = nil
|
34
|
+
self.save
|
35
|
+
Taskflow::Worker.perform_async self.flow.id.to_s,self.id.to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def skip
|
40
|
+
self.reload
|
41
|
+
if self.state == 'paused'
|
42
|
+
self.update_attributes! state: 'skipped'
|
43
|
+
Taskflow::Worker.perform_async self.flow.id.to_s,self.id.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def remove_self_in_stream
|
50
|
+
downstream.delete self if downstream.include? self
|
51
|
+
upstream.delete self if upstream.include? self
|
52
|
+
end
|
53
|
+
|
54
|
+
def suspend
|
55
|
+
throw :control,:suspend
|
56
|
+
end
|
57
|
+
|
58
|
+
def tflogger
|
59
|
+
@tflogger ||= (
|
60
|
+
_logger = flow.tflogger
|
61
|
+
_logger.instance_variable_set '@step_id',self.index
|
62
|
+
_logger.instance_variable_set '@writer',self.name
|
63
|
+
_logger
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def method_missing(name,*args)
|
68
|
+
if /^(set|append|clear)_(input|output|data)$/ =~ name.to_s
|
69
|
+
act,fd = name.to_s.split '_'
|
70
|
+
if act == 'set'
|
71
|
+
return false unless args.first
|
72
|
+
self.update_attributes! "#{fd}"=>args.first
|
73
|
+
elsif act == 'append'
|
74
|
+
return false unless args.first
|
75
|
+
self.update_attributes! "#{fd}"=>self.send("#{fd}").merge(args.first)
|
76
|
+
else
|
77
|
+
self.update_attributes! "#{fd}"=>{}
|
78
|
+
end
|
79
|
+
else
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_default_property
|
85
|
+
self.klass ||= self.class.to_s
|
86
|
+
self.state ||= 'pending'
|
87
|
+
self.input ||= {}
|
88
|
+
self.output ||= {}
|
89
|
+
self.data ||= {}
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
class Taskflow::Worker
|
3
|
+
include ::Sidekiq::Worker
|
4
|
+
sidekiq_options :retry => false
|
5
|
+
|
6
|
+
def perform(task_flow_id,job_id,opts={})
|
7
|
+
flow = Taskflow::Flow.find task_flow_id
|
8
|
+
task = Taskflow::Task.find job_id
|
9
|
+
# reload task with its type
|
10
|
+
task = Kernel.const_get(task.klass).find job_id
|
11
|
+
begin
|
12
|
+
reason = catch :control do
|
13
|
+
check_flow_state flow
|
14
|
+
check_task_state task
|
15
|
+
task.update_attributes! state: 'running',started_at: Time.now,ended_at: nil, progress: 0.5,output: {},error: nil,result: nil
|
16
|
+
task.go logger
|
17
|
+
end
|
18
|
+
case reason
|
19
|
+
when :flow_halt
|
20
|
+
flow.update_attributes! ended_at: Time.now unless flow.ended_at
|
21
|
+
when :suspend
|
22
|
+
task.update_attributes! result: 'suspend',state: 'paused'
|
23
|
+
when :skip
|
24
|
+
task.update_attributes! state: 'skipped',data: {}
|
25
|
+
when :already_running,:already_stopped
|
26
|
+
return
|
27
|
+
else
|
28
|
+
task.update_attributes! data: {},ended_at: Time.now,progress: 1,state: 'stopped',result: 'success'
|
29
|
+
end
|
30
|
+
rescue=>exception
|
31
|
+
task.error = {
|
32
|
+
class: exception.class.to_s,
|
33
|
+
message: exception.to_s,
|
34
|
+
backtrace: exception.backtrace
|
35
|
+
}
|
36
|
+
task.state = 'paused'
|
37
|
+
task.result = 'error'
|
38
|
+
task.ended_at = Time.now
|
39
|
+
task.save
|
40
|
+
end
|
41
|
+
update_flow flow.reload
|
42
|
+
flow.schedule
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def check_flow_state(flow)
|
47
|
+
if flow.state == 'stopped' || flow.halt_by
|
48
|
+
throw :control, :flow_halt
|
49
|
+
end
|
50
|
+
end
|
51
|
+
def check_task_state(task)
|
52
|
+
case task.state
|
53
|
+
when 'pending'
|
54
|
+
task.update_attributes state: 'running'
|
55
|
+
when 'running'
|
56
|
+
throw :control, :already_running
|
57
|
+
when 'paused'
|
58
|
+
throw :control, :suspend if task.result == 'suspend'
|
59
|
+
when 'stopped'
|
60
|
+
throw :control, :already_stopped
|
61
|
+
when 'skipped'
|
62
|
+
throw :control,:skip
|
63
|
+
else
|
64
|
+
raise "Unkown task state #{task.state}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_flow(flow)
|
69
|
+
return if flow.halt_by || flow.state == 'stopped'
|
70
|
+
flow.progress = flow.tasks.map(&:progress).sum / flow.tasks.size
|
71
|
+
if flow.halt_by
|
72
|
+
flow.state = 'stopped'
|
73
|
+
elsif flow.tasks.all?{|t| %w(stopped skipped).include? t.state }
|
74
|
+
flow.state = 'stopped'
|
75
|
+
elsif flow.tasks.any?{|t| t.state == 'paused' }
|
76
|
+
flow.state = 'paused'
|
77
|
+
flow.result = flow.tasks.find_by(state: 'paused').result
|
78
|
+
else
|
79
|
+
flow.state = 'running'
|
80
|
+
end
|
81
|
+
if flow.state == 'stopped'
|
82
|
+
flow.result = flow.tasks.all?{|t| t.result == 'success' } ? 'success' : 'warning'
|
83
|
+
flow.ended_at = Time.now
|
84
|
+
if flow.next_config
|
85
|
+
logger.info "Auto boot next flow, #{flow.next_config}"
|
86
|
+
Taskflow::Flow.launch flow.next_config[:name],flow.next_config[:config]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
flow.save
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
data/lib/taskflow.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "taskflow/version"
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
require 'taskflow/custom_hash'
|
4
|
+
require 'taskflow/flow'
|
5
|
+
require 'taskflow/task'
|
6
|
+
require 'taskflow/worker'
|
7
|
+
require 'taskflow/logger'
|
8
|
+
require 'taskflow/record'
|
9
|
+
|
10
|
+
module Taskflow
|
11
|
+
def self.table_name_prefix
|
12
|
+
'taskflow_'
|
13
|
+
end
|
14
|
+
end
|
data/taskflow.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'taskflow/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "taskflow-ar"
|
8
|
+
spec.version = Taskflow::VERSION
|
9
|
+
spec.authors = ["qujianping"]
|
10
|
+
spec.email = ["qjpcpu@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Easy task flow based on sidekiq.}
|
13
|
+
spec.description = %q{Easy task flow based on sidekiq.}
|
14
|
+
spec.homepage = "https://github.com/qjpcpu/taskflow-ar"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_runtime_dependency "sidekiq","~> 3.3"
|
33
|
+
spec.add_runtime_dependency "activesupport","~> 4.2"
|
34
|
+
spec.add_runtime_dependency "activerecord","~> 4.2"
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: taskflow-ar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- qujianping
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sidekiq
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.2'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.2'
|
83
|
+
description: Easy task flow based on sidekiq.
|
84
|
+
email:
|
85
|
+
- qjpcpu@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- CODE_OF_CONDUCT.md
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- bin/console
|
97
|
+
- bin/setup
|
98
|
+
- lib/generators/taskflow/taskflow_generator.rb
|
99
|
+
- lib/generators/templates/create_taskflow_flows.rb
|
100
|
+
- lib/generators/templates/create_taskflow_loggers.rb
|
101
|
+
- lib/generators/templates/create_taskflow_records.rb
|
102
|
+
- lib/generators/templates/create_taskflow_relation.rb
|
103
|
+
- lib/generators/templates/create_taskflow_tasks.rb
|
104
|
+
- lib/taskflow.rb
|
105
|
+
- lib/taskflow/custom_hash.rb
|
106
|
+
- lib/taskflow/flow.rb
|
107
|
+
- lib/taskflow/logger.rb
|
108
|
+
- lib/taskflow/record.rb
|
109
|
+
- lib/taskflow/task.rb
|
110
|
+
- lib/taskflow/version.rb
|
111
|
+
- lib/taskflow/worker.rb
|
112
|
+
- taskflow.gemspec
|
113
|
+
homepage: https://github.com/qjpcpu/taskflow-ar
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata:
|
117
|
+
allowed_push_host: https://rubygems.org
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 2.2.2
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: Easy task flow based on sidekiq.
|
138
|
+
test_files: []
|