patriot-workflow-scheduler 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +8 -8
  2. data/lib/patriot.rb +1 -0
  3. data/lib/patriot/command.rb +11 -11
  4. data/lib/patriot/command/base.rb +3 -3
  5. data/lib/patriot/command/composite.rb +20 -6
  6. data/lib/patriot/command/post_processor.rb +2 -2
  7. data/lib/patriot/command/post_processor/base.rb +3 -2
  8. data/lib/patriot/command/post_processor/mail_notification.rb +3 -3
  9. data/lib/patriot/command/post_processor/retrial.rb +3 -3
  10. data/lib/patriot/command/sh_command.rb +14 -0
  11. data/lib/patriot/controller/worker_admin_controller.rb +13 -8
  12. data/lib/patriot/job_store/base.rb +16 -7
  13. data/lib/patriot/job_store/in_memory_store.rb +121 -29
  14. data/lib/patriot/job_store/job.rb +7 -7
  15. data/lib/patriot/job_store/job_ticket.rb +1 -0
  16. data/lib/patriot/job_store/rdb_job_store.rb +161 -54
  17. data/lib/patriot/tool/patriot_commands/execute.rb +1 -1
  18. data/lib/patriot/tool/patriot_commands/job.rb +6 -4
  19. data/lib/patriot/tool/patriot_commands/register.rb +7 -5
  20. data/lib/patriot/util/config.rb +14 -3
  21. data/lib/patriot/util/config/inifile_config.rb +1 -1
  22. data/lib/patriot/version.rb +3 -0
  23. data/lib/patriot/worker/base.rb +6 -3
  24. data/lib/patriot/worker/info_server.rb +34 -24
  25. data/lib/patriot/worker/servlet.rb +4 -8
  26. data/lib/patriot/worker/servlet/api_servlet_base.rb +40 -0
  27. data/lib/patriot/worker/servlet/index_servlet.rb +32 -0
  28. data/lib/patriot/worker/servlet/job_api_servlet.rb +156 -0
  29. data/lib/patriot/worker/servlet/worker_api_servlet.rb +67 -0
  30. data/skel/batch/sample/daily/test.pbc +1 -1
  31. data/skel/public/css/bootstrap.min.css +7412 -0
  32. data/skel/public/css/original.css +179 -54
  33. data/skel/public/js/patriot-workflow-scheduler-0.8.0.js +82252 -0
  34. data/skel/public/js/patriot-workflow-scheduler-0.8.0.min.js +26 -0
  35. data/skel/public/js/patriot-workflow-scheduler-0.8.0.min.js.map +1 -0
  36. data/skel/public/templates/_jobs.erb +4 -5
  37. data/skel/public/templates/job.erb +2 -4
  38. data/skel/public/templates/layout.erb +10 -10
  39. data/skel/public/views/index.erb +13 -0
  40. metadata +40 -4
  41. data/lib/patriot/worker/servlet/job_servlet.rb +0 -128
  42. data/lib/patriot/worker/servlet/worker_status_servlet.rb +0 -52
@@ -14,20 +14,31 @@ module Patriot
14
14
  # default plugins directory
15
15
  DEFAULT_PLUGIN_DIR = "plugins"
16
16
  # plugin directory
17
- PLUGIN_LIB_DIR = 'lib'
17
+ PLUGIN_LIB_DIR = 'lib'
18
18
  # plugin initiation script
19
19
  PLUGIN_INIT_SCRIPT = 'init.rb'
20
20
  # admin user mail address
21
21
  ADMIN_USER_KEY = 'admin_user'
22
22
 
23
+ # a comma separated list of hosts where workers are running
24
+ WORKER_HOST_KEY = 'worker_hosts'
25
+ # port number used by info server
26
+ INFO_SERVER_PORT_KEY = 'info_server_port'
27
+
28
+ # configuratio key for admin user name
29
+ USERNAME_KEY = 'info_server.admin.username'
30
+ # configuratio key for admin password
31
+ PASSWORD_KEY = 'info_server.admin.password'
32
+
33
+
23
34
  # load configuration file
24
35
  # @param option [Hash]
25
36
  # @option option :path [String] path to configuration file
26
37
  # @option option :type [String] load type (differe by tool)
27
38
  # @option option :ignore_plugin [Boolean] set true not to load plugins
28
39
  def load_config(option = {})
29
- option = {:path => DEFAULT_CONFIG,
30
- :type => nil,
40
+ option = {:path => DEFAULT_CONFIG,
41
+ :type => nil,
31
42
  :ignore_plugin => false }.merge(option)
32
43
  conf = nil
33
44
  case File.extname(option[:path])
@@ -5,7 +5,7 @@ module Patriot
5
5
  # a configuration implementation definied by the ini-file format
6
6
  class IniFileConfig < Patriot::Util::Config::Base
7
7
 
8
- # comman section name
8
+ # common section name
9
9
  COMMON_SECTION = 'common'
10
10
 
11
11
  # @param path [String] path to a configuration file
@@ -0,0 +1,3 @@
1
+ module Patriot
2
+ VERSION = "0.8.0"
3
+ end
@@ -28,12 +28,13 @@ module Patriot
28
28
  # @abstract
29
29
  # base class for worker implementations
30
30
  class Base
31
-
31
+
32
32
  include Patriot::Util::Logger
33
33
  include Patriot::Util::Retry
34
34
  include Patriot::JobStore::Factory
35
-
35
+
36
36
  attr_accessor :host, :status, :cycle, :job_store, :config
37
+ attr_reader :started_at
37
38
 
38
39
  # @param config [Patriot::Util::Config::Base]
39
40
  def initialize(config)
@@ -73,6 +74,7 @@ module Patriot
73
74
  job_ticket.exit_code = Patriot::Command::ExitCode::SUCCEEDED
74
75
  rescue Exception => e
75
76
  @logger.warn " job : #{job_ticket.job_id} failed"
77
+ @logger.warn e
76
78
  job_ticket.description = e.to_s
77
79
  else
78
80
  job_ticket.description = command.description
@@ -122,6 +124,7 @@ module Patriot
122
124
  File.open(pid_file, 'w') {|f| f.write($$)} # save pid for shutdown
123
125
  set_traps
124
126
  @info_server.start_server
127
+ @started_at = Time.now
125
128
  @logger.info "initiating worker #{@node}@#{@host}"
126
129
  init_worker
127
130
  @status = Patriot::Worker::Status::ACTIVE
@@ -136,7 +139,7 @@ module Patriot
136
139
 
137
140
  # should be overrided in sub class
138
141
  # This method is for implementation-specific configuration
139
- def init_worker
142
+ def init_worker
140
143
  raise NotImplementedError
141
144
  end
142
145
 
@@ -1,3 +1,6 @@
1
+ require 'thin'
2
+ require 'rack/rewrite'
3
+
1
4
  module Patriot
2
5
  module Worker
3
6
  # info server (web management console and for monitoring)
@@ -8,15 +11,11 @@ module Patriot
8
11
  # default port number
9
12
  DEFAULT_PORT = '36104'
10
13
 
11
- # urls used by this server
12
- URLS_KEY = 'info_server.urls'
13
- # mapping from the url to a servlet for the url
14
- URL_MAP_KEY_PREFIX = 'info_server.urlmap'
15
-
16
14
  # configuration key for rack handler used to start this server
17
15
  RACK_HANDLER_KEY = 'info_server.rack.handler'
18
16
  # default rack handler
19
- DEFAULT_RACK_HANDLER = 'Rack::Handler::WEBrick'
17
+ # DEFAULT_RACK_HANDLER = 'Rack::Handler::WEBrick'
18
+ DEFAULT_RACK_HANDLER = 'Rack::Handler::Thin'
20
19
 
21
20
  include Patriot::Util::Config
22
21
  include Patriot::Util::Logger
@@ -39,35 +38,41 @@ module Patriot
39
38
  @logger.info("port is not set. starting info server is skipped")
40
39
  return
41
40
  end
42
- @server_thread = Thread.new do
41
+ @server_thread = Thread.new do
43
42
  begin
44
43
  @handler = eval(@config.get(RACK_HANDLER_KEY, DEFAULT_RACK_HANDLER))
45
44
  app = Rack::URLMap.new(get_url_map)
46
45
  app = Rack::CommonLogger.new(app, build_access_logger)
47
- @handler.run app, {:Port => @port, :Host => '0.0.0.0'}
46
+ app = Rack::Rewrite.new(app){
47
+ # below 4 rules are for backward compatibility
48
+ r301 %r{^/jobs/?$}, "/"
49
+ rewrite %r{^/jobs/([^/]+)$}, "/api/v1/jobs/$1"
50
+ rewrite "/worker", "/api/v1/workers/this/state"
51
+ rewrite "/worker/status", "/api/v1/workers/this/state"
52
+ # for web console
53
+ rewrite %r{^(?!/api)}, "/"
54
+ }
55
+ app = Rack::Static.new(app, :urls => ["/js", "/css"], :root => File.join($home, "public"))
56
+ # TODO set options based on Handler type
57
+ @handler.run(app, {:Port => @port, :Host => '0.0.0.0', :signals => false}) do |server|
58
+ server.threaded = true
59
+ server.threadpool_size = 5
60
+ server.timeout = 60
61
+ end
48
62
  rescue => e
49
63
  @logger.error e
64
+ raise e
50
65
  end
51
66
  end
52
- @logger.info "info server has started"
67
+ @logger.info "info server has started with #{@handler.class}"
53
68
  return true
54
69
  end
55
70
 
56
71
  # @return [Hash<String, Sinatra::Base>]
57
72
  def get_url_map
58
- urls = @config.get(URLS_KEY, nil)
59
- if urls.nil?
60
- urlmap = {"/jobs" => Patriot::Worker::Servlet::JobServlet,
61
- "/worker" => Patriot::Worker::Servlet::WorkerStatusServlet}
62
- else
63
- urlmap = {}
64
- urls = [url] unless urls.is_a? Array
65
- urls.each do |u|
66
- servlet = eval( @config.get("#{URL_MAP_KEY_PREFIX}.#{u}") )
67
- u = "/#{u}" unless u.start_with?("/")
68
- urlmap[u] = servlet
69
- end
70
- end
73
+ urlmap = {"/" => Patriot::Worker::Servlet::IndexServlet,
74
+ "/api/v1/jobs" => Patriot::Worker::Servlet::JobAPIServlet,
75
+ "/api/v1/workers" => Patriot::Worker::Servlet::WorkerAPIServlet}
71
76
  urlmap.values.each{|servlet| servlet.configure(@worker, @config)}
72
77
  return urlmap
73
78
  end
@@ -82,8 +87,13 @@ module Patriot
82
87
  def shutdown_server
83
88
  return false if @server.nil?
84
89
  unless @server_thread.nil?
85
- @handler.shutdown
86
- @logger.info "info server shutdowned"
90
+ begin
91
+ @handler.shutdown
92
+ @logger.info "info server shutdowned"
93
+ rescue => e
94
+ @logger.error "failed to shutdown infoserver", e
95
+ raise e
96
+ end
87
97
  end
88
98
  end
89
99
 
@@ -10,14 +10,10 @@ module Patriot
10
10
  # default offset for searching jobs
11
11
  DEFAULT_JOB_OFFSET = 0
12
12
 
13
- # configuratio key for admin user name
14
- USERNAME_KEY = 'info_server.admin.username'
15
- # configuratio key for admin password
16
- PASSWORD_KEY = 'info_server.admin.password'
17
-
18
- require 'patriot/worker/servlet/job_servlet.rb'
19
- require 'patriot/worker/servlet/worker_status_servlet.rb'
20
-
13
+ require 'patriot/worker/servlet/index_servlet.rb'
14
+ require 'patriot/worker/servlet/api_servlet_base.rb'
15
+ require 'patriot/worker/servlet/job_api_servlet.rb'
16
+ require 'patriot/worker/servlet/worker_api_servlet.rb'
21
17
  end
22
18
  end
23
19
  end
@@ -0,0 +1,40 @@
1
+ require 'sinatra/base'
2
+
3
+ module Patriot
4
+ module Worker
5
+ module Servlet
6
+ class APIServletBase < Sinatra::Base
7
+ set :show_exceptions, :after_handler
8
+
9
+ ### Helper Methods
10
+ helpers do
11
+ # require authorization for updating
12
+ def protected!
13
+ return if authorized?
14
+ headers['WWW-Authenticate'] = 'Basic Realm="Admin Only"'
15
+ halt 401, "Not Authorized"
16
+ end
17
+
18
+ # authorize user (basic authentication)
19
+ def authorized?
20
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
21
+ return @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == [@@username, @@password]
22
+ end
23
+ end
24
+
25
+ # @param worker [Patriot::Wokrer::Base]
26
+ # @param config [Patriot::Util::Config::Base]
27
+ def self.configure(worker, config)
28
+ @@worker = worker
29
+ @@config = config
30
+ @@username = config.get(Patriot::Util::Config::USERNAME_KEY, "")
31
+ @@password = config.get(Patriot::Util::Config::PASSWORD_KEY, "")
32
+ end
33
+
34
+ before do
35
+ content_type 'application/json;charset=utf8'
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/contrib'
3
+
4
+ module Patriot
5
+ module Worker
6
+ module Servlet
7
+
8
+ # allow to monitor worker status
9
+ class IndexServlet < Sinatra::Base
10
+ register Sinatra::Contrib
11
+
12
+ set :public_folder, File.join($home, "public")
13
+ set :views, File.join($home, "public", "views")
14
+
15
+ # @param worker [Patriot::Worker::Base]
16
+ # @param config [Patriot::Util::Config::Base]
17
+ def self.configure(worker, config)
18
+ @@worker = worker
19
+ end
20
+
21
+ before do
22
+ @worker = @@worker
23
+ end
24
+
25
+ get '/' do
26
+ erb :index
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,156 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/contrib'
3
+
4
+ module Patriot
5
+ module Worker
6
+ module Servlet
7
+ # excepton thrown in case job not found
8
+ class JobNotFoundException < Exception; end
9
+ # provide job management functionalities
10
+ class JobAPIServlet < Patriot::Worker::Servlet::APIServletBase
11
+ register Sinatra::Contrib
12
+ include Patriot::Util::DateUtil
13
+ include Patriot::Command::Parser
14
+
15
+ set :show_exceptions, :after_handler
16
+
17
+ get '/stats' do
18
+ return JSON.generate(@@worker.job_store.get_job_size(:ignore_states => [Patriot::JobStore::JobState::SUCCEEDED]))
19
+ end
20
+
21
+ get '/' do
22
+ state = (params['state'] || Patriot::JobStore::JobState::FAILED).to_i
23
+ limit = (params['limit'] || DEFAULT_JOB_LIMIT).to_i
24
+ offset = (params['offset'] || DEFAULT_JOB_OFFSET).to_i
25
+
26
+ query = {:limit => limit, :offset => offset}
27
+ query[:filter_exp] = params['filter_exp'] unless params['filter_exp'].blank?
28
+ job_ids = @@worker.job_store.find_jobs_by_state(state, query) || []
29
+ return JSON.generate(job_ids.map{|job_id| {:job_id => job_id, :state => state}})
30
+ end
31
+
32
+ get '/:job_id' do
33
+ job_id = params[:job_id]
34
+ job = @@worker.job_store.get(job_id, {:include_dependency => true})
35
+ halt(404, json({ERROR: "Job #{job_id} not found"})) if job.nil?
36
+ return JSON.generate(job.attributes.merge({'job_id' => job_id, "update_id" => job.update_id}))
37
+ end
38
+
39
+ get '/:job_id/histories' do
40
+ job_id = params[:job_id]
41
+ history_size = params['size'] || 3
42
+ histories = @@worker.job_store.get_execution_history(job_id, {:limit => history_size})
43
+ return JSON.generate(histories)
44
+ end
45
+
46
+ # get dependency graph of the specified job
47
+ #
48
+ # @param [String] job_id
49
+ # @param [String] p_level a max number of producer dependency level to get
50
+ # @param [String] c_level a max number of consumer dependency level to get
51
+ get '/:job_id/graph' do
52
+ job_id = params[:job_id]
53
+ producer_depth = (params['p_depth'] || 2).to_i
54
+ consumer_depth = (params['c_depth'] || 2).to_i
55
+ graph = @@worker.job_store.get_graph(job_id, {:producer_depth => producer_depth, :consumer_depth => consumer_depth})
56
+ return JSON.generate(graph)
57
+ end
58
+
59
+ post '/' do
60
+ protected!
61
+ body = JSON.parse(request.body.read)
62
+ halt(400, json({ERROR: "COMMAND_CLASS is not provided"})) if body["COMMAND_CLASS"].blank?
63
+ halt(400, json({ERROR: "Patriot::Command::CommandGroup is not acceptable"})) if body["COMMAND_CLASS"] == "Patriot::Command::CommandGroup"
64
+ command_class = body.delete("COMMAND_CLASS").gsub(/\./, '::').constantize
65
+
66
+ job = _build_command(command_class, body)[0]
67
+ job[Patriot::Command::STATE_ATTR] ||= body["state"]
68
+ @@worker.job_store.register(Time.now.to_i, [job])
69
+ return JSON.generate({:job_id => job.job_id})
70
+ end
71
+
72
+ put '/' do
73
+ protected!
74
+ body = JSON.parse(request.body.read)
75
+ job_ids = body["job_ids"]
76
+ state = body['state']
77
+ _set_state_of_jobs(job_ids, state)
78
+ return JSON.generate(job_ids.map{|job_id| {"job_id" => job_id, "state" => state} })
79
+ end
80
+
81
+ put '/:job_id' do
82
+ protected!
83
+ job_id = params['job_id']
84
+ job = @@worker.job_store.get(job_id)
85
+ halt(404, json({ERROR: "Job #{job_id} not found"})) if job.nil?
86
+
87
+ body = JSON.parse(request.body.read)
88
+ state = body['state']
89
+ options = body['option'] || {}
90
+ job_ids = _set_state_of_jobs(job_id, state, options)
91
+ return JSON.generate(job_ids.map{|jid| {"job_id" => jid, "state" => state}})
92
+ end
93
+
94
+ delete '/' do
95
+ protected!
96
+ body = request.body.read
97
+ job_ids = []
98
+ if body != ''
99
+ body = JSON.parse(body)
100
+ job_ids = body["job_ids"]
101
+ else
102
+ job_ids = JSON.parse(params["job_ids"])
103
+ end
104
+ job_ids.each{ |job_id| @@worker.job_store.delete_job(job_id) }
105
+ return JSON.generate(job_ids.map{|job_id| {"job_id" => job_id} })
106
+ end
107
+
108
+ delete '/:job_id' do
109
+ protected!
110
+ job_id = params['job_id']
111
+ job = @@worker.job_store.get(job_id)
112
+ halt(404, json({ERROR: "Job #{job_id} not found"})) if job.nil?
113
+
114
+ @@worker.job_store.delete_job(job_id)
115
+ return JSON.generate({"job_id" => job_id})
116
+ end
117
+
118
+ error JobNotFoundException do
119
+ "Job #{env['sinatra.error'].message} is not found"
120
+ end
121
+
122
+ # @private
123
+ def _set_state_of_jobs(job_ids, state, opts = {})
124
+ job_ids = [job_ids] unless job_ids.is_a? Array
125
+ opts = {'with_subsequent' => false}.merge(opts)
126
+ opts = {:include_subsequent => false}.merge(opts)
127
+ update_id = Time.now.to_i
128
+ @@worker.job_store.set_state(update_id, job_ids, state)
129
+ if opts['with_subsequent']
130
+ @@worker.job_store.process_subsequent(job_ids) do |job_store, jobs|
131
+ next if jobs.empty?
132
+ subsequent_ids = jobs.map(&:job_id)
133
+ @@worker.job_store.set_state(update_id, subsequent_ids, state)
134
+ job_ids |= subsequent_ids
135
+ end
136
+ end
137
+ return job_ids.uniq
138
+ end
139
+ private :_set_state_of_jobs
140
+
141
+ # @private
142
+ def _build_command(clazz, params)
143
+ _params = params.dup
144
+ _params["target_datetime"] = Date.today
145
+ cmd = clazz.new(@@config)
146
+ cmd.produce(_params.delete("products")) unless _params["products"].nil?
147
+ cmd.require(_params.delete("requisites")) unless _params["requisites"].nil?
148
+ cmds = cmd.build(_params)
149
+ return cmds.map{|c| c.to_job}
150
+ end
151
+ private :_build_command
152
+
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,67 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/contrib'
3
+
4
+ module Patriot
5
+ module Worker
6
+ module Servlet
7
+ # excepton thrown when a woker is not accessible
8
+ class WorkerInaccessibleException < Exception; end
9
+ # provide worker management functionalities
10
+ class WorkerAPIServlet < Patriot::Worker::Servlet::APIServletBase
11
+ register Sinatra::Contrib
12
+
13
+ HOST_KEY = "host"
14
+ VERSION_KEY = "version"
15
+ CLASS_KEY = "class"
16
+ STARTED_AT_KEY = "started_at"
17
+ CONFIG_KEY = "config"
18
+
19
+ STATE_KEY = "state"
20
+
21
+ LOCALHOST_EXPS = ["localhost", "127.0.0.1"]
22
+
23
+ set :show_exceptions, :after_handler
24
+
25
+ get '/' do
26
+ worker_hosts = @@config.get(Patriot::Util::Config::WORKER_HOST_KEY)
27
+ worker_hosts = [worker_hosts] unless worker_hosts.is_a?(Array)
28
+ worker_hosts = @@config.get(Patriot::Util::Config::WORKER_HOST_KEY).map do |h|
29
+ h = h.split(":")
30
+ port = h.size == 2 ? h[1] : Patriot::Worker::InfoServer::DEFAULT_PORT
31
+ {'host' => h[0], 'port' => port}
32
+ end
33
+ return JSON.generate(worker_hosts)
34
+ end
35
+
36
+ get '/this' do
37
+ return JSON.generate(
38
+ STATE_KEY => @@worker.status,
39
+ HOST_KEY => @@worker.host,
40
+ VERSION_KEY => Patriot::VERSION,
41
+ CLASS_KEY => @@worker.class.to_s,
42
+ STARTED_AT_KEY => @@worker.started_at,
43
+ CONFIG_KEY => {} # TODO
44
+ )
45
+ end
46
+
47
+ get '/this/state' do
48
+ return JSON.generate(
49
+ STATE_KEY => @@worker.status
50
+ )
51
+ end
52
+
53
+ put '/this/state' do
54
+ protected!
55
+ new_status = params['status']
56
+ if [Patriot::Worker::Status::ACTIVE, Patriot::Worker::Status::SLEEP ].include?(@@worker.status)
57
+ @@worker.status = new_status
58
+ else
59
+ # state cannot be changed in shutdown process
60
+ halt 403
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
+ end