faastruby-rpc 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/Gemfile.lock +3 -3
- data/README.md +42 -35
- data/lib/faastruby-rpc/function.rb +86 -46
- data/lib/faastruby-rpc/test_helper.rb +49 -0
- data/lib/faastruby-rpc/version.rb +1 -1
- data/lib/faastruby-rpc.rb +10 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0399784305d0de9f8ee8db0f602cc2062a1c1d8d631bdfd0514c1d5b846e7c0
|
4
|
+
data.tar.gz: f437fe5dce46945b840117d3cfa6a0b68699046b7f0a64ba7822fd2278dcc4cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab2e9df498673377e78ba3d94103a4cc07daca5ce7bcd601aede51ded5e60a74560821e5cc51089a988fbfd45a448c14a88dcb5825ea79d5a2facf1ad2d70643
|
7
|
+
data.tar.gz: a946ba11384ce3d9ec7a090f9be8405b9a69e69400fe153cff602a2cc2fd35e815c7474296f9dde19c8128f4c7e5db68396cf62e490d974aaf00dedc4fe8b222
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
faastruby-rpc (0.1.
|
4
|
+
faastruby-rpc (0.1.4)
|
5
5
|
oj (~> 3.6)
|
6
6
|
|
7
7
|
GEM
|
@@ -13,7 +13,7 @@ GEM
|
|
13
13
|
safe_yaml (~> 1.0.0)
|
14
14
|
diff-lcs (1.3)
|
15
15
|
hashdiff (0.3.7)
|
16
|
-
oj (3.7.
|
16
|
+
oj (3.7.6)
|
17
17
|
public_suffix (3.0.3)
|
18
18
|
rake (10.5.0)
|
19
19
|
rspec (3.8.0)
|
@@ -46,4 +46,4 @@ DEPENDENCIES
|
|
46
46
|
webmock (~> 3.4)
|
47
47
|
|
48
48
|
BUNDLED WITH
|
49
|
-
1.
|
49
|
+
1.17.3
|
data/README.md
CHANGED
@@ -5,69 +5,76 @@ Wrapper to make it easy to call FaaStRuby functions.
|
|
5
5
|
#### What is FaaStRuby?
|
6
6
|
FaaStRuby is a serverless platform built for Ruby developers.
|
7
7
|
|
8
|
-
* [Tutorial](https://faastruby.io/
|
8
|
+
* [Tutorial](https://faastruby.io/getting-started)
|
9
9
|
|
10
|
-
## Calling functions from within a function
|
10
|
+
## Calling functions from within a function (RPC calls)
|
11
11
|
|
12
|
-
To call
|
12
|
+
To call another function you must first require it on the top of `handler.rb`, passing a string that will be converted to a constant. You then use the constant to call the function and get its response.
|
13
|
+
|
14
|
+
To call the function, use the method `call`. Here is an example.
|
13
15
|
|
14
16
|
```ruby
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
|
17
|
+
require_function 'paulo/hello-world', as: 'HelloWorld'
|
18
|
+
def handler(event)
|
19
|
+
hello = HelloWorld.call # Async call
|
20
|
+
hello.class #=> FaaStRuby::RPC::Function
|
21
|
+
hello.returned? #=> false # READ BELOW TO UNDERSTAND
|
22
|
+
hello #=> 'Hello, World!' - The RPC response body
|
23
|
+
hello.body #=> 'Hello, World!' - Also the RPC response body
|
24
|
+
hello.returned? #=> true # READ BELOW TO UNDERSTAND
|
25
|
+
hello.code #=> 200 - The status code
|
26
|
+
hello.headers #=> {"content-type"=>"text/plain", "x-content-type-options"=>"nosniff", "connection"=>"close", "content-length"=>"5"} - The response headers
|
27
|
+
render text: hello
|
28
|
+
end
|
21
29
|
```
|
30
|
+
The biggest problem with serverless applications is the latency resultant of multiple calls to different functions. This is called Tail Latency.
|
31
|
+
To minimize this problem, `faastruby-rpc` will handle the request to other functions in an async fashion.
|
32
|
+
You should design your application with that in mind. For example, you can design your application so the external function calls are done early in the program execution and perform other tasks while you wait for the response.
|
22
33
|
|
23
|
-
`
|
24
|
-
* result.body - The response body from the function you called
|
25
|
-
* result.code - The HTTP status code returned by the function
|
26
|
-
* result.headers - The headers returned by the functions
|
34
|
+
In the example above, `hello = HelloWorld.call` will issue a non-blocking HTTP request in a separate thread to the called function's endpoint, assign it to a variable and continue execution. When you need the return value from that function, just use the variable. If you call the variable before the request is completed, the execution will block until an answer is received from the external function. To minimize tail latency, just design your application around those async calls.
|
27
35
|
|
28
|
-
|
36
|
+
If at any point you need to know if the external function call already returned without blocking the execution of your program, use the method `returned?`. So in the example above, `hello.returned?` will be false until the request is fulfilled.
|
29
37
|
|
30
|
-
|
38
|
+
## Passing arguments to the called function
|
39
|
+
|
40
|
+
Say you have the following function in `my-workspace/echo`:
|
31
41
|
|
32
42
|
```ruby
|
33
|
-
|
34
|
-
|
35
|
-
render text:
|
43
|
+
# Note the required keyword argument 'name:'
|
44
|
+
def handler(event, name:)
|
45
|
+
render text: name
|
36
46
|
end
|
37
47
|
```
|
38
48
|
|
39
|
-
|
40
|
-
* method: POST
|
41
|
-
* header `Content-Type: application/json`
|
42
|
-
* header `Faastruby-Rpc: true`
|
43
|
-
* body: JSON array
|
49
|
+
If you want to call this function in another function, you can simply pass the argument within `call`:
|
44
50
|
|
45
|
-
`invoke` is just a helper to the following method:
|
46
51
|
```ruby
|
47
|
-
|
48
|
-
|
52
|
+
require_function 'my-workspace/echo', as: 'Echo'
|
53
|
+
def handler(event)
|
54
|
+
name = Echo.call(name: 'John Doe') # Async call
|
55
|
+
render text: "Hello, #{name}!" # calling 'name' will block until Echo returns
|
56
|
+
end
|
49
57
|
```
|
58
|
+
You can use positional or keyword arguments when calling external functions, as long as the external function's `handler` method is defined with matching arguments.
|
50
59
|
|
51
60
|
This gem is already required when you run your functions in FaaStRuby, or using `faastruby server`.
|
52
61
|
|
53
62
|
## Handling errors
|
54
63
|
|
55
|
-
By default, an exception is raised if the invoked function HTTP status code is greater
|
64
|
+
By default, an exception is raised if the invoked function HTTP status code is greater or equal to 400. This is important to make your functions easier to debug, and you will always know what to expect from that function call.
|
56
65
|
|
57
|
-
To disable this behaviour, pass `raise_errors: false`
|
66
|
+
To disable this behaviour, pass `raise_errors: false` when requiring the function. For example:
|
58
67
|
|
59
68
|
```ruby
|
60
|
-
|
61
|
-
# or
|
62
|
-
FaaStRuby::RPC::Function.new("paulo/hello", raise_errors: false).call(body: nil)
|
69
|
+
require_function 'paulo/hello-world', as: 'HelloWorld', raise_errors: false
|
63
70
|
```
|
64
71
|
|
65
|
-
## Stubbing
|
66
|
-
If you are testing a function that
|
72
|
+
## Stubbing RPC calls in your function tests
|
73
|
+
If you are testing a function that required another one, you likely will want to fake that call. To do that, use the following test helper:
|
67
74
|
|
68
75
|
```ruby
|
69
|
-
# This will
|
70
|
-
#
|
76
|
+
# This will make it fake the calls to 'paulo/hello-world'
|
77
|
+
# and return the values you pass in the block.
|
71
78
|
require 'faastruby-rpc/test_helper'
|
72
79
|
|
73
80
|
FaaStRuby::RPC.stub_call('paulo/hello-world') do |response|
|
@@ -1,25 +1,20 @@
|
|
1
1
|
module FaaStRuby
|
2
2
|
FAASTRUBY_HOST = ENV['FAASTRUBY_HOST'] || "http://localhost:3000"
|
3
3
|
module RPC
|
4
|
-
@@response = {}
|
5
|
-
def self.stub_call(function_path, &block)
|
6
|
-
helper = TestHelper.new
|
7
|
-
block.call(helper)
|
8
|
-
response = Struct.new(:body, :code, :headers, :klass)
|
9
|
-
@@response[function_path] = response.new(helper.body, helper.code, helper.headers, helper.klass)
|
10
|
-
end
|
11
|
-
def self.stub_call?(path)
|
12
|
-
@@response[path]
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.response(path)
|
16
|
-
@@response[path]
|
17
|
-
end
|
18
|
-
|
19
4
|
class ExecutionError < StandardError
|
20
5
|
end
|
6
|
+
class Response
|
7
|
+
attr_reader :body, :code, :headers, :klass
|
8
|
+
def initialize(body, code, headers, klass = nil)
|
9
|
+
@body = body
|
10
|
+
@code = code
|
11
|
+
@headers = headers
|
12
|
+
@klass = klass
|
13
|
+
end
|
14
|
+
end
|
21
15
|
class Function
|
22
16
|
def initialize(path, raise_errors: true)
|
17
|
+
@response = nil
|
23
18
|
@path = path
|
24
19
|
@methods = {
|
25
20
|
'post' => Net::HTTP::Post,
|
@@ -28,51 +23,96 @@ module FaaStRuby
|
|
28
23
|
'patch' => Net::HTTP::Patch,
|
29
24
|
'delete' => Net::HTTP::Delete
|
30
25
|
}
|
31
|
-
@response = Struct.new(:body, :code, :headers, :klass)
|
32
26
|
@raise_errors = raise_errors
|
33
27
|
end
|
34
|
-
def
|
35
|
-
|
28
|
+
def call_with(*args)
|
29
|
+
execute(req_body: Oj.dump(args), headers: {'Content-Type' => 'application/json', 'Faastruby-Rpc' => 'true'})
|
36
30
|
end
|
37
31
|
|
38
|
-
def call(
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
resp_body =
|
55
|
-
|
56
|
-
|
57
|
-
|
32
|
+
def call(*args)
|
33
|
+
return call_with(*args) if args.any?
|
34
|
+
execute(method: 'get')
|
35
|
+
end
|
36
|
+
|
37
|
+
def execute(req_body: nil, query_params: {}, headers: {}, method: 'post')
|
38
|
+
@thread = Thread.new do
|
39
|
+
url = "#{FAASTRUBY_HOST}/#{@path}#{convert_query_params(query_params)}"
|
40
|
+
uri = URI.parse(url)
|
41
|
+
use_ssl = uri.scheme == 'https' ? true : false
|
42
|
+
response = fetch(use_ssl: use_ssl, uri: uri, headers: headers, method: @methods[method], req_body: req_body)
|
43
|
+
resp_headers = {}
|
44
|
+
response.each{|k,v| resp_headers[k] = v}
|
45
|
+
case resp_headers['content-type']
|
46
|
+
when 'application/json'
|
47
|
+
begin
|
48
|
+
resp_body = Oj.load(response.body)
|
49
|
+
rescue Oj::ParseError => e
|
50
|
+
if response.body.is_a?(String)
|
51
|
+
resp_body = response.body
|
52
|
+
else
|
53
|
+
raise e if @raise_errors
|
54
|
+
resp_body = {
|
55
|
+
'error' => e.message,
|
56
|
+
'location' => e.backtrace&.first
|
57
|
+
}
|
58
|
+
end
|
58
59
|
end
|
60
|
+
when 'application/yaml'
|
61
|
+
resp_body = YAML.load(response.body)
|
62
|
+
else
|
63
|
+
resp_body = response.body
|
59
64
|
end
|
60
|
-
|
61
|
-
|
62
|
-
else
|
63
|
-
resp_body = response.body
|
65
|
+
raise FaaStRuby::RPC::ExecutionError.new("Function #{@path} returned status code #{response.code} - #{resp_body['error']} - #{resp_body['location']}") if response.code.to_i >= 400 && @raise_errors
|
66
|
+
@response = FaaStRuby::RPC::Response.new(resp_body, response.code.to_i, resp_headers)
|
64
67
|
end
|
65
|
-
|
66
|
-
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def returned?
|
72
|
+
!@response.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
def response
|
76
|
+
wait unless returned?
|
77
|
+
@response
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s
|
81
|
+
body || ""
|
82
|
+
end
|
83
|
+
|
84
|
+
def body
|
85
|
+
# wait unless returned?
|
86
|
+
response.body
|
87
|
+
end
|
88
|
+
|
89
|
+
def code
|
90
|
+
# wait unless returned?
|
91
|
+
response.code
|
92
|
+
end
|
93
|
+
|
94
|
+
def headers
|
95
|
+
# wait unless returned?
|
96
|
+
response.headers
|
97
|
+
end
|
98
|
+
|
99
|
+
def klass
|
100
|
+
# wait unless returned?
|
101
|
+
response.klass
|
67
102
|
end
|
68
103
|
|
69
104
|
private
|
105
|
+
|
106
|
+
def wait
|
107
|
+
@thread.join
|
108
|
+
end
|
109
|
+
|
70
110
|
def convert_query_params(query_params)
|
71
111
|
return "" unless query_params.any?
|
72
112
|
"?#{URI.encode_www_form(query_params)}"
|
73
113
|
end
|
74
114
|
|
75
|
-
def fetch(use_ssl:, uri:, limit: 10, method: Net::HTTP::Post, headers: {},
|
115
|
+
def fetch(use_ssl:, uri:, limit: 10, method: Net::HTTP::Post, headers: {}, req_body: nil)
|
76
116
|
# You should choose a better exception.
|
77
117
|
raise ArgumentError, 'too many HTTP redirects' if limit == 0
|
78
118
|
http = Net::HTTP.new(uri.host, uri.port)
|
@@ -81,7 +121,7 @@ module FaaStRuby
|
|
81
121
|
http.ssl_options = OpenSSL::SSL::OP_NO_SSLv2 + OpenSSL::SSL::OP_NO_SSLv3 + OpenSSL::SSL::OP_NO_COMPRESSION
|
82
122
|
end
|
83
123
|
request = method.new(uri.request_uri, headers)
|
84
|
-
request.body =
|
124
|
+
request.body = req_body
|
85
125
|
response = http.request(request)
|
86
126
|
|
87
127
|
case response
|
@@ -1,6 +1,19 @@
|
|
1
1
|
require 'faastruby-rpc'
|
2
2
|
module FaaStRuby
|
3
3
|
module RPC
|
4
|
+
@@response = {}
|
5
|
+
def self.stub_call(function_path, &block)
|
6
|
+
helper = TestHelper.new
|
7
|
+
block.call(helper)
|
8
|
+
@@response[function_path] = FaaStRuby::RPC::Response.new(helper.body, helper.code, helper.headers, helper.klass)
|
9
|
+
end
|
10
|
+
def self.stub_call?(path)
|
11
|
+
@@response[path]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.response(path)
|
15
|
+
@@response[path]
|
16
|
+
end
|
4
17
|
class TestHelper
|
5
18
|
attr_accessor :body, :code, :headers, :klass
|
6
19
|
def initialize
|
@@ -12,3 +25,39 @@ module FaaStRuby
|
|
12
25
|
end
|
13
26
|
end
|
14
27
|
end
|
28
|
+
|
29
|
+
FaaStRuby::RPC::Function.class_eval do
|
30
|
+
def execute(req_body: nil, query_params: {}, headers: {}, method: 'post')
|
31
|
+
@thread = Thread.new do
|
32
|
+
url = "#{FAASTRUBY_HOST}/#{@path}#{convert_query_params(query_params)}"
|
33
|
+
uri = URI.parse(url)
|
34
|
+
use_ssl = uri.scheme == 'https' ? true : false
|
35
|
+
response = FaaStRuby::RPC.response(@path)
|
36
|
+
resp_headers = {}
|
37
|
+
response.headers.each{|k,v| resp_headers[k] = v}
|
38
|
+
case resp_headers['content-type']
|
39
|
+
when 'application/json'
|
40
|
+
begin
|
41
|
+
resp_body = Oj.load(response.body)
|
42
|
+
rescue Oj::ParseError => e
|
43
|
+
if response.body.is_a?(String)
|
44
|
+
resp_body = response.body
|
45
|
+
else
|
46
|
+
raise e if @raise_errors
|
47
|
+
resp_body = {
|
48
|
+
'error' => e.message,
|
49
|
+
'location' => e.backtrace&.first
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
when 'application/yaml'
|
54
|
+
resp_body = YAML.load(response.body)
|
55
|
+
else
|
56
|
+
resp_body = response.body
|
57
|
+
end
|
58
|
+
raise FaaStRuby::RPC::ExecutionError.new("Function #{@path} returned status code #{response.code} - #{resp_body['error']} - #{resp_body['location']}") if response.code.to_i >= 400 && @raise_errors
|
59
|
+
@response = FaaStRuby::RPC::Response.new(resp_body, response.code.to_i, resp_headers)
|
60
|
+
end
|
61
|
+
self
|
62
|
+
end
|
63
|
+
end
|
data/lib/faastruby-rpc.rb
CHANGED
@@ -13,5 +13,15 @@ Net::HTTP.class_eval do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def invoke(function, raise_errors: true)
|
16
|
+
function(function, raise_errors: raise_errors)
|
17
|
+
end
|
18
|
+
|
19
|
+
def function(function, raise_errors: true)
|
16
20
|
FaaStRuby::RPC::Function.new(function, raise_errors: raise_errors)
|
17
21
|
end
|
22
|
+
|
23
|
+
def require_function(function, as:, raise_errors: true)
|
24
|
+
Object.send(:remove_const, as.capitalize) if Object.const_defined?(as.capitalize)
|
25
|
+
Object.const_set as.capitalize, FaaStRuby::RPC::Function.new(function, raise_errors: raise_errors)
|
26
|
+
return false
|
27
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faastruby-rpc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paulo Arruda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-12-
|
11
|
+
date: 2018-12-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|