sp-job 0.1.17 → 0.2.2
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.
- 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
|