async_request 0.0.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/async_request/jobs_controller.rb +36 -6
  3. data/app/models/async_request/job.rb +49 -2
  4. data/app/poros/async_request/json_web_token.rb +26 -0
  5. data/app/workers/async_request/job_processor.rb +6 -6
  6. data/config/routes.rb +1 -1
  7. data/lib/async_request.rb +28 -0
  8. data/lib/async_request/engine.rb +2 -1
  9. data/lib/async_request/version.rb +1 -1
  10. data/lib/generators/async_request_generator.rb +6 -3
  11. data/lib/templates/async_request.rb +5 -0
  12. data/lib/templates/create_async_request_jobs.rb +1 -1
  13. data/spec/controllers/async_request/jobs_controller_spec.rb +80 -41
  14. data/spec/controllers/dummy_controller_spec.rb +49 -0
  15. data/spec/dummy/app/controllers/application_controller.rb +0 -5
  16. data/spec/dummy/app/controllers/dummy_controller.rb +11 -0
  17. data/spec/dummy/app/workers/worker_returning_nil.rb +5 -0
  18. data/spec/dummy/app/workers/worker_with_errors.rb +8 -0
  19. data/spec/dummy/app/workers/worker_with_symbol.rb +5 -0
  20. data/spec/dummy/app/workers/worker_without_errors.rb +5 -0
  21. data/spec/dummy/bin/bundle +1 -0
  22. data/spec/dummy/bin/rails +1 -0
  23. data/spec/dummy/bin/rake +1 -0
  24. data/spec/dummy/bin/setup +9 -8
  25. data/spec/dummy/config/application.rb +1 -2
  26. data/spec/dummy/config/initializers/async_request.rb +5 -0
  27. data/spec/dummy/config/routes.rb +2 -2
  28. data/spec/dummy/db/migrate/20170815023204_create_async_request_jobs.rb +16 -0
  29. data/spec/dummy/db/schema.rb +1 -1
  30. data/spec/dummy/log/test.log +0 -5472
  31. data/spec/factories/async_request_job.rb +6 -0
  32. data/spec/models/async_request/job_spec.rb +40 -0
  33. data/spec/spec_helper.rb +1 -0
  34. data/spec/support/helpers.rb +5 -0
  35. data/spec/workers/async_request/job_processor_spec.rb +47 -17
  36. metadata +85 -54
  37. data/app/assets/javascripts/async_request/application.js +0 -13
  38. data/app/assets/stylesheets/async_request/application.css +0 -15
  39. data/app/helpers/async_request/application_helper.rb +0 -15
  40. data/lib/tasks/async_request_tasks.rake +0 -4
  41. data/spec/dummy/app/workers/test.rb +0 -7
  42. data/spec/dummy/log/development.log +0 -827
  43. 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: e3c0c80e8b0b2ebd22e02a571b7688b13d3cc23d
4
- data.tar.gz: 723d1a492e3baa01fae0fa8825b1e8fa581e85da
3
+ metadata.gz: 45805a4b75fb35b1facc6480ebba378982a03d2a
4
+ data.tar.gz: 8278641e1d569f0c39d577281dd595d1a1b2b687
5
5
  SHA512:
6
- metadata.gz: 656bb7dd04557c465fe180b724137cbf275be654cd895864828e868b0dbb1a2d2bf52e4c2bf4aae326f4f3d14ae6814029be09091611cfc46d967402202555ba
7
- data.tar.gz: b37b45bc6a96f28cf6731abf9af02a7c4af0be031f92ae8195b94045d137e5a6942c731c482281d17f7e791ea6ae967e8835b33468bf7cfb590b10d3fd97debe
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
- job = Job.find_by(uid: params[:id])
5
- return head :not_found unless job.present?
6
- job.processed? ? render_finished_job(job) : render_pending(job)
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 render_pending(job)
12
- render json: { status: job.status }, status: :accepted
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: JSON.parse(job.response), status: job.status_code
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: [:waiting, :processing, :processed]
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
- status, response = job.worker.constantize.new.execute(*job.params)
10
- job.update_attributes!(
11
- status: Job.statuses[:processed],
12
- status_code: status,
13
- response: response.to_json
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
@@ -1,3 +1,3 @@
1
1
  AsyncRequest::Engine.routes.draw do
2
- resources :jobs, only: [:show]
2
+ resource :job, only: [:show]
3
3
  end
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module AsyncRequest
2
- VERSION = '0.0.7'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -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
- file_name = 'create_async_request_jobs.rb'
8
- now = DateTime.current.strftime('%Y%m%d%H%M%S')
9
- copy_file file_name, "db/migrate/#{now}_#{file_name}"
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
@@ -0,0 +1,5 @@
1
+ AsyncRequest.configure do |config|
2
+ config.encode_key = Rails.application.secrets.secret_key_base
3
+ config.decode_key = Rails.application.secrets.secret_key_base
4
+ config.token_expiration = 1.day
5
+ end
@@ -1,5 +1,5 @@
1
1
  class CreateAsyncRequestJobs < ActiveRecord::Migration
2
- def change
2
+ def change # rubocop:disable Metrics/MethodLength
3
3
  create_table :async_request_jobs do |t|
4
4
  t.string :worker
5
5
  t.integer :status
@@ -1,46 +1,85 @@
1
1
  require 'spec_helper'
2
2
 
3
- module AsyncRequest
4
- describe JobsController do
5
- routes { AsyncRequest::Engine.routes }
6
-
7
- describe '#show' do
8
- context 'when there is no job with the given id' do
9
- let!(:job) { FactoryGirl.create(:async_request_job, :waiting) }
10
- it 'returns 404' do
11
- get :show, id: job.uid + 'ABC'
12
- expect(response.status).to eq(404)
13
- end
14
- end
15
-
16
- context 'when the job exists but it is in a waiting status' do
17
- let!(:job) { FactoryGirl.create(:async_request_job, :waiting) }
18
- it 'returns 202' do
19
- get :show, id: job.uid
20
- expect(response.status).to eq(202)
21
- end
22
- end
23
-
24
- context 'when the job exists but it is in a processing status' do
25
- let!(:job) { FactoryGirl.create(:async_request_job, :processing) }
26
- it 'returns 202' do
27
- get :show, id: job.uid
28
- expect(response.status).to eq(202)
29
- end
30
- end
31
-
32
- context 'when the job exists and has finished' do
33
- let!(:job) { FactoryGirl.create(:async_request_job, :processed) }
34
-
35
- it 'returns the saved status code' do
36
- get :show, id: job.uid
37
- expect(response.status).to eq(job.status_code)
38
- end
39
-
40
- it 'returns the saved status code' do
41
- get :show, id: job.uid
42
- expect(response_body).to eq(JSON.parse(job.response))
43
- end
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