async_request 0.0.7 → 1.0.0
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/app/controllers/async_request/jobs_controller.rb +36 -6
- data/app/models/async_request/job.rb +49 -2
- data/app/poros/async_request/json_web_token.rb +26 -0
- data/app/workers/async_request/job_processor.rb +6 -6
- data/config/routes.rb +1 -1
- data/lib/async_request.rb +28 -0
- data/lib/async_request/engine.rb +2 -1
- data/lib/async_request/version.rb +1 -1
- data/lib/generators/async_request_generator.rb +6 -3
- data/lib/templates/async_request.rb +5 -0
- data/lib/templates/create_async_request_jobs.rb +1 -1
- data/spec/controllers/async_request/jobs_controller_spec.rb +80 -41
- data/spec/controllers/dummy_controller_spec.rb +49 -0
- data/spec/dummy/app/controllers/application_controller.rb +0 -5
- data/spec/dummy/app/controllers/dummy_controller.rb +11 -0
- data/spec/dummy/app/workers/worker_returning_nil.rb +5 -0
- data/spec/dummy/app/workers/worker_with_errors.rb +8 -0
- data/spec/dummy/app/workers/worker_with_symbol.rb +5 -0
- data/spec/dummy/app/workers/worker_without_errors.rb +5 -0
- data/spec/dummy/bin/bundle +1 -0
- data/spec/dummy/bin/rails +1 -0
- data/spec/dummy/bin/rake +1 -0
- data/spec/dummy/bin/setup +9 -8
- data/spec/dummy/config/application.rb +1 -2
- data/spec/dummy/config/initializers/async_request.rb +5 -0
- data/spec/dummy/config/routes.rb +2 -2
- data/spec/dummy/db/migrate/20170815023204_create_async_request_jobs.rb +16 -0
- data/spec/dummy/db/schema.rb +1 -1
- data/spec/dummy/log/test.log +0 -5472
- data/spec/factories/async_request_job.rb +6 -0
- data/spec/models/async_request/job_spec.rb +40 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/helpers.rb +5 -0
- data/spec/workers/async_request/job_processor_spec.rb +47 -17
- metadata +85 -54
- data/app/assets/javascripts/async_request/application.js +0 -13
- data/app/assets/stylesheets/async_request/application.css +0 -15
- data/app/helpers/async_request/application_helper.rb +0 -15
- data/lib/tasks/async_request_tasks.rake +0 -4
- data/spec/dummy/app/workers/test.rb +0 -7
- data/spec/dummy/log/development.log +0 -827
- data/spec/helpers/async_request/application_helper_spec.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45805a4b75fb35b1facc6480ebba378982a03d2a
|
4
|
+
data.tar.gz: 8278641e1d569f0c39d577281dd595d1a1b2b687
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a043dd82166c8a01b2e6a471da23b29c475a307fdcfe2ba5400365688c7c31d7c805cbd1e65bf855602adc5fb7726be2024330b68a5feb43691718a58c73e8bd
|
7
|
+
data.tar.gz: 49cbb5615b4adf3c1d87d118dc871270dfa3c530f5687cbe55834b77339444ccd816a1be605dade3fa893effebf6feaccb8dbb04b4c9f65ad32740d95767f666
|
@@ -1,19 +1,49 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
1
3
|
module AsyncRequest
|
2
4
|
class JobsController < ActionController::Base
|
3
5
|
def show
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
return render_invalid_token unless valid_token?
|
7
|
+
job = Job.find_by(id: token[:job_id])
|
8
|
+
return head :not_found if job.blank?
|
9
|
+
job.finished? ? render_finished_job(job) : render_pending
|
7
10
|
end
|
8
11
|
|
9
12
|
private
|
10
13
|
|
11
|
-
def
|
12
|
-
|
14
|
+
def valid_token?
|
15
|
+
token[:job_id].present? && Time.zone.now.to_i < token[:expires_in]
|
16
|
+
end
|
17
|
+
|
18
|
+
def token
|
19
|
+
@token ||= decode_token
|
20
|
+
end
|
21
|
+
|
22
|
+
def decode_token
|
23
|
+
HashWithIndifferentAccess.new(
|
24
|
+
JsonWebToken.decode(
|
25
|
+
request.headers[AsyncRequest.config[:request_header_key]].split(' ').last
|
26
|
+
)
|
27
|
+
)
|
28
|
+
rescue StandardError
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
|
32
|
+
def render_invalid_token
|
33
|
+
render json: { errors: [{ message: 'Invalid token' }] }, status: :bad_request
|
34
|
+
end
|
35
|
+
|
36
|
+
def render_pending
|
37
|
+
head :accepted
|
13
38
|
end
|
14
39
|
|
15
40
|
def render_finished_job(job)
|
16
|
-
render json:
|
41
|
+
render json: {
|
42
|
+
status: job.status,
|
43
|
+
response: {
|
44
|
+
status_code: job.status_code, body: job.response
|
45
|
+
}
|
46
|
+
}, status: :ok
|
17
47
|
end
|
18
48
|
end
|
19
49
|
end
|
@@ -1,6 +1,53 @@
|
|
1
1
|
module AsyncRequest
|
2
|
-
class Job < ActiveRecord::Base
|
2
|
+
class Job < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
|
3
3
|
serialize :params, Array
|
4
|
-
enum status:
|
4
|
+
enum status: { waiting: 0, processing: 1, processed: 2, failed: 3 }
|
5
|
+
|
6
|
+
def self.create_and_enqueue(worker_class, *params)
|
7
|
+
raise ArgumentError if worker_class.nil?
|
8
|
+
create(
|
9
|
+
worker: worker_class,
|
10
|
+
params: params,
|
11
|
+
status: statuses[:waiting],
|
12
|
+
uid: SecureRandom.uuid
|
13
|
+
).tap { |job| JobProcessor.perform_async(job.id) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def token
|
17
|
+
@token ||= JsonWebToken.encode(id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def successfully_processed!(response, status_code)
|
21
|
+
Rails.logger.info("Processing finished successfully for job with id=#{id}")
|
22
|
+
update_attributes!(
|
23
|
+
status: :processed,
|
24
|
+
status_code: map_status_code(status_code),
|
25
|
+
response: response.to_s
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def processing!
|
30
|
+
Rails.logger.info("Processing job with id=#{id}")
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def finished?
|
35
|
+
processed? || failed?
|
36
|
+
end
|
37
|
+
|
38
|
+
def finished_with_errors!(error)
|
39
|
+
Rails.logger.info("Processing failed for job with id=#{id}")
|
40
|
+
Rails.logger.info(error.message)
|
41
|
+
Rails.logger.info(error.backtrace.inspect)
|
42
|
+
update_attributes!(status: :failed, status_code: 500,
|
43
|
+
response: { error: error.message }.to_json)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def map_status_code(status_code)
|
49
|
+
return Rack::Utils::SYMBOL_TO_STATUS_CODE[status_code] if status_code.is_a?(Symbol)
|
50
|
+
status_code.to_i
|
51
|
+
end
|
5
52
|
end
|
6
53
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module AsyncRequest
|
5
|
+
class JsonWebToken
|
6
|
+
def self.encode(job_id, expiration = AsyncRequest.config[:token_expiration].to_i)
|
7
|
+
JWT.encode(
|
8
|
+
{
|
9
|
+
job_id: job_id,
|
10
|
+
expires_in: (Time.zone.now + expiration).to_i
|
11
|
+
},
|
12
|
+
AsyncRequest.config[:encode_key],
|
13
|
+
AsyncRequest.config[:sign_algorithm]
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.decode(token)
|
18
|
+
JWT.decode(
|
19
|
+
token,
|
20
|
+
AsyncRequest.config[:decode_key],
|
21
|
+
true,
|
22
|
+
algorithm: AsyncRequest.config[:sign_algorithm]
|
23
|
+
).first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -6,12 +6,12 @@ module AsyncRequest
|
|
6
6
|
def perform(id)
|
7
7
|
job = Job.find(id)
|
8
8
|
job.processing!
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
begin
|
10
|
+
status, response = job.worker.constantize.new.execute(*job.params)
|
11
|
+
job.successfully_processed!(response, status)
|
12
|
+
rescue StandardError => e
|
13
|
+
job.finished_with_errors! e
|
14
|
+
end
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
data/config/routes.rb
CHANGED
data/lib/async_request.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
require 'async_request/engine'
|
2
2
|
|
3
3
|
module AsyncRequest
|
4
|
+
VALID_ALGORITHMS = %w[HS256 RS256].freeze
|
5
|
+
|
4
6
|
@config = {
|
7
|
+
sign_algorithm: 'HS256',
|
8
|
+
encode_key: nil,
|
9
|
+
decode_key: nil,
|
10
|
+
token_expiration: 86_400,
|
11
|
+
request_header_key: 'X-JOB-AUTHORIZATION',
|
5
12
|
queue: 'default',
|
6
13
|
retry: false
|
7
14
|
}
|
@@ -10,6 +17,27 @@ module AsyncRequest
|
|
10
17
|
yield self
|
11
18
|
end
|
12
19
|
|
20
|
+
def self.sign_algorithm=(sign_algorithm)
|
21
|
+
raise ArgumentError unless VALID_ALGORITHMS.include?(sign_algorithm)
|
22
|
+
@config[:sign_algorithm] = sign_algorithm
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.encode_key=(encode_key)
|
26
|
+
@config[:encode_key] = encode_key
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.decode_key=(decode_key)
|
30
|
+
@config[:decode_key] = decode_key
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.token_expiration=(token_expiration)
|
34
|
+
@config[:token_expiration] = token_expiration
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.request_header_key=(request_header_key)
|
38
|
+
@config[:request_header_key] = request_header_key
|
39
|
+
end
|
40
|
+
|
13
41
|
def self.queue=(queue)
|
14
42
|
@config[:queue] = queue
|
15
43
|
end
|
data/lib/async_request/engine.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
require 'sidekiq'
|
2
|
+
|
2
3
|
module AsyncRequest
|
3
4
|
class Engine < ::Rails::Engine
|
4
5
|
isolate_namespace AsyncRequest
|
5
6
|
|
6
7
|
initializer 'async_request', before: :load_config_initializers do |app|
|
7
8
|
Rails.application.routes.append do
|
8
|
-
mount AsyncRequest::Engine, at: '/async_request'
|
9
|
+
mount AsyncRequest::Engine, at: '/async_request', as: 'async_request'
|
9
10
|
end
|
10
11
|
|
11
12
|
unless app.root.to_s.match root.to_s
|
@@ -4,8 +4,11 @@ class AsyncRequestGenerator < Rails::Generators::Base
|
|
4
4
|
source_root File.expand_path('../../templates', __FILE__)
|
5
5
|
|
6
6
|
def copy_initializer_file
|
7
|
-
|
8
|
-
now =
|
9
|
-
copy_file
|
7
|
+
migration_file = 'create_async_request_jobs.rb'
|
8
|
+
now = Time.zone.now.strftime('%Y%m%d%H%M%S') # rubocop:disable Style/FormatStringToken
|
9
|
+
copy_file migration_file, "db/migrate/#{now}_#{migration_file}"
|
10
|
+
|
11
|
+
config_file = 'async_request.rb'
|
12
|
+
copy_file config_file, "config/initializers/#{config_file}"
|
10
13
|
end
|
11
14
|
end
|
@@ -1,46 +1,85 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
3
|
+
describe AsyncRequest::JobsController do
|
4
|
+
routes { AsyncRequest::Engine.routes }
|
5
|
+
|
6
|
+
describe '#show' do
|
7
|
+
let(:job) { create(:async_request_job, status) }
|
8
|
+
let(:job_id) { job.id }
|
9
|
+
let(:job_token) { AsyncRequest::JsonWebToken.encode(job_id) }
|
10
|
+
let(:status) { :waiting }
|
11
|
+
|
12
|
+
before { request.headers['X-JOB-AUTHORIZATION'] = "Bearer #{job_token}" }
|
13
|
+
|
14
|
+
context 'when there is no job with the given id' do
|
15
|
+
let(:job_id) { 1000 }
|
16
|
+
|
17
|
+
it 'returns status not found' do
|
18
|
+
get :show
|
19
|
+
expect(response).to have_http_status :not_found
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when receiving an expired token' do
|
24
|
+
let(:job_token) do
|
25
|
+
AsyncRequest::JsonWebToken.encode(create(:async_request_job).id, (1.day * -1).to_i)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns status bad request' do
|
29
|
+
get :show
|
30
|
+
expect(response).to have_http_status :bad_request
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when the job exists but it is in a waiting status' do
|
35
|
+
it 'returns status accepted' do
|
36
|
+
get :show
|
37
|
+
expect(response).to have_http_status :accepted
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when the job exists but it is in a processing status' do
|
42
|
+
let(:status) { :processing }
|
43
|
+
|
44
|
+
it 'returns status accepted' do
|
45
|
+
get :show
|
46
|
+
expect(response).to have_http_status :accepted
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'when the job exists and has finished' do
|
51
|
+
let(:status) { :processed }
|
52
|
+
|
53
|
+
it 'returns the saved status code' do
|
54
|
+
get :show
|
55
|
+
expect(response).to have_http_status :ok
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'returns as body job\'s status code and response' do
|
59
|
+
get :show
|
60
|
+
controller_response = {
|
61
|
+
'status' => 'processed',
|
62
|
+
'response' => { 'status_code' => job.status_code, 'body' => job.response }
|
63
|
+
}
|
64
|
+
expect(response_body).to eq(controller_response)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when the job exists and has a failed status' do
|
69
|
+
let(:status) { :failed }
|
70
|
+
|
71
|
+
it 'returns the saved status code' do
|
72
|
+
get :show
|
73
|
+
expect(response).to have_http_status :ok
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'returns as body job\'s status code and response' do
|
77
|
+
get :show
|
78
|
+
controller_response = {
|
79
|
+
'status' => 'failed',
|
80
|
+
'response' => { 'status_code' => job.status_code, 'body' => job.response }
|
81
|
+
}
|
82
|
+
expect(response_body).to eq(controller_response)
|
44
83
|
end
|
45
84
|
end
|
46
85
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DummyController, type: :controller do
|
4
|
+
describe '.async_option_1' do
|
5
|
+
subject { post :async_option_1 }
|
6
|
+
|
7
|
+
it 'returns status code accepted' do
|
8
|
+
subject
|
9
|
+
expect(response).to have_http_status(:accepted)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'creates the job model' do
|
13
|
+
expect { subject }.to change { AsyncRequest::Job.count }.by(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the url' do
|
17
|
+
subject
|
18
|
+
expect(response_body['url']).to be_present
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns the token' do
|
22
|
+
subject
|
23
|
+
expect(response_body['token']).to be_present
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '.async_option_2' do
|
28
|
+
subject { post :async_option_2 }
|
29
|
+
|
30
|
+
it 'returns status code accepted' do
|
31
|
+
subject
|
32
|
+
expect(response).to have_http_status(:accepted)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'creates the job model' do
|
36
|
+
expect { subject }.to change { AsyncRequest::Job.count }.by(1)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns the token' do
|
40
|
+
subject
|
41
|
+
expect(response_body['token']).to be_present
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'returns the location header' do
|
45
|
+
subject
|
46
|
+
expect(response.headers['Location']).to be_present
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|