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.
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