functions_framework 0.1.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.
- checksums.yaml +7 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE +201 -0
- data/README.md +218 -0
- data/bin/functions-framework +19 -0
- data/lib/functions_framework.rb +237 -0
- data/lib/functions_framework/cli.rb +113 -0
- data/lib/functions_framework/cloud_events.rb +143 -0
- data/lib/functions_framework/cloud_events/binary_content.rb +59 -0
- data/lib/functions_framework/cloud_events/content_type.rb +139 -0
- data/lib/functions_framework/cloud_events/event.rb +277 -0
- data/lib/functions_framework/cloud_events/json_structure.rb +88 -0
- data/lib/functions_framework/function.rb +75 -0
- data/lib/functions_framework/registry.rb +137 -0
- data/lib/functions_framework/server.rb +436 -0
- data/lib/functions_framework/testing.rb +244 -0
- data/lib/functions_framework/version.rb +21 -0
- metadata +187 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
# Copyright 2020 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "base64"
|
16
|
+
require "json"
|
17
|
+
|
18
|
+
module FunctionsFramework
|
19
|
+
module CloudEvents
|
20
|
+
##
|
21
|
+
# A content handler for the JSON structure and JSON batch format.
|
22
|
+
# See https://github.com/cloudevents/spec/blob/master/json-format.md
|
23
|
+
#
|
24
|
+
module JsonStructure
|
25
|
+
class << self
|
26
|
+
##
|
27
|
+
# Decode an event from the given input string
|
28
|
+
#
|
29
|
+
# @param input [IO] An IO-like object providing a JSON-formatted string
|
30
|
+
# @param content_type [FunctionsFramework::CloudEvents::ContentType]
|
31
|
+
# the content type
|
32
|
+
# @return [FunctionsFramework::CloudEvents::Event]
|
33
|
+
#
|
34
|
+
def decode_structured_content input, content_type
|
35
|
+
input = input.read if input.respond_to? :read
|
36
|
+
charset = content_type.charset
|
37
|
+
input = input.encode charset if charset
|
38
|
+
structure = ::JSON.parse input
|
39
|
+
decode_hash_structure structure
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Decode a batch of events from the given input string
|
44
|
+
#
|
45
|
+
# @param input [IO] An IO-like object providing a JSON-formatted string
|
46
|
+
# @param content_type [FunctionsFramework::CloudEvents::ContentType]
|
47
|
+
# the content type
|
48
|
+
# @return [Array<FunctionsFramework::CloudEvents::Event>]
|
49
|
+
#
|
50
|
+
def decode_batched_content input, content_type
|
51
|
+
input = input.read if input.respond_to? :read
|
52
|
+
charset = content_type.charset
|
53
|
+
input = input.encode charset if charset
|
54
|
+
structure_array = Array(::JSON.parse(input))
|
55
|
+
structure_array.map { |structure| decode_hash_structure structure }
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Decode a single event from a hash data structure with keys and types
|
60
|
+
# conforming to the JSON event format
|
61
|
+
#
|
62
|
+
# @param structure [Hash] Input hash
|
63
|
+
# @return [FunctionsFramework::CloudEvents::Event]
|
64
|
+
#
|
65
|
+
def decode_hash_structure structure
|
66
|
+
data =
|
67
|
+
if structure.key? "data_base64"
|
68
|
+
::Base64.decode64 structure["data_base64"]
|
69
|
+
else
|
70
|
+
structure["data"]
|
71
|
+
end
|
72
|
+
spec_version = structure["specversion"]
|
73
|
+
raise "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
|
74
|
+
Event.new \
|
75
|
+
id: structure["id"],
|
76
|
+
source: structure["source"],
|
77
|
+
type: structure["type"],
|
78
|
+
spec_version: spec_version,
|
79
|
+
data: data,
|
80
|
+
data_content_type: structure["datacontenttype"],
|
81
|
+
data_schema: structure["dataschema"],
|
82
|
+
subject: structure["subject"],
|
83
|
+
time: structure["time"]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Copyright 2020 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
module FunctionsFramework
|
16
|
+
##
|
17
|
+
# Representation of a function.
|
18
|
+
#
|
19
|
+
# A function has a name, a type, and a code definition.
|
20
|
+
#
|
21
|
+
class Function
|
22
|
+
##
|
23
|
+
# Create a new function definition.
|
24
|
+
#
|
25
|
+
# @param name [String] The function name
|
26
|
+
# @param type [Symbol] The type of function. Valid types are
|
27
|
+
# `:http`, `:event`, and `:cloud_event`.
|
28
|
+
# @param block [Proc] The function code as a proc
|
29
|
+
#
|
30
|
+
def initialize name, type, &block
|
31
|
+
@name = name
|
32
|
+
@type = type
|
33
|
+
@block = block
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# @return [String] The function name
|
38
|
+
#
|
39
|
+
attr_reader :name
|
40
|
+
|
41
|
+
##
|
42
|
+
# @return [Symbol] The function type
|
43
|
+
#
|
44
|
+
attr_reader :type
|
45
|
+
|
46
|
+
##
|
47
|
+
# @return [Proc] The function code as a proc
|
48
|
+
#
|
49
|
+
attr_reader :block
|
50
|
+
|
51
|
+
##
|
52
|
+
# Call the function. You must pass an argument appropriate to the type
|
53
|
+
# of function.
|
54
|
+
#
|
55
|
+
# * A `:http` type function takes a `Rack::Request` argument, and returns
|
56
|
+
# a Rack response type. See {FunctionsFramework::Registry.add_http}.
|
57
|
+
# * A `:event` or `:cloud_event` type function takes a
|
58
|
+
# {FunctionsFramework::CloudEvents::Event} argument, and does not
|
59
|
+
# return a value. See {FunctionsFramework::Registry.add_cloud_event}.
|
60
|
+
# Note that for an `:event` type function, the passed event argument is
|
61
|
+
# split into two arguments when passed to the underlying block.
|
62
|
+
#
|
63
|
+
# @param argument [Rack::Request,FunctionsFramework::CloudEvents::Event]
|
64
|
+
# @return [Object]
|
65
|
+
#
|
66
|
+
def call argument
|
67
|
+
case type
|
68
|
+
when :event
|
69
|
+
block.call argument.data, argument
|
70
|
+
else
|
71
|
+
block.call argument
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# Copyright 2020 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "monitor"
|
16
|
+
|
17
|
+
module FunctionsFramework
|
18
|
+
##
|
19
|
+
# Registry providing lookup of functions by name.
|
20
|
+
#
|
21
|
+
class Registry
|
22
|
+
include ::MonitorMixin
|
23
|
+
|
24
|
+
##
|
25
|
+
# Create a new empty registry.
|
26
|
+
#
|
27
|
+
def initialize
|
28
|
+
super()
|
29
|
+
@functions = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Look up a function definition by name.
|
34
|
+
#
|
35
|
+
# @param name [String] The function name
|
36
|
+
# @return [FunctionsFramework::Function] if the function is found
|
37
|
+
# @return [nil] if the function is not found
|
38
|
+
#
|
39
|
+
def [] name
|
40
|
+
@functions[name.to_s]
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Returns the list of defined names
|
45
|
+
#
|
46
|
+
# @return [Array<String>]
|
47
|
+
#
|
48
|
+
def names
|
49
|
+
@functions.keys.sort
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Add an HTTP function to the registry.
|
54
|
+
#
|
55
|
+
# You must provide a name for the function, and a block that implemets the
|
56
|
+
# function. The block should take a single `Rack::Request` argument. It
|
57
|
+
# should return one of the following:
|
58
|
+
# * A standard 3-element Rack response array. See
|
59
|
+
# https://github.com/rack/rack/blob/master/SPEC
|
60
|
+
# * A `Rack::Response` object.
|
61
|
+
# * A simple String that will be sent as the response body.
|
62
|
+
# * A Hash object that will be encoded as JSON and sent as the response
|
63
|
+
# body.
|
64
|
+
#
|
65
|
+
# @param name [String] The function name
|
66
|
+
# @param block [Proc] The function code as a proc
|
67
|
+
# @return [self]
|
68
|
+
#
|
69
|
+
def add_http name, &block
|
70
|
+
name = name.to_s
|
71
|
+
synchronize do
|
72
|
+
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
73
|
+
@functions[name] = Function.new name, :http, &block
|
74
|
+
end
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Add a CloudEvent function to the registry.
|
80
|
+
#
|
81
|
+
# You must provide a name for the function, and a block that implemets the
|
82
|
+
# function. The block should take two arguments: the event _data_ and the
|
83
|
+
# event _context_. Any return value is ignored.
|
84
|
+
#
|
85
|
+
# The event data argument will be one of the following types:
|
86
|
+
# * A `String` (with encoding `ASCII-8BIT`) if the data is in the form of
|
87
|
+
# binary data. You may choose to perform additional interpretation of
|
88
|
+
# the binary data using information in the content type provided by the
|
89
|
+
# context argument.
|
90
|
+
# * Any data type that can be represented in JSON (i.e. `String`,
|
91
|
+
# `Integer`, `Array`, `Hash`, `true`, `false`, or `nil`) if the event
|
92
|
+
# came with a JSON payload. The content type may also be set in the
|
93
|
+
# context if the data is a String.
|
94
|
+
#
|
95
|
+
# The context argument will be of type {FunctionsFramework::CloudEvents::Event},
|
96
|
+
# and will contain CloudEvents context attributes such as `id` and `type`.
|
97
|
+
#
|
98
|
+
# See also {#add_cloud_event} which creates a function that takes a single
|
99
|
+
# argument of type {FunctionsFramework::CloudEvents::Event}.
|
100
|
+
#
|
101
|
+
# @param name [String] The function name
|
102
|
+
# @param block [Proc] The function code as a proc
|
103
|
+
# @return [self]
|
104
|
+
#
|
105
|
+
def add_event name, &block
|
106
|
+
name = name.to_s
|
107
|
+
synchronize do
|
108
|
+
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
109
|
+
@functions[name] = Function.new name, :event, &block
|
110
|
+
end
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Add a CloudEvent function to the registry.
|
116
|
+
#
|
117
|
+
# You must provide a name for the function, and a block that implemets the
|
118
|
+
# function. The block should take _one_ argument: the event object of type
|
119
|
+
# {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
|
120
|
+
#
|
121
|
+
# See also {#add_event} which creates a function that takes data and
|
122
|
+
# context as separate arguments.
|
123
|
+
#
|
124
|
+
# @param name [String] The function name
|
125
|
+
# @param block [Proc] The function code as a proc
|
126
|
+
# @return [self]
|
127
|
+
#
|
128
|
+
def add_cloud_event name, &block
|
129
|
+
name = name.to_s
|
130
|
+
synchronize do
|
131
|
+
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
132
|
+
@functions[name] = Function.new name, :cloud_event, &block
|
133
|
+
end
|
134
|
+
self
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,436 @@
|
|
1
|
+
# Copyright 2020 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "json"
|
16
|
+
require "monitor"
|
17
|
+
|
18
|
+
require "puma"
|
19
|
+
require "puma/server"
|
20
|
+
require "rack"
|
21
|
+
|
22
|
+
module FunctionsFramework
|
23
|
+
##
|
24
|
+
# A web server that wraps a function.
|
25
|
+
#
|
26
|
+
class Server
|
27
|
+
include ::MonitorMixin
|
28
|
+
|
29
|
+
##
|
30
|
+
# Create a new web server given a function. Yields a
|
31
|
+
# {FunctionsFramework::Server::Config} object that you can use to set
|
32
|
+
# server configuration parameters. This block is the only opportunity to
|
33
|
+
# set configuration; once the server is initialized, configuration is
|
34
|
+
# frozen.
|
35
|
+
#
|
36
|
+
# @param function [FunctionsFramework::Function] The function to execute.
|
37
|
+
# @yield [FunctionsFramework::Server::Config] A config object that can be
|
38
|
+
# manipulated to configure this server.
|
39
|
+
#
|
40
|
+
def initialize function
|
41
|
+
super()
|
42
|
+
@config = Config.new
|
43
|
+
yield @config if block_given?
|
44
|
+
@config.freeze
|
45
|
+
@function = function
|
46
|
+
@app =
|
47
|
+
case function.type
|
48
|
+
when :http
|
49
|
+
HttpApp.new function, @config
|
50
|
+
when :event, :cloud_event
|
51
|
+
EventApp.new function, @config
|
52
|
+
else
|
53
|
+
raise "Unrecognized function type: #{function.type}"
|
54
|
+
end
|
55
|
+
@server = nil
|
56
|
+
@signals_installed = false
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# The function to execute.
|
61
|
+
# @return [FunctionsFramework::Function]
|
62
|
+
#
|
63
|
+
attr_reader :function
|
64
|
+
|
65
|
+
##
|
66
|
+
# The final configuration. This is a frozen object that cannot be modified.
|
67
|
+
# @return [FunctionsFramework::Server::Config]
|
68
|
+
#
|
69
|
+
attr_reader :config
|
70
|
+
|
71
|
+
##
|
72
|
+
# Start the web server in the background. Does nothing if the web server
|
73
|
+
# is already running.
|
74
|
+
#
|
75
|
+
# @return [self]
|
76
|
+
#
|
77
|
+
def start
|
78
|
+
synchronize do
|
79
|
+
unless running?
|
80
|
+
@server = ::Puma::Server.new @app
|
81
|
+
@server.min_threads = @config.min_threads
|
82
|
+
@server.max_threads = @config.max_threads
|
83
|
+
@server.leak_stack_on_error = @config.show_error_details?
|
84
|
+
@server.binder.add_tcp_listener @config.bind_addr, @config.port
|
85
|
+
@server.run true
|
86
|
+
@config.logger.info "FunctionsFramework: Serving function #{@function.name.inspect}" \
|
87
|
+
" on port #{@config.port}..."
|
88
|
+
end
|
89
|
+
end
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Stop the web server in the background. Does nothing if the web server
|
95
|
+
# is not running.
|
96
|
+
#
|
97
|
+
# @param force [Boolean] Use a forced halt instead of a graceful shutdown
|
98
|
+
# @param wait [Boolean] Block until shutdown is complete
|
99
|
+
# @return [self]
|
100
|
+
#
|
101
|
+
def stop force: false, wait: false
|
102
|
+
synchronize do
|
103
|
+
if running?
|
104
|
+
@config.logger.info "FunctionsFramework: Shutting down server..."
|
105
|
+
if force
|
106
|
+
@server.halt wait
|
107
|
+
else
|
108
|
+
@server.stop wait
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Wait for the server to stop. Returns immediately if the server is not
|
117
|
+
# running.
|
118
|
+
#
|
119
|
+
# @param timeout [nil,Numeric] The timeout. If `nil` (the default), waits
|
120
|
+
# indefinitely, otherwise times out after the given number of seconds.
|
121
|
+
# @return [self]
|
122
|
+
#
|
123
|
+
def wait_until_stopped timeout: nil
|
124
|
+
@server&.thread&.join timeout
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Determine if the web server is currently running
|
130
|
+
#
|
131
|
+
# @return [Boolean]
|
132
|
+
#
|
133
|
+
def running?
|
134
|
+
@server&.thread&.alive?
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Cause this server to respond to SIGTERM, SIGINT, and SIGHUP by shutting
|
139
|
+
# down gracefully.
|
140
|
+
#
|
141
|
+
# @return [self]
|
142
|
+
#
|
143
|
+
def respond_to_signals
|
144
|
+
synchronize do
|
145
|
+
return self if @signals_installed
|
146
|
+
::Signal.trap "SIGTERM" do
|
147
|
+
Server.signal_enqueue "SIGTERM", @config.logger, @server
|
148
|
+
end
|
149
|
+
::Signal.trap "SIGINT" do
|
150
|
+
Server.signal_enqueue "SIGINT", @config.logger, @server
|
151
|
+
end
|
152
|
+
::Signal.trap "SIGHUP" do
|
153
|
+
Server.signal_enqueue "SIGHUP", @config.logger, @server
|
154
|
+
end
|
155
|
+
@signals_installed = true
|
156
|
+
end
|
157
|
+
self
|
158
|
+
end
|
159
|
+
|
160
|
+
class << self
|
161
|
+
## @private
|
162
|
+
def start_signal_queue
|
163
|
+
@signal_queue = ::Queue.new
|
164
|
+
::Thread.start do
|
165
|
+
loop do
|
166
|
+
signal, logger, server = @signal_queue.pop
|
167
|
+
logger.info "FunctionsFramework: Caught #{signal}; shutting down server..."
|
168
|
+
server&.stop
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
## @private
|
174
|
+
def signal_enqueue signal, logger, server
|
175
|
+
@signal_queue << [signal, logger, server]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
start_signal_queue
|
180
|
+
|
181
|
+
##
|
182
|
+
# The web server configuration. This object is yielded from the
|
183
|
+
# {FunctionsFramework::Server} constructor and can be modified at that
|
184
|
+
# point. Afterward, it is available from {FunctionsFramework::Server#config}
|
185
|
+
# but it is frozen.
|
186
|
+
#
|
187
|
+
class Config
|
188
|
+
##
|
189
|
+
# Create a new config object with the default settings
|
190
|
+
#
|
191
|
+
def initialize
|
192
|
+
self.rack_env = nil
|
193
|
+
self.bind_addr = nil
|
194
|
+
self.port = nil
|
195
|
+
self.min_threads = nil
|
196
|
+
self.max_threads = nil
|
197
|
+
self.show_error_details = nil
|
198
|
+
self.logger = nil
|
199
|
+
end
|
200
|
+
|
201
|
+
##
|
202
|
+
# Set the Rack environment, or `nil` to use the default.
|
203
|
+
# @param rack_env [String,nil]
|
204
|
+
#
|
205
|
+
def rack_env= rack_env
|
206
|
+
@rack_env = rack_env || ::ENV["RACK_ENV"] ||
|
207
|
+
(::ENV["K_REVISION"] ? "production" : "development")
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Set the bind address, or `nil` to use the default.
|
212
|
+
# @param bind_addr [String,nil]
|
213
|
+
#
|
214
|
+
def bind_addr= bind_addr
|
215
|
+
@bind_addr = bind_addr || ::ENV["BIND_ADDR"] || "0.0.0.0"
|
216
|
+
end
|
217
|
+
|
218
|
+
##
|
219
|
+
# Set the port number, or `nil` to use the default.
|
220
|
+
# @param port [Integer,nil]
|
221
|
+
#
|
222
|
+
def port= port
|
223
|
+
@port = (port || ::ENV["PORT"] || 8080).to_i
|
224
|
+
end
|
225
|
+
|
226
|
+
##
|
227
|
+
# Set the minimum number of worker threads, or `nil` to use the default.
|
228
|
+
# @param min_threads [Integer,nil]
|
229
|
+
#
|
230
|
+
def min_threads= min_threads
|
231
|
+
@min_threads = (min_threads || ::ENV["MIN_THREADS"])&.to_i
|
232
|
+
end
|
233
|
+
|
234
|
+
##
|
235
|
+
# Set the maximum number of worker threads, or `nil` to use the default.
|
236
|
+
# @param max_threads [Integer,nil]
|
237
|
+
#
|
238
|
+
def max_threads= max_threads
|
239
|
+
@max_threads = (max_threads || ::ENV["MAX_THREADS"])&.to_i
|
240
|
+
end
|
241
|
+
|
242
|
+
##
|
243
|
+
# Set whether to show detailed error messages, or `nil` to use the default.
|
244
|
+
# @param show_error_details [Boolean,nil]
|
245
|
+
#
|
246
|
+
def show_error_details= show_error_details
|
247
|
+
val = show_error_details.nil? ? ::ENV["DETAILED_ERRORS"] : show_error_details
|
248
|
+
@show_error_details = val ? true : false
|
249
|
+
end
|
250
|
+
|
251
|
+
##
|
252
|
+
# Set the logger for server messages, or `nil` to use the global default.
|
253
|
+
# @param logger [Logger]
|
254
|
+
#
|
255
|
+
def logger= logger
|
256
|
+
@logger = logger || ::FunctionsFramework.logger
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Returns the current Rack environment.
|
261
|
+
# @return [String]
|
262
|
+
#
|
263
|
+
def rack_env
|
264
|
+
@rack_env
|
265
|
+
end
|
266
|
+
|
267
|
+
##
|
268
|
+
# Returns the current bind address.
|
269
|
+
# @return [String]
|
270
|
+
#
|
271
|
+
def bind_addr
|
272
|
+
@bind_addr
|
273
|
+
end
|
274
|
+
|
275
|
+
##
|
276
|
+
# Returns the current port number.
|
277
|
+
# @return [Integer]
|
278
|
+
#
|
279
|
+
def port
|
280
|
+
@port
|
281
|
+
end
|
282
|
+
|
283
|
+
##
|
284
|
+
# Returns the minimum number of worker threads in the thread pool.
|
285
|
+
# @return [Integer]
|
286
|
+
#
|
287
|
+
def min_threads
|
288
|
+
@min_threads || 1
|
289
|
+
end
|
290
|
+
|
291
|
+
##
|
292
|
+
# Returns the maximum number of worker threads in the thread pool.
|
293
|
+
# @return [Integer]
|
294
|
+
#
|
295
|
+
def max_threads
|
296
|
+
@max_threads || (@rack_env == "development" ? 1 : 16)
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# Returns whether to show detailed error messages.
|
301
|
+
# @return [Boolean]
|
302
|
+
#
|
303
|
+
def show_error_details?
|
304
|
+
@show_error_details.nil? ? (@rack_env == "development") : @show_error_details
|
305
|
+
end
|
306
|
+
|
307
|
+
##
|
308
|
+
# Returns the logger.
|
309
|
+
# @return [Logger]
|
310
|
+
#
|
311
|
+
def logger
|
312
|
+
@logger
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
## @private
|
317
|
+
class AppBase
|
318
|
+
BLACKLISTED_PATHS = ["/favicon.ico", "/robots.txt"].freeze
|
319
|
+
|
320
|
+
def initialize config
|
321
|
+
@config = config
|
322
|
+
end
|
323
|
+
|
324
|
+
def blacklisted_path? env
|
325
|
+
path = env[::Rack::SCRIPT_NAME].to_s + env[::Rack::PATH_INFO].to_s
|
326
|
+
BLACKLISTED_PATHS.include? path
|
327
|
+
end
|
328
|
+
|
329
|
+
def interpret_response response
|
330
|
+
case response
|
331
|
+
when ::Array
|
332
|
+
response
|
333
|
+
when ::Rack::Response
|
334
|
+
response.finish
|
335
|
+
when ::String
|
336
|
+
string_response response, "text/plain", 200
|
337
|
+
when ::Hash
|
338
|
+
json = ::JSON.dump response
|
339
|
+
string_response json, "application/json", 200
|
340
|
+
when ::StandardError
|
341
|
+
error = error_message response
|
342
|
+
string_response error, "text/plain", 500
|
343
|
+
else
|
344
|
+
e = ::StandardError.new "Unexpected response type: #{response.class}"
|
345
|
+
error = error_message e
|
346
|
+
string_response error, "text/plain", 500
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def notfound_response
|
351
|
+
string_response "Not found", "text/plain", 404
|
352
|
+
end
|
353
|
+
|
354
|
+
def string_response string, content_type, status
|
355
|
+
headers = {
|
356
|
+
"Content-Type" => content_type,
|
357
|
+
"Content-Length" => string.bytesize
|
358
|
+
}
|
359
|
+
[status, headers, [string]]
|
360
|
+
end
|
361
|
+
|
362
|
+
def error_message error
|
363
|
+
if @config.show_error_details?
|
364
|
+
"#{error.class}: #{error.message}\n#{error.backtrace}\n"
|
365
|
+
else
|
366
|
+
"Unexpected internal error"
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def usage_message error
|
371
|
+
if @config.show_error_details?
|
372
|
+
"Failed to decode CloudEvent: #{error.inspect}"
|
373
|
+
else
|
374
|
+
"Failed to decode CloudEvent"
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
## @private
|
380
|
+
class HttpApp < AppBase
|
381
|
+
def initialize function, config
|
382
|
+
super config
|
383
|
+
@function = function
|
384
|
+
end
|
385
|
+
|
386
|
+
def call env
|
387
|
+
return notfound_response if blacklisted_path? env
|
388
|
+
response =
|
389
|
+
begin
|
390
|
+
logger = env["rack.logger"] = @config.logger
|
391
|
+
request = ::Rack::Request.new env
|
392
|
+
logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
|
393
|
+
@function.call request
|
394
|
+
rescue ::StandardError => e
|
395
|
+
logger.warn e
|
396
|
+
e
|
397
|
+
end
|
398
|
+
interpret_response response
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
## @private
|
403
|
+
class EventApp < AppBase
|
404
|
+
def initialize function, config
|
405
|
+
super config
|
406
|
+
@function = function
|
407
|
+
end
|
408
|
+
|
409
|
+
def call env
|
410
|
+
return notfound_response if blacklisted_path? env
|
411
|
+
logger = env["rack.logger"] = @config.logger
|
412
|
+
event =
|
413
|
+
begin
|
414
|
+
CloudEvents.decode_rack_env env
|
415
|
+
rescue ::StandardError => e
|
416
|
+
e
|
417
|
+
end
|
418
|
+
response =
|
419
|
+
if event.is_a? CloudEvents::Event
|
420
|
+
logger.info "FunctionsFramework: Handling CloudEvent"
|
421
|
+
begin
|
422
|
+
@function.call event
|
423
|
+
"ok"
|
424
|
+
rescue ::StandardError => e
|
425
|
+
logger.warn e
|
426
|
+
e
|
427
|
+
end
|
428
|
+
else
|
429
|
+
logger.warn e.inspect
|
430
|
+
string_response usage_message(e), "text/plain", 400
|
431
|
+
end
|
432
|
+
interpret_response response
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|