async_endpoint 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4c76ec1af2af2996551f67ab9d709bc39752b61f
4
+ data.tar.gz: 52cd077da34e1a3dc2fa448adbd9fbc64ce4893a
5
+ SHA512:
6
+ metadata.gz: 91f26c554c8e2df7812ca5ce6136a9363d5d9ecb752e34c29c46e30a2cf9c8e73a39aa777cc6eb0d2e8615fb9814d97c8c8b1d5f0f3e729181f6bc1b4a016b32
7
+ data.tar.gz: ce94a3b0085772b0118c1d516c6072d191865a0d891c96f862524b0ad7c36d73194e26bf950a415d4a9c0e96783ffe69a9ec3de1981695c5a87936ecd57eb599
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 mrosso10
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,78 @@
1
+ # Async Endpoint
2
+
3
+ ## Summary
4
+
5
+ Often in our Rails applications we have tasks that may take a lot of time to finish, such as external API requests. This is risky to perform inside our endpoints because it blocks our threads and is not scalable. Here we provide a solution to this problem, using sidekiq to run our heavy work in background
6
+
7
+ ## Approach
8
+
9
+ To avoid making the external requests inside our endpoints we dellegate this task to a sidekiq
10
+ worker and later we have to check periodically the status of the sidekiq job until it is completed.
11
+
12
+ The flow goes like this:
13
+
14
+ 1. We make an AJAX request
15
+ 2. The endpoint enqueues a Sidekiq job that executes the request to the external API. Then returns
16
+ the `async_request_id` with "202 Accepted" HTTP status. For security reasons it also returns an `async_request_token`, that it's actually the Sidekiq job ID.
17
+ 3. We wait some time and make another AJAX request but this time sending the `async_request_id` by parameter and the `async_request_token`.
18
+ 4. The endpoint checks the status of the Sidekiq job. If it is completed returns with HTTP 200
19
+ status code. If it is not, returns again with "202 Accepted" HTTP status and we go back to step 3.
20
+
21
+
22
+ ## Usage
23
+
24
+ First we need to define a class that inherits from AsyncRequest and implements the `execute_task`
25
+ method. Here we will make the request to the external API. We can use parameters set in the
26
+ controller endpoint and we need to set the response data using setter methods.
27
+
28
+ ```ruby
29
+ class MyAsyncRequest < AsyncRequest
30
+ def execute_task
31
+ @response = execute_external_request(params_hash[:param_1], params_hash[:param_2])
32
+ if @response.present?
33
+ self.some_data = @response['some_data']
34
+ self.another_data = @response['another_data']
35
+ done
36
+ else
37
+ failed 'My custom error message'
38
+ end
39
+ end
40
+ end
41
+ ```
42
+
43
+ In the controller endpoint we create an instance of our previously defined class. We can pass
44
+ parameters that can be used when the external request is made. Then we define two procs to handle
45
+ both successful and failed state.
46
+
47
+ ```ruby
48
+ class MyController < ActionController::Base
49
+ def my_endpoint
50
+ @async_request = MyAsyncRequest.init(self, {param_1: 'value_1', param_2: 'value_2'})
51
+ done_proc = proc do
52
+ render json: { some_data: @async_request.some_data,
53
+ another_data: @async_request.another_data }
54
+ end
55
+ failed_proc = proc do
56
+ render json: { error: @async_request.error_message }
57
+ end
58
+ @async_request.handle(done_proc, failed_proc)
59
+ end
60
+ end
61
+ ```
62
+
63
+ The only thing left is the javascript code. To start an async request we simply use the
64
+ `$.getAsyncRequest` method instead of the regular `$.get` method.
65
+
66
+ ```javascript
67
+ $.getAsyncRequest(url, optional_timeout_in_milliseconds)
68
+ .success(function(response) {
69
+ // Do some work
70
+ }).error(function(response) {
71
+ // Handle error
72
+ });
73
+ ```
74
+
75
+ As you can see, the only difference with the regular `$.get` method is that you can pass an optional
76
+ parameter that is the maximum time to wait for the response. If this time is reached and the request
77
+ has not been made the error handler is dispatched.
78
+
@@ -0,0 +1,69 @@
1
+ JQueryXHR = ->
2
+ _error = null
3
+ _success = null
4
+ _complete = null
5
+ _arguments = null
6
+ successCallbacks = []
7
+ errorCallbacks = []
8
+ completeCallbacks = []
9
+
10
+ @success = (callback) ->
11
+ if _success
12
+ callback.apply this, _arguments
13
+ successCallbacks.push callback
14
+ this
15
+
16
+ @error = (callback) ->
17
+ if _error
18
+ callback.apply this, _arguments
19
+ errorCallbacks.push callback
20
+ this
21
+
22
+ @complete = (callback) ->
23
+ if _complete
24
+ callback.apply this, _arguments
25
+ completeCallbacks.push callback
26
+ this
27
+
28
+ @dispatchSuccess = ->
29
+ `var i`
30
+ _arguments = arguments
31
+ _complete = _success = true
32
+ for i of successCallbacks
33
+ successCallbacks[i].apply this, _arguments
34
+ for i of completeCallbacks
35
+ completeCallbacks[i].apply this, _arguments
36
+ return
37
+
38
+ @dispatchError = ->
39
+ `var i`
40
+ _arguments = arguments
41
+ _complete = _success = true
42
+ for i of errorCallbacks
43
+ errorCallbacks[i].apply this, _arguments
44
+ for i of completeCallbacks
45
+ completeCallbacks[i].apply this, _arguments
46
+ return
47
+ return
48
+
49
+
50
+ window.jQuery.getAsyncEndpoint = (url, max_time_to_wait, async_request_id, async_request_token, jqXHR, start_time) ->
51
+ jqXHR = new JQueryXHR() if jqXHR == undefined
52
+ max_time_to_wait = 30000 if max_time_to_wait == undefined
53
+ start_time = Date.now() if start_time == undefined
54
+ time_elapsed = Date.now() - start_time
55
+ $.get(url, 'async_request_id': async_request_id, 'async_request_token': async_request_token)
56
+ .success((response, status, xhr) ->
57
+ if xhr.status == 202
58
+ if time_elapsed < max_time_to_wait
59
+ setTimeout (->
60
+ $.getAsyncEndpoint url, max_time_to_wait, response.async_request_id, response.async_request_token, jqXHR, start_time
61
+ ), time_elapsed * 0.4 + 1000
62
+ else
63
+ jqXHR.dispatchError()
64
+ else
65
+ jqXHR.dispatchSuccess(response, status, xhr)
66
+ ).error( ->
67
+ jqXHR.dispatchError()
68
+ )
69
+ jqXHR
@@ -0,0 +1,96 @@
1
+ module AsyncEndpoint
2
+ class AsyncRequest < ActiveRecord::Base
3
+ enum status: [:pending, :processing, :done, :failed]
4
+ attr_accessor :controller
5
+
6
+ def self.init(controller, hash_params)
7
+ if controller.params[:async_request_id].present?
8
+ async_request = find(controller.params[:async_request_id])
9
+ unless async_request.token_is_valid(controller.params[:async_request_token])
10
+ fail 'Authenticity token is not valid'
11
+ end
12
+ else
13
+ async_request = create(params: hash_params.to_json)
14
+ end
15
+ async_request.controller = controller
16
+ async_request
17
+ end
18
+
19
+ def done
20
+ self.status = AsyncRequest.statuses[:done]
21
+ end
22
+
23
+ def failed(message = nil)
24
+ self.status = AsyncRequest.statuses[:failed]
25
+ self.error_message = message
26
+ end
27
+
28
+ def error_message
29
+ response_hash[:error_message]
30
+ end
31
+
32
+ def params_hash
33
+ return {} unless params.present?
34
+ JSON.parse(params).deep_symbolize_keys
35
+ end
36
+
37
+ def response_hash
38
+ return {} unless response.present?
39
+ JSON.parse(response).deep_symbolize_keys
40
+ end
41
+
42
+ def method_missing(method, *args)
43
+ if method.to_s.ends_with?('=')
44
+ key = method.to_s.chomp('=').to_sym
45
+ self.response = response_hash.merge(key => args.first).to_json
46
+ else
47
+ response_hash[method]
48
+ end
49
+ end
50
+
51
+ def failed_or_crashed?
52
+ failed? || job_failed?
53
+ end
54
+
55
+ def job_failed?
56
+ # Sidekiq::Status.failed? jid
57
+ false
58
+ end
59
+
60
+ def execute
61
+ execute_task
62
+ save
63
+ end
64
+
65
+ def execute_task
66
+ fail 'execute_task must be implemented in subclass'
67
+ end
68
+
69
+ def handle(done, fail)
70
+ return fail.call if failed_or_crashed?
71
+ start_worker if pending?
72
+ render_accepted if pending? || processing?
73
+ done.call if done?
74
+ end
75
+
76
+ def render_accepted
77
+ controller.instance_exec(id, jid, &render_accepted_proc)
78
+ end
79
+
80
+ def render_accepted_proc
81
+ proc do |async_request_id, jid|
82
+ render json: { async_request_id: async_request_id, async_request_token: jid },
83
+ status: :accepted
84
+ end
85
+ end
86
+
87
+ def start_worker
88
+ jid = AsyncEndpointWorker.perform_async(id)
89
+ update_attributes(jid: jid, status: AsyncRequest.statuses[:processing])
90
+ end
91
+
92
+ def token_is_valid(token)
93
+ token.present? && ActiveSupport::SecurityUtils.secure_compare(token, jid)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,10 @@
1
+ module AsyncEndpoint
2
+ class AsyncEndpointWorker
3
+ include Sidekiq::Worker
4
+
5
+ def perform(async_request_id)
6
+ async_request = AsyncRequest.find(async_request_id)
7
+ async_request.execute
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ require "async_endpoint/engine"
2
+
3
+ module AsyncEndpoint
4
+ end
@@ -0,0 +1,5 @@
1
+ module AsyncEndpoint
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace AsyncEndpoint
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module AsyncEndpoint
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'rails/generators/base'
2
+
3
+ class AsyncEndpointGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ def copy_initializer_file
7
+ file_name = "create_async_request.rb"
8
+ now = DateTime.current.strftime("%Y%m%d%H%M%S")
9
+ copy_file file_name, "db/migrate/#{now}_#{file_name}"
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ class CreateAsyncRequest < ActiveRecord::Migration
2
+ def change
3
+ create_table :async_endpoint_async_requests do |t|
4
+ t.string :jid
5
+ t.integer :status, default: 0, null: false
6
+ t.text :response
7
+ t.text :params
8
+ t.string :type
9
+
10
+ t.timestamps null: false
11
+ end
12
+
13
+ add_index :async_endpoint_async_requests, :jid, unique: true
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async_endpoint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Martín Rosso
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sidekiq
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.1'
41
+ description: Often in our Rails applications we have tasks that may take a lot of
42
+ time to finish, such as external API requests. This is risky to perform inside our
43
+ endpoints because it blocks our threads and is not scalable. Here we provide a solution
44
+ to this problem, using sidekiq to run our heavy work in background
45
+ email:
46
+ - mrosso10@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - LICENSE
52
+ - README.md
53
+ - app/assets/javascripts/async_endpoint.coffee
54
+ - app/models/async_endpoint/async_request.rb
55
+ - app/workers/async_endpoint/async_endpoint_worker.rb
56
+ - lib/async_endpoint.rb
57
+ - lib/async_endpoint/engine.rb
58
+ - lib/async_endpoint/version.rb
59
+ - lib/generators/async_endpoint_generator.rb
60
+ - lib/generators/templates/create_async_request.rb
61
+ homepage: https://github.com/mrosso10/async_endpoint
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.4.5.1
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Make asynchronous endpoints in your Ruby on Rails application
85
+ test_files: []