async_endpoint 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +78 -0
- data/app/assets/javascripts/async_endpoint.coffee +69 -0
- data/app/models/async_endpoint/async_request.rb +96 -0
- data/app/workers/async_endpoint/async_endpoint_worker.rb +10 -0
- data/lib/async_endpoint.rb +4 -0
- data/lib/async_endpoint/engine.rb +5 -0
- data/lib/async_endpoint/version.rb +3 -0
- data/lib/generators/async_endpoint_generator.rb +11 -0
- data/lib/generators/templates/create_async_request.rb +15 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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,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: []
|