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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6156f5bfcd486038697b1ba4e232093527fe1794c7616ab12405047a1e2659fd
4
- data.tar.gz: 8cc0fd2b4c0c954bf71034335a77696ec96d337e8cae58ad587722772d5f1be8
3
+ metadata.gz: e0399784305d0de9f8ee8db0f602cc2062a1c1d8d631bdfd0514c1d5b846e7c0
4
+ data.tar.gz: f437fe5dce46945b840117d3cfa6a0b68699046b7f0a64ba7822fd2278dcc4cc
5
5
  SHA512:
6
- metadata.gz: 4af94ef769bfe71c805c5896ce1d07bba252f335ccad7c856189fcfc9c40d1dcd6175db6db08e200cb92c0d16bedf216b143cd64ea706703599793c7ba6bfbb0
7
- data.tar.gz: 06cd23c88f89e7394b9374dfadf5d597c38f45435f7500dd66864e6fd7a56396dd18754cfe2818af7746de3f61deb2629fdd64d81a6982217b9e921ef983d29b
6
+ metadata.gz: ab2e9df498673377e78ba3d94103a4cc07daca5ce7bcd601aede51ded5e60a74560821e5cc51089a988fbfd45a448c14a88dcb5825ea79d5a2facf1ad2d70643
7
+ data.tar.gz: a946ba11384ce3d9ec7a090f9be8405b9a69e69400fe153cff602a2cc2fd35e815c7474296f9dde19c8128f4c7e5db68396cf62e490d974aaf00dedc4fe8b222
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - Dec 31 2018
4
+ - Redesigned UX for calling of external functions
5
+
3
6
  ## 0.1.3 - Dec 15 2018
4
7
  - Add test helper to stub invoke()
5
8
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- faastruby-rpc (0.1.3)
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.4)
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.16.5
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/tutorial.html)
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 a function, use the helper method `invoke`:
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
- # You need the function path, which is WORKSPACE_NAME/FUNCTION_NAME
16
- function = 'paulo/hello'
17
- # Invoke the function the get the result
18
- result = invoke(function).call
19
- # Or passing arguments:
20
- result = invoke(function).with('Paulo', likes_ruby: true)
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
- `result` is a Struct with the following attributes:
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
- The arguments in `with` are passed as arguments to your function (after the `event`). You can capture them with positional arguments, keyword arguments or just a generic `*args` if you want to have the flexibility of sending a variable number of arguments.
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
- Here is the source code of `paulo/hello`:
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
- def handler event, name = nil
34
- response = name ? "Hello, #{name}!" : 'Hello, there!'
35
- render text: response
43
+ # Note the required keyword argument 'name:'
44
+ def handler(event, name:)
45
+ render text: name
36
46
  end
37
47
  ```
38
48
 
39
- When you call `invoke`, a request is sent with the following properties:
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
- # Calling a function that way defaults to method=GET
48
- FaaStRuby::RPC::Function.new("FUNCTION_PATH").call(body: nil, query_params: {}, headers: {}, method: 'get')
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 than 400. This is important to make your functions easier to debug, and you will always know what to expect from that function call.
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` to the `invoke` method, or to `FaaStRuby::RPC::Function.new`. Example:
66
+ To disable this behaviour, pass `raise_errors: false` when requiring the function. For example:
58
67
 
59
68
  ```ruby
60
- invoke('paulo/hello', raise_errors: false).call
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 invoke() in your function tests
66
- If you are testing a function that invokes another one, you likely will want to fake that call. To do that, use the following test helper:
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 cause invoke('paulo/hello-world')... to fake the call to
70
- # 'paulo/hello-world' and instead return the values you pass in the block.
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 with(*args)
35
- call(body: Oj.dump(args), headers: {'Content-Type' => 'application/json', 'Faastruby-Rpc' => 'true'})
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(body: nil, query_params: {}, headers: {}, method: 'post')
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 = FaaStRuby::RPC.stub_call?(@path) ? FaaStRuby::RPC.response(@path) : fetch(use_ssl: use_ssl, uri: uri, headers: headers, method: @methods[method], body: 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
- }
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
- when 'application/yaml'
61
- resp_body = YAML.load(response.body)
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
- 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.new(resp_body, response.code.to_i, resp_headers)
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: {}, body: nil)
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 = 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
@@ -1,5 +1,5 @@
1
1
  module FaaStRuby
2
2
  module RPC
3
- VERSION = '0.1.3'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  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.1.3
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-15 00:00:00.000000000 Z
11
+ date: 2018-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj