sp-job 0.1.17 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +40 -17
- data/VERSION +1 -1
- data/bin/console +1 -1
- data/bin/unique-file +55 -0
- data/lib/sp-job.rb +9 -8
- data/lib/sp/job/back_burner.rb +301 -42
- data/lib/sp/job/broker.rb +59 -21
- data/lib/sp/job/common.rb +288 -156
- data/lib/sp/job/job_db_adapter.rb +63 -0
- data/lib/sp/job/{engine.rb → jwt.rb} +14 -12
- data/lib/sp/job/mail_queue.rb +60 -0
- data/lib/sp/job/pg_connection.rb +68 -76
- data/lib/sp/job/unique_file.rb +73 -0
- data/lib/sp/job/uploaded_image_converter.rb +35 -16
- data/lib/sp/job/worker.rb +1 -8
- data/lib/sp/job/worker_thread.rb +57 -0
- data/lib/tasks/configure.rake +66 -16
- data/sp-job.gemspec +10 -8
- metadata +23 -17
@@ -0,0 +1,63 @@
|
|
1
|
+
module SP
|
2
|
+
module Job
|
3
|
+
|
4
|
+
unless RUBY_ENGINE == 'jruby' # TODO suck in the base class from SP-DUH
|
5
|
+
|
6
|
+
class JobDbAdapter < ::SP::Duh::JSONAPI::Adapters::Db
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Implement the JSONAPI request by direct querying of the JSONAPI function in the database
|
11
|
+
def do_request_on_the_db(method, path, params)
|
12
|
+
jsonapi_query = %Q[ SELECT * FROM public.jsonapi($1, $2, $3, $4, $5, $6, $7, $8, $9) ]
|
13
|
+
|
14
|
+
response = service.connection.exec jsonapi_query, method, (method == 'GET' ? url_with_params_for_query(path, params) : url(path)), (method == 'GET' ? '' : params_for_body(params)), user_id, company_id, company_schema, sharded_schema, accounting_schema, accounting_prefix
|
15
|
+
response.first if response.first
|
16
|
+
end
|
17
|
+
|
18
|
+
def explicit_do_request_on_the_db(exp_accounting_schema, exp_accounting_prefix, method, path, params)
|
19
|
+
jsonapi_query = %Q[ SELECT * FROM public.jsonapi($1, $2, $3, $4, $5, $6, $7, $8, $9) ]
|
20
|
+
|
21
|
+
response = service.connection.exec jsonapi_query, method, (method == 'GET' ? url_with_params_for_query(path, params) : url(path)), (method == 'GET' ? '' : params_for_body(params)), user_id, company_id, company_schema, sharded_schema, exp_accounting_schema, exp_accounting_prefix
|
22
|
+
response.first if response.first
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_id ; service.parameters.user_id ; end
|
26
|
+
def company_id ; service.parameters.company_id ; end
|
27
|
+
def company_schema ; service.parameters.company_schema.nil? ? nil : service.parameters.company_schema ; end
|
28
|
+
def sharded_schema ; service.parameters.sharded_schema.nil? ? nil : service.parameters.sharded_schema ; end
|
29
|
+
def accounting_schema ; service.parameters.accounting_schema.nil? ? nil : service.parameters.accounting_schema ; end
|
30
|
+
def accounting_prefix ; service.parameters.accounting_prefix.nil? ? nil : service.parameters.accounting_prefix ; end
|
31
|
+
|
32
|
+
def params_for_body(params)
|
33
|
+
params.blank? ? '' : params.to_json
|
34
|
+
end
|
35
|
+
|
36
|
+
def params_for_query(params)
|
37
|
+
query = ""
|
38
|
+
if !params.blank?
|
39
|
+
case
|
40
|
+
when params.is_a?(Array)
|
41
|
+
# query = params.join('&')
|
42
|
+
query = params.map{ |v| URI.encode(URI.encode(v), "&") }.join('&')
|
43
|
+
when params.is_a?(Hash)
|
44
|
+
query = params.map do |k,v|
|
45
|
+
if v.is_a?(String)
|
46
|
+
"#{k}=\"#{URI.encode(URI.encode(v), "&")}\""
|
47
|
+
else
|
48
|
+
"#{k}=#{v}"
|
49
|
+
end
|
50
|
+
end.join('&')
|
51
|
+
else
|
52
|
+
query = params.to_s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
query
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#
|
2
|
-
#
|
2
|
+
# Helper to obtain tokens to access toconline API's.
|
3
3
|
#
|
4
|
-
#
|
4
|
+
# And this is the mix-in we'll apply to Job execution classes
|
5
5
|
#
|
6
6
|
# sp-job is free software: you can redistribute it and/or modify
|
7
7
|
# it under the terms of the GNU Affero General Public License as published by
|
@@ -19,16 +19,18 @@
|
|
19
19
|
# encoding: utf-8
|
20
20
|
#
|
21
21
|
|
22
|
+
require 'jwt' # https://github.com/jwt/ruby-jwt
|
23
|
+
|
22
24
|
module SP
|
23
25
|
module Job
|
24
|
-
|
25
|
-
|
26
|
+
class JWTHelper
|
27
|
+
|
28
|
+
# encode & sign jwt
|
29
|
+
def self.encode(key:, payload:)
|
30
|
+
rsa_private = OpenSSL::PKey::RSA.new( File.read( key ) )
|
31
|
+
return JWT.encode payload, rsa_private, 'RS256', { :typ => "JWT" }
|
32
|
+
end #self.encodeJWT
|
26
33
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
34
|
+
end # end class 'JWT'
|
35
|
+
end # module Job
|
36
|
+
end# module SP
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2011-2017 Cloudware S.A. All rights reserved.
|
3
|
+
#
|
4
|
+
# This file is part of sp-job.
|
5
|
+
#
|
6
|
+
# sp-job is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Affero General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# sp-job is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Affero General Public License
|
17
|
+
# along with sp-job. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
# encoding: utf-8
|
20
|
+
#
|
21
|
+
# require 'sp-job'
|
22
|
+
# require 'sp/job/back_burner'
|
23
|
+
# require 'sp/job/mail_queue'
|
24
|
+
#
|
25
|
+
# class MailQueue < ::SP::Job::MailQueue
|
26
|
+
#
|
27
|
+
# # Overide methods if needed!
|
28
|
+
#
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# Backburner.work('mail-queue')
|
32
|
+
#
|
33
|
+
|
34
|
+
module SP
|
35
|
+
module Job
|
36
|
+
class MailQueue
|
37
|
+
extend SP::Job::Common
|
38
|
+
include Backburner::Queue
|
39
|
+
queue 'mail-queue'
|
40
|
+
queue_respond_timeout 30
|
41
|
+
|
42
|
+
def self.perform (job)
|
43
|
+
email = synchronous_send_email(
|
44
|
+
body: job[:body],
|
45
|
+
template: job[:template],
|
46
|
+
to: job[:to],
|
47
|
+
reply_to: job[:reply_to],
|
48
|
+
subject: job[:subject],
|
49
|
+
attachments: job[:attachments]
|
50
|
+
)
|
51
|
+
logger.info "mailto: #{job[:to]} - #{job[:subject]}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.on_failure (e, job)
|
55
|
+
logger.info "Mail to #{job[:to]} failed"
|
56
|
+
end
|
57
|
+
|
58
|
+
end # MailQueue
|
59
|
+
end # Job
|
60
|
+
end # SP
|
data/lib/sp/job/pg_connection.rb
CHANGED
@@ -27,20 +27,6 @@ module SP
|
|
27
27
|
|
28
28
|
class PGConnection
|
29
29
|
|
30
|
-
private
|
31
|
-
|
32
|
-
#
|
33
|
-
# Private Data
|
34
|
-
#
|
35
|
-
@owner = nil
|
36
|
-
@config = nil
|
37
|
-
@connection = nil
|
38
|
-
@treshold = -1
|
39
|
-
@counter = 0
|
40
|
-
@statements = []
|
41
|
-
|
42
|
-
public
|
43
|
-
|
44
30
|
#
|
45
31
|
# Public Attributes
|
46
32
|
#
|
@@ -52,13 +38,14 @@ module SP
|
|
52
38
|
# @param owner
|
53
39
|
# @param config
|
54
40
|
#
|
55
|
-
def initialize (owner:, config:)
|
41
|
+
def initialize (owner:, config:, multithreaded: false)
|
42
|
+
@mutex = multithreaded ? Mutex.new : ::SP::Job::FauxMutex.new
|
56
43
|
@owner = owner
|
57
44
|
@config = config
|
58
|
-
@connection
|
45
|
+
@connection = nil
|
59
46
|
@treshold = -1
|
60
47
|
@counter = 0
|
61
|
-
@
|
48
|
+
@id_cache = {}
|
62
49
|
min = @config[:min_queries_per_conn]
|
63
50
|
max = @config[:max_queries_per_conn]
|
64
51
|
if (!max.nil? && max > 0) || (!min.nil? && min > 0)
|
@@ -66,7 +53,7 @@ module SP
|
|
66
53
|
new_min, new_max = [min, max].minmax
|
67
54
|
new_min = new_min if new_min <= 0
|
68
55
|
if new_min + new_min > 0
|
69
|
-
@treshold = (new_min + (
|
56
|
+
@treshold = (new_min + (new_max - new_min) * rand).to_i
|
70
57
|
else
|
71
58
|
@treshold = new_min.to_i
|
72
59
|
end
|
@@ -78,98 +65,103 @@ module SP
|
|
78
65
|
# Previous one ( if any ) will be closed first.
|
79
66
|
#
|
80
67
|
def connect ()
|
81
|
-
|
82
|
-
|
68
|
+
@mutex.synchronize {
|
69
|
+
_disconnect()
|
70
|
+
@connection = PG.connect(@config[:conn_str])
|
71
|
+
}
|
83
72
|
end
|
84
73
|
|
85
74
|
#
|
86
75
|
# Close currenly open database connection.
|
87
76
|
#
|
88
77
|
def disconnect ()
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
while @statements.count > 0 do
|
93
|
-
@connection.exec("DEALLOCATE #{@statements.pop()}")
|
94
|
-
end
|
95
|
-
@connection.close
|
96
|
-
@connection = nil
|
97
|
-
@counter = 0
|
78
|
+
@mutex.synchronize {
|
79
|
+
_disconnect()
|
80
|
+
}
|
98
81
|
end
|
99
82
|
|
100
83
|
#
|
101
|
-
#
|
84
|
+
# Execute a prepared SQL statement.
|
102
85
|
#
|
103
|
-
# @param query
|
104
|
-
#
|
105
|
-
# @return
|
86
|
+
# @param query the SQL query with data binding
|
87
|
+
# @param args all the args for the query
|
88
|
+
# @return query result.
|
106
89
|
#
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
90
|
+
def exec (query, *args)
|
91
|
+
@mutex.synchronize {
|
92
|
+
if nil == @connection
|
93
|
+
_connect()
|
94
|
+
end
|
95
|
+
_check_life_span()
|
96
|
+
unless @id_cache.has_key? query
|
97
|
+
id = "p#{Digest::MD5.hexdigest(query)}"
|
98
|
+
@connection.prepare(id, query)
|
99
|
+
@id_cache[query] = id
|
100
|
+
else
|
101
|
+
id = @id_cache[query]
|
102
|
+
end
|
103
|
+
@connection.exec_prepared(id, args)
|
104
|
+
}
|
119
105
|
end
|
120
106
|
|
121
107
|
#
|
122
|
-
# Execute a
|
123
|
-
#
|
124
|
-
# @param id
|
125
|
-
# @param args
|
108
|
+
# Execute a query,
|
126
109
|
#
|
127
|
-
# @
|
110
|
+
# @param query
|
128
111
|
#
|
129
|
-
def
|
130
|
-
|
131
|
-
|
112
|
+
def query (query:)
|
113
|
+
@mutex.synchronize {
|
114
|
+
unless query.nil?
|
115
|
+
_check_life_span()
|
116
|
+
@connection.exec(query)
|
117
|
+
end
|
118
|
+
}
|
132
119
|
end
|
133
120
|
|
134
121
|
#
|
135
|
-
#
|
122
|
+
# Call this to check if the database is not a production database where it's
|
123
|
+
# dangerous to make development stuff. It checks the presence of a magic parameter
|
124
|
+
# on the PG configuration that marks the database as a development arena
|
136
125
|
#
|
137
|
-
|
138
|
-
|
139
|
-
#
|
140
|
-
def dealloc_statement (id:)
|
141
|
-
if nil == id
|
142
|
-
while @statements.count > 0 do
|
143
|
-
@connection.exec("DEALLOCATE #{@statements.pop()}")
|
144
|
-
end
|
145
|
-
else
|
146
|
-
@statements.delete!(id)
|
147
|
-
@connection.exec("DEALLOCATE #{id}")
|
148
|
-
end
|
126
|
+
def safety_check ()
|
127
|
+
SP::Duh::Db::safety_check(@connection)
|
149
128
|
end
|
150
129
|
|
151
130
|
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
# @param query
|
131
|
+
# Returns the configured connection string
|
155
132
|
#
|
156
|
-
def
|
157
|
-
|
158
|
-
check_life_span()
|
159
|
-
@connection.exec(query)
|
160
|
-
end
|
133
|
+
def conn_str
|
134
|
+
@config[:conn_str]
|
161
135
|
end
|
162
136
|
|
163
137
|
private
|
164
138
|
|
139
|
+
def _connect ()
|
140
|
+
_disconnect()
|
141
|
+
@connection = PG.connect(@config[:conn_str])
|
142
|
+
end
|
143
|
+
|
144
|
+
def _disconnect ()
|
145
|
+
if @connection.nil?
|
146
|
+
return
|
147
|
+
end
|
148
|
+
|
149
|
+
@connection.exec("DEALLOCATE ALL")
|
150
|
+
@id_cache = {}
|
151
|
+
|
152
|
+
@connection.close
|
153
|
+
@connection = nil
|
154
|
+
@counter = 0
|
155
|
+
end
|
156
|
+
|
165
157
|
#
|
166
158
|
# Check connection life span
|
167
159
|
#
|
168
|
-
def
|
160
|
+
def _check_life_span ()
|
169
161
|
return unless @treshold > 0
|
170
162
|
@counter += 1
|
171
163
|
if @counter > @treshold
|
172
|
-
|
164
|
+
_connect()
|
173
165
|
end
|
174
166
|
end
|
175
167
|
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2011-2016 Cloudware S.A. All rights reserved.
|
3
|
+
#
|
4
|
+
# This file is part of sp-job.
|
5
|
+
#
|
6
|
+
# And this is the mix-in we'll apply to Job execution classes
|
7
|
+
#
|
8
|
+
# sp-job is free software: you can redistribute it and/or modify
|
9
|
+
# it under the terms of the GNU Affero General Public License as published by
|
10
|
+
# the Free Software Foundation, either version 3 of the License, or
|
11
|
+
# (at your option) any later version.
|
12
|
+
#
|
13
|
+
# sp-job is distributed in the hope that it will be useful,
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
# GNU General Public License for more details.
|
17
|
+
#
|
18
|
+
# You should have received a copy of the GNU Affero General Public License
|
19
|
+
# along with sp-job. If not, see <http://www.gnu.org/licenses/>.
|
20
|
+
#
|
21
|
+
# encoding: utf-8
|
22
|
+
#
|
23
|
+
require 'ffi'
|
24
|
+
require 'os'
|
25
|
+
require 'fileutils'
|
26
|
+
|
27
|
+
module SP
|
28
|
+
module Job
|
29
|
+
module Unique
|
30
|
+
module File
|
31
|
+
extend FFI::Library
|
32
|
+
ffi_lib 'c'
|
33
|
+
attach_function :puts, [ :string ], :int
|
34
|
+
attach_function :mkstemps, [:string, :int], :int
|
35
|
+
attach_function :fcntl, [:int, :int, :pointer], :int
|
36
|
+
attach_function :close, [ :int ], :int
|
37
|
+
unless OS.mac?
|
38
|
+
attach_function :readlink, [ :string, :pointer, :int ], :int
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Creates a uniquely named file inside the specified folder. The created file is empty
|
43
|
+
#
|
44
|
+
# @param a_folder Folder where the file will be created
|
45
|
+
# @param a_suffix file suffix, warning must include the .
|
46
|
+
# @return Absolute file path
|
47
|
+
#
|
48
|
+
def self.create (a_folder, a_extension)
|
49
|
+
FileUtils.mkdir_p(a_folder) if !Dir.exist?(a_folder)
|
50
|
+
fd = ::SP::Job::Unique::File.mkstemps("#{a_folder}/XXXXXX#{a_extension}", a_extension.length)
|
51
|
+
return nil if fd < 0
|
52
|
+
|
53
|
+
ptr = FFI::MemoryPointer.new(:char, 8192) # Assumes max path is less that this
|
54
|
+
if OS.mac?
|
55
|
+
r = ::SP::Job::Unique::File.fcntl(fd, 50, ptr) # 50 is F_GETPATH in OSX
|
56
|
+
else
|
57
|
+
r = ::SP::Job::Unique::File.readlink("/proc/self/fd/#{fd}", ptr, 8192)
|
58
|
+
if r > 0 && r < 8192
|
59
|
+
r = 0
|
60
|
+
end
|
61
|
+
end
|
62
|
+
::SP::Job::Unique::File.close(fd)
|
63
|
+
return nil if r != 0
|
64
|
+
return nil if ptr.null?
|
65
|
+
|
66
|
+
rv = ptr.read_string
|
67
|
+
|
68
|
+
return rv
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -28,9 +28,7 @@
|
|
28
28
|
# require 'sp/job/back_burner'
|
29
29
|
# require 'sp/job/uploaded_image_converter'
|
30
30
|
#
|
31
|
-
# class CLASSNAME
|
32
|
-
# extend SP::Job::Common
|
33
|
-
# extend SP::Job::UploadedImageConverter
|
31
|
+
# class CLASSNAME < ::SP::Job::UploadedImageConverter
|
34
32
|
#
|
35
33
|
# def self.perform(job)
|
36
34
|
#
|
@@ -48,23 +46,40 @@
|
|
48
46
|
|
49
47
|
module SP
|
50
48
|
module Job
|
51
|
-
|
49
|
+
class UploadedImageConverter
|
52
50
|
extend SP::Job::Common
|
53
51
|
|
54
52
|
def self.perform (job)
|
55
53
|
|
56
|
-
|
54
|
+
raise_error(message: 'i18n_entity_id_must_be_defined') if job[:entity_id].nil? || job[:entity_id].to_i == 0
|
57
55
|
|
58
56
|
step = 100 / (job[:copies].size + 1)
|
59
|
-
|
60
|
-
|
57
|
+
progress = step
|
58
|
+
original = File.join(config[:paths][:temporary_uploads], job[:original])
|
59
|
+
destination = File.join(config[:paths][:uploads_storage], job[:entity], id_to_path(job[:entity_id]), job[:folder])
|
61
60
|
|
62
61
|
#
|
63
|
-
#
|
62
|
+
# Check the original image, check format and limits
|
64
63
|
#
|
65
64
|
FileUtils::mkdir_p destination
|
66
|
-
image
|
67
|
-
|
65
|
+
update_progress(progress: progress, message: 'i18n_reading_original_$image', image: job[:original])
|
66
|
+
img_info = %x[identify #{original}]
|
67
|
+
m = %r[.*\.ul\s(\w+)\s(\d+)x(\d+)\s.*].match img_info
|
68
|
+
if $?.success? == false
|
69
|
+
return report_error(message: 'i18n_invalid_image', info: "Image #{original} can't be identified '#{img_info}'")
|
70
|
+
end
|
71
|
+
if m.nil? || m.size != 4
|
72
|
+
return report_error(message: 'i18n_invalid_image', info: "Image #{original} can't be identified '#{img_info}'")
|
73
|
+
end
|
74
|
+
unless config[:options][:formats].include? m[1]
|
75
|
+
return report_error(message: 'i18n_unsupported_$format', format: m[1])
|
76
|
+
end
|
77
|
+
if m[2].to_i > config[:options][:max_width]
|
78
|
+
return report_error(message: 'i18n_image_too_wide_$width$max_width', width: m[2], max_width: config[:options][:max_width])
|
79
|
+
end
|
80
|
+
if m[3].to_i > config[:options][:max_height]
|
81
|
+
return report_error(message: 'i18n_image_too_tall_$height$max_height', height: m[3], max_height: config[:options][:max_height])
|
82
|
+
end
|
68
83
|
|
69
84
|
barrier = true # To force progress on first scalling
|
70
85
|
|
@@ -72,18 +87,22 @@ module SP
|
|
72
87
|
# Iterate the copies array
|
73
88
|
#
|
74
89
|
job[:copies].each do |copy|
|
75
|
-
|
76
|
-
|
77
|
-
|
90
|
+
%x[convert #{original} -geometry #{copy[:geometry]} #{File.join(destination, copy[:name])}]
|
91
|
+
unless $?.success?
|
92
|
+
raise_error(message: 'i18n_internal_error', info: "convert failed to scale #{original} to #{copy[:geometry]}")
|
78
93
|
end
|
79
|
-
|
80
|
-
update_progress(
|
94
|
+
progress += step
|
95
|
+
update_progress(progress: progress, message: 'i18n_scalling_image_$name$geometry', name: copy[:name], geometry: copy[:geometry], barrier: barrier)
|
81
96
|
logger.debug("Scaled to geometry #{copy[:geometry]}")
|
82
97
|
barrier = false
|
83
98
|
end
|
84
99
|
|
85
100
|
# Closing arguments, all done
|
86
|
-
|
101
|
+
send_response(message: 'i18n_image_conversion_complete', link: File.join('/',job[:entity], id_to_path(job[:entity_id]), job[:folder], 'logo_template.png'))
|
102
|
+
|
103
|
+
# Remove original file
|
104
|
+
FileUtils::rm_f(original) if config[:options][:delete_originals]
|
105
|
+
|
87
106
|
end
|
88
107
|
|
89
108
|
end # UploadedImageConverter
|