gi_job 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +15 -0
  7. data/Gemfile.lock +34 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +69 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/gi_job.gemspec +29 -0
  14. data/lib/generators/gi_job/install/install_generator.rb +49 -0
  15. data/lib/generators/gi_job/install/templates/models/gi_job_file.rb +28 -0
  16. data/lib/generators/gi_job/install/templates/models/gi_job_log.rb +29 -0
  17. data/lib/generators/gi_job/install/templates/models/gi_job_transaction.rb +39 -0
  18. data/lib/generators/gi_job/install/templates/schemas/gi_job_files.schema +11 -0
  19. data/lib/generators/gi_job/install/templates/schemas/gi_job_logs.schema +10 -0
  20. data/lib/generators/gi_job/install/templates/schemas/gi_job_transactions.schema +18 -0
  21. data/lib/generators/gi_job/install/templates/uploaders/gi_log_file_carrier_wave_uploader.rb +25 -0
  22. data/lib/gi_job.rb +32 -0
  23. data/lib/gi_job/jobs/concerns/job_constructor.rb +185 -0
  24. data/lib/gi_job/jobs/concerns/job_runner.rb +549 -0
  25. data/lib/gi_job/jobs/concerns/jobable.rb +28 -0
  26. data/lib/gi_job/jobs/job_command_suspended.rb +11 -0
  27. data/lib/gi_job/jobs/job_utils.rb +22 -0
  28. data/lib/gi_job/models/concerns/gi_job_fileable.rb +79 -0
  29. data/lib/gi_job/models/concerns/gi_job_loggable.rb +26 -0
  30. data/lib/gi_job/models/concerns/gi_job_ownerble.rb +20 -0
  31. data/lib/gi_job/models/concerns/gi_job_transactionable.rb +220 -0
  32. data/lib/gi_job/tasks/gi_job_task.rake +176 -0
  33. data/lib/gi_job/version.rb +3 -0
  34. metadata +78 -0
@@ -0,0 +1,28 @@
1
+ require "gi_job/jobs/concerns/job_constructor"
2
+ require "gi_job/jobs/concerns/job_runner"
3
+
4
+ module GiJob
5
+ module Jobs
6
+ module Concerns
7
+ module Jobable
8
+
9
+ extend ActiveSupport::Concern
10
+ included do # begin included
11
+ include GiJob::Jobs::Concerns::JobConstructor
12
+ include GiJob::Jobs::Concerns::JobRunner
13
+
14
+ def abc()
15
+ puts "abc!"
16
+ end
17
+ end # end included
18
+
19
+ class_methods do # begin class_methods
20
+ def abcd()
21
+ puts "abc!"
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ module GiJob
2
+ module Jobs
3
+ class JobCommandSuspended < StandardError
4
+ attr_reader :process_progress
5
+ def initialize(process_progress)
6
+ super
7
+ @process_progress = process_progress
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module GiJob
2
+ module Jobs
3
+ class JobUtils
4
+
5
+ def logging_fatal(execption)
6
+ GiJob.logger.fatal(execption.message)
7
+ GiJob.logger.fatal(execption.backtrace.join("\n"))
8
+ end
9
+
10
+ def self.check_job_suspended(gi_job_transaction)
11
+ if gi_job_transaction.process_status_suspended?
12
+ # 一時停止の場合は解除する
13
+ gi_job_transaction.append_log_info(division: "job再開", description: "ジョブを再開しました")
14
+ gi_job_transaction.process_status_none!
15
+ end
16
+ end
17
+
18
+ end # end JobUtils
19
+ end
20
+ end
21
+
22
+ # Commons::GemUtils.reload("/gems/gi_job/lib/gi_job") && reload! && ConsoleTest.new.test_c
@@ -0,0 +1,79 @@
1
+ module GiJob
2
+ module Models
3
+ module Concerns
4
+ module GiJobFileable
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do # begin included
9
+
10
+ mount_uploader :file, GiLogFileCarrierWaveUploader
11
+
12
+ belongs_to :gi_job_transaction
13
+
14
+ end # end included
15
+
16
+ class_methods do # begin class_methods
17
+
18
+ def create_file!(
19
+ gi_job_transaction:,
20
+ file_path: nil,
21
+ uploaded_file: nil,
22
+ rows: 0
23
+ )
24
+ file = nil
25
+ name = nil
26
+ size = 0
27
+
28
+ if file_path.present?
29
+ name = File.basename(file_path)
30
+ file = File.open(file_path)
31
+ size = File.size(file_path)
32
+
33
+ elsif uploaded_file.present?
34
+ GiJob.logger.ap(uploaded_file: uploaded_file)
35
+ name = File.basename(uploaded_file.original_filename)
36
+ file = uploaded_file
37
+ size = uploaded_file.size
38
+ end
39
+
40
+ self.create!({
41
+ gi_job_transaction: gi_job_transaction,
42
+ name: name,
43
+ file: file,
44
+ rows: rows,
45
+ size: size,
46
+ })
47
+ end
48
+
49
+
50
+ def create_action_dispatch_uploaded_file(file, file_name)
51
+ params = {
52
+ :filename => "#{file_name}",
53
+ :type => File.extname(file_name).delete('.'),
54
+ :tempfile => file
55
+ }
56
+ # logger.ap({"method": "base64_conversion", params: params})
57
+ ActionDispatch::Http::UploadedFile.new(params)
58
+ end
59
+
60
+ end # end class_methods
61
+
62
+
63
+
64
+ def data_source
65
+ self.file.force_encoding('utf-8')
66
+ end
67
+
68
+ def delete_data_source!
69
+ self.update!({file: nil})
70
+ end
71
+
72
+ def has_file?
73
+ self.file_blob.present? || self.file.present?
74
+ end
75
+
76
+ end # end GiJobFileable
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,26 @@
1
+ module GiJob
2
+ module Models
3
+ module Concerns
4
+ module GiJobLoggable
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do # begin included
9
+
10
+ belongs_to :gi_job_transaction
11
+
12
+ enum level: {
13
+ info: 100,
14
+ warning: 200,
15
+ error: 300,
16
+ }, _prefix: true
17
+
18
+ end # end included
19
+
20
+ class_methods do # begin class_methods
21
+ end # end class_methods
22
+
23
+ end # end GiJobLoggable
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module GiJob
2
+ module Models
3
+ module Concerns
4
+ module GiJobOwnerble
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do # begin included
9
+
10
+ has_many :gi_job_transaction, as: :owner
11
+
12
+ end # end included
13
+
14
+ class_methods do # begin class_methods
15
+ end # end class_methods
16
+
17
+ end # end GiJobLoggable
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,220 @@
1
+ module GiJob
2
+ module Models
3
+ module Concerns
4
+ module GiJobTransactionable
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do # begin included
9
+
10
+ serialize :parameter
11
+ serialize :process_progress
12
+
13
+ belongs_to :owner, polymorphic: true, optional: true
14
+ has_many :gi_job_logs, dependent: :destroy
15
+ has_many :gi_job_files, dependent: :destroy
16
+
17
+ enum status: {
18
+ registerd: 100,
19
+ running: 200,
20
+ completed: 300,
21
+ error: 400,
22
+ }, _prefix: true
23
+
24
+ enum process_status: {
25
+ none: 100,
26
+ suspended: 200,
27
+ terminated: 300,
28
+ }, _prefix: true
29
+
30
+ enum command: {
31
+ none: 100,
32
+ stop: 200,
33
+ }, _prefix: true
34
+
35
+
36
+ scope :search_jobs, -> (params) {
37
+ search_by_id(params[:id])
38
+ .search_by_name(params[:name])
39
+ .search_by_status(params[:status])
40
+ .order_by(params[:order_by])
41
+ }
42
+
43
+ scope :search_by_id, -> (value) {
44
+ where(id: value) if value.present?
45
+ }
46
+
47
+ scope :search_by_name, -> (value) {
48
+ where(name: value) if value.present?
49
+ }
50
+
51
+ scope :search_by_status, -> (value) {
52
+ v = value.is_a?(String) ? GiJobTransaction.statuses[value] : value
53
+ where(status: v) if v.present?
54
+ }
55
+
56
+ scope :order_by, -> (order_by = nil){
57
+ if order_by.present?
58
+ order("#{order_by}")
59
+ else
60
+ order("gi_job_transactions.updated_at desc")
61
+ end
62
+ }
63
+
64
+ scope :ghosts, -> (no_reaction_minutes) {
65
+ # TODO 更新が無いjobを検出する
66
+ # 本来であればちゃんとprocessキルがされる際にstatusを更新する
67
+ # これは、何らかの原因で jobレコードが更新さずに jobプロセスが死んでしまった場合の暫定的な救済処置である
68
+
69
+ to_datetime = Time.zone.now - no_reaction_minutes.minutes
70
+ where(process_status: :none)
71
+ .where(status: [GiJobTransaction.statuses[:registerd], GiJobTransaction.statuses[:running]])
72
+ .where("gi_job_transactions.updated_at < ?", to_datetime)
73
+ }
74
+
75
+ scope :suspended, -> () {
76
+ where(process_status: :suspended)
77
+ }
78
+
79
+ scope :ended, -> () {
80
+ where(status: [GiJobTransaction.statuses[:completed], GiJobTransaction.statuses[:error]])
81
+ }
82
+
83
+ scope :old_ended, -> (time_limit = 14.days) {
84
+ # 既に終了して2週間起つjobを検出する
85
+ time_now = Time.now
86
+ to_datetime = time_now - time_limit
87
+ Rails.logger.ap("delete_old_file #{time_now} - #{time_limit} = to_datetime: #{to_datetime}")
88
+ where(process_status: :none)
89
+ .ended
90
+ .where("gi_job_transactions.updated_at < ?", to_datetime)
91
+ }
92
+
93
+ scope :has_job_file_data_sources, -> () {
94
+ # 有効なdata_sourcesを持っているジョブを検出する
95
+ eager_load(:gi_job_files)
96
+ .where("gi_job_files.file is not null")
97
+ }
98
+
99
+ scope :past_ended_same_type_jobs, -> (gi_job_transaction) {
100
+ where(
101
+ owner: gi_job_transaction.owner,
102
+ name: gi_job_transaction.name
103
+ ).where(
104
+ "gi_job_transactions.created_at < ?", gi_job_transaction.created_at
105
+ ).ended.where.not(
106
+ id: gi_job_transaction.id
107
+ )
108
+ }
109
+
110
+ end # end included
111
+
112
+ class_methods do # begin class_methods
113
+
114
+ def delete_old_for_one_shot(gi_job_transaction)
115
+ # 単発ジョブ向け。過去のjob_transactionを削除する。
116
+ self.past_ended_same_type_jobs(gi_job_transaction).destroy_all
117
+ end
118
+
119
+ end # end class_methods
120
+
121
+ def append_log_info(**args) append_log(level: :info, **args) end
122
+ def append_log_warning(**args) append_log(level: :warning, **args) end
123
+ def append_log_error(**args) append_log(level: :error, **args) end
124
+
125
+ def append_log(level: :info, position: nil, division: :none, description: nil)
126
+ GiJobLog.create!({
127
+ gi_job_transaction: self,
128
+ level: GiJobLog.levels[level],
129
+ position: position,
130
+ division: division,
131
+ description: description
132
+ })
133
+ end
134
+
135
+ def append_log(level: :info, position: nil, division: :none, description: nil)
136
+ GiJobLog.create!({
137
+ gi_job_transaction: self,
138
+ level: GiJobLog.levels[level],
139
+ position: position,
140
+ division: division,
141
+ description: description
142
+ })
143
+ end
144
+
145
+ def create_job_logs_hash
146
+ hash = {
147
+ info: [],
148
+ warning: [],
149
+ error: [],
150
+ }
151
+ self.gi_job_logs.each {|job_log| hash[job_log.level.to_sym] << job_log}
152
+ hash
153
+ end
154
+
155
+ def counts_ended
156
+ (counts_completed || 0) + (counts_errord || 0)
157
+ end
158
+
159
+ def has_error_log?
160
+ gi_job_logs.any?{|job_log| job_log.error?}
161
+ end
162
+
163
+ def to_simple
164
+ job_logs_hash = create_job_logs_hash
165
+ {
166
+ id: self.id,
167
+ name: self.name,
168
+ status: self.status,
169
+ counts_all: self.counts_all,
170
+ counts_progress: self.counts_progress,
171
+ counts_completed: self.counts_completed,
172
+ counts_errord: self.counts_errord,
173
+ process_status: self.process_status,
174
+ created_at: self.created_at,
175
+ updated_at: self.updated_at,
176
+ gi_job_logs: self.gi_job_logs,
177
+ job_logs_hash: job_logs_hash,
178
+ has_error_log: job_logs_hash[:error].present?,
179
+ }
180
+ end
181
+
182
+ def status_ended?
183
+ status_completed? || status_error?
184
+ end
185
+
186
+ def create_file!(**params)
187
+ GiJobFile.create_file!(gi_job_transaction: self, **params)
188
+ end
189
+
190
+ def log_files_filter_common_warns
191
+ gi_job_log_files.filter do |gi_job_log_file|
192
+ gi_job_log_file.file_name.start_with?("警告")
193
+ end
194
+ end
195
+
196
+ def log_files_filter_common_errors
197
+ gi_job_log_files.filter do |gi_job_log_file|
198
+ gi_job_log_file.file_name.start_with?("エラー")
199
+ end
200
+ end
201
+
202
+
203
+ ### for displays
204
+ def to_string_line
205
+ [
206
+ "#{self.id}",
207
+ "#{self.updated_at}",
208
+ "#{self.owner_type}(#{self.owner_id})",
209
+ "#{self.name}",
210
+ "#{self.status}",
211
+ "#{self.counts_progress}/#{self.counts_all}",
212
+ "errord: #{self.counts_errord}",
213
+ ].join(", ")
214
+ end
215
+
216
+
217
+ end # end GiJobTransactionable
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,176 @@
1
+ require 'sidekiq/api'
2
+
3
+ namespace :gi_job_task do
4
+ desc "job_status が更新されずにプロセスが無くなってしまったジョブの job_status を errorにする"
5
+ task :terminate_ghost_jobs => :environment do
6
+ # puts "start terminate_ghost_jobs"
7
+ time_now = Time.now
8
+ no_reaction_minutes = 30 # 30分間更新されていない未完了のジョブ
9
+ jobs = Job.ghosts(no_reaction_minutes)
10
+ counts_target_jobs = jobs.size
11
+ counts_terminated = 0
12
+ jobs.each do |job|
13
+ # Rails.logger.ap({job: job})
14
+
15
+ # ジョブステータスを更新
16
+ job.append_error("強制停止", "ジョブが停止されました。再度実行してください。")
17
+ job.update_columns({job_status: Job.job_statuses[:error]})
18
+ counts_terminated += 1
19
+
20
+ Rails.logger.fatal("terminate_ghost_job: job_id: #{job.id}")
21
+ end
22
+
23
+ puts "#{time_now.strftime('%Y/%m/%d %H:%M:%S')} end terminate_ghost_jobs #{counts_terminated}/#{counts_target_jobs}"
24
+ end
25
+
26
+ task :delete_old_tmp_files => :environment do
27
+ time_now = Time.now
28
+ # 一時ディレクトリ内の古いファイルを削除
29
+ # クラスタ環境下では一時ディレクトリのファイルはdistでは作成されないが、
30
+ # materialize_job_files で作成されたファイルを削除するために動かしている
31
+ paths = Jobable.delete_old_files
32
+ local_deleted_counts = 0
33
+ paths.each do |path|
34
+ Rails.logger.info("delete_old_files: path: #{path}")
35
+ local_deleted_counts += 1
36
+ end
37
+
38
+ # 終了している古い job_files の file を削除する
39
+ old_ended_has_data_sources_jobs = Job.old_ended.has_job_file_data_sources
40
+ db_deleted_counts = 0
41
+ old_ended_has_data_sources_jobs.each do |job|
42
+ job.job_files.each do |job_file|
43
+ Rails.logger.info("job_file.delete_data_source: job_id: #{job.id}, job_file_id: #{job_file.id}")
44
+ job_file.delete_data_source!
45
+ db_deleted_counts += 1
46
+ end
47
+ end
48
+
49
+ puts "#{time_now.strftime('%Y/%m/%d %H:%M:%S')} end delete_old_tmp_files local: #{local_deleted_counts}, db: #{db_deleted_counts}"
50
+ end
51
+
52
+ ##### 状況確認のための task
53
+ desc "job / sidekiq の状態を表示する"
54
+ task :status => :environment do
55
+ Rake::Task["gi_job_task:show_recently_jobs"].invoke
56
+ Rake::Task["gi_job_task:show_job_statuses"].invoke
57
+ Rake::Task["gi_job_task:show_sidekiq_statuses"].invoke
58
+ end
59
+
60
+ desc "直近10ジョブを表示する"
61
+ task :show_recently_jobs => :environment do
62
+ puts "- show recentry jobs"
63
+ gi_job_transaction = GiJobTransaction.preload(:owner, :gi_job_logs).order(updated_at: :desc).limit(10)
64
+ strings = gi_job_transaction.map(&:to_string_line)
65
+ puts strings.join("\n")
66
+ end
67
+
68
+ desc "全ジョブのステータス集計を表示する"
69
+ task :show_job_statuses => :environment do
70
+ puts "- show jobs statuses"
71
+ grouped = GiJobTransaction.group(:status, :process_status).count
72
+ Rails.logger.ap({grouped: grouped})
73
+ display = grouped.map do |keys, counts|
74
+ "#{sprintf("%-12s", keys[0])} | #{sprintf("%-12s", keys[1])}: #{sprintf("%12d", counts)}"
75
+ end.join("\n")
76
+ puts display
77
+ end
78
+
79
+ desc "sidekiqの状況を表示する"
80
+ task :show_sidekiq_statuses => :environment do
81
+ puts "- show sidekiq statuses"
82
+ # Rails.logger.ap({task: "show sidekiq statuses"})
83
+
84
+ puts "実行中: #{Sidekiq::Workers.new.size} 件"
85
+ Sidekiq::Workers.new.each do |process_id, thread_id, sidekiq_job|
86
+ Rails.logger.ap({
87
+ process_id: process_id,
88
+ thread_id: thread_id,
89
+ sidekiq_job: sidekiq_job
90
+ })
91
+ payload = sidekiq_job["payload"]
92
+ payload_args = payload["args"][0]
93
+ job_argument = payload_args["arguments"][0]
94
+ strings = []
95
+ strings << "pid: #{process_id}"
96
+ strings << "tid: #{thread_id}"
97
+ strings << "sjid: #{payload_args["job_id"]}"
98
+ if job_argument && job_argument["gi_job_transaction"] && job_argument["gi_job_transaction"]["_aj_globalid"]
99
+ strings << "job_id: #{job_argument["gi_job_transaction"]["_aj_globalid"]}"
100
+ end
101
+ strings << "class: #{payload_args["job_class"]}"
102
+
103
+ puts "[#{strings.join("][")}]"
104
+ end
105
+
106
+ display_waiting_proc = Proc.new do |sidekiq_job|
107
+ Rails.logger.ap({
108
+ sidekiq_job: sidekiq_job
109
+ })
110
+ payload = sidekiq_job.item
111
+ payload_args = payload["args"][0]
112
+ job_argument = payload_args["arguments"][0]
113
+ strings = []
114
+ strings << "sjid: #{payload_args["job_id"]}"
115
+ if job_argument && job_argument["gi_job_transaction"] && job_argument["gi_job_transaction"]["_aj_globalid"]
116
+ strings << "job_id: #{job_argument["gi_job_transaction"]["_aj_globalid"]}"
117
+ end
118
+ strings << "class: #{payload_args["job_class"]}"
119
+
120
+ puts "[#{strings.join("][")}]"
121
+ end
122
+
123
+ puts "キュー: #{Sidekiq::Queue.new.size} 件"
124
+ Sidekiq::Queue.new.each do |sidekiq_job|
125
+ display_waiting_proc.call(sidekiq_job)
126
+ end
127
+
128
+ puts "再試行: #{Sidekiq::RetrySet.new.size} 件"
129
+ Sidekiq::RetrySet.new.each do |sidekiq_job|
130
+ display_waiting_proc.call(sidekiq_job)
131
+ end
132
+ end
133
+
134
+
135
+
136
+ task :change_maintenance, ['mode'] => :environment do |task, args|
137
+ mode = args[:mode]
138
+ if mode.blank?
139
+ # modeが指定されなかった場合は現在のモードを表示して終了する
140
+ maintenance = Maintenance.find_record
141
+ puts "current mode: #{maintenance.mode}"
142
+
143
+ else
144
+ # mode変更
145
+ Maintenance.change_mode!(mode)
146
+
147
+ if mode == "none"
148
+ # suspendedされていたジョブを再起動する
149
+ jobs = Job.suspended
150
+ jobs.each do |job|
151
+ job_class_string = job.job_type.classify
152
+ Rails.logger.ap("class => #{job_class_string}")
153
+
154
+ job_class = job_class_string.constantize # -> ModelName
155
+ Rails.logger.ap({job_class: job_class})
156
+
157
+ job_class.start_job(job)
158
+
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ task :materialize_job_files, ['job_id'] => :environment do |task, args|
165
+ # dbに格納されたjob_fileを/tmpに作成する
166
+ job_id = args[:job_id]
167
+ job = Job.eager_load(:job_files).find_by(id: job_id)
168
+ job.job_files.each_with_index do |job_file, index|
169
+ file_name_suffix = "#{index == 0 ? "" : "_#{index.to_s}"}_#{job_file.file_name}"
170
+ file_path = Jobable.create_file_path(job, file_name_suffix)
171
+ Jobable.file_create(job_file.data_source, file_path)
172
+ puts "create_file: #{file_path}"
173
+ end
174
+ end
175
+
176
+ end