async_endpoint 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []