functions_framework 0.0.0 → 0.1.0

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.
@@ -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,423 @@
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
+ def initialize config
319
+ @config = config
320
+ end
321
+
322
+ def interpret_response response
323
+ case response
324
+ when ::Array
325
+ response
326
+ when ::Rack::Response
327
+ response.finish
328
+ when ::String
329
+ string_response response, "text/plain", 200
330
+ when ::Hash
331
+ json = ::JSON.dump response
332
+ string_response json, "application/json", 200
333
+ when ::StandardError
334
+ error = error_message response
335
+ string_response error, "text/plain", 500
336
+ else
337
+ e = ::StandardError.new "Unexpected response type: #{response.class}"
338
+ error = error_message e
339
+ string_response error, "text/plain", 500
340
+ end
341
+ end
342
+
343
+ def string_response string, content_type, status
344
+ headers = {
345
+ "Content-Type" => content_type,
346
+ "Content-Length" => string.bytesize
347
+ }
348
+ [status, headers, [string]]
349
+ end
350
+
351
+ def error_message error
352
+ if @config.show_error_details?
353
+ "#{error.class}: #{error.message}\n#{error.backtrace}\n"
354
+ else
355
+ "Unexpected internal error"
356
+ end
357
+ end
358
+
359
+ def usage_message error
360
+ if @config.show_error_details?
361
+ "Failed to decode CloudEvent: #{error.inspect}"
362
+ else
363
+ "Failed to decode CloudEvent"
364
+ end
365
+ end
366
+ end
367
+
368
+ ## @private
369
+ class HttpApp < AppBase
370
+ def initialize function, config
371
+ super config
372
+ @function = function
373
+ end
374
+
375
+ def call env
376
+ response =
377
+ begin
378
+ logger = env["rack.logger"] = @config.logger
379
+ request = ::Rack::Request.new env
380
+ logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
381
+ @function.call request
382
+ rescue ::StandardError => e
383
+ logger.warn e
384
+ e
385
+ end
386
+ interpret_response response
387
+ end
388
+ end
389
+
390
+ ## @private
391
+ class EventApp < AppBase
392
+ def initialize function, config
393
+ super config
394
+ @function = function
395
+ end
396
+
397
+ def call env
398
+ logger = env["rack.logger"] = @config.logger
399
+ event =
400
+ begin
401
+ CloudEvents.decode_rack_env env
402
+ rescue ::StandardError => e
403
+ e
404
+ end
405
+ response =
406
+ if event.is_a? CloudEvents::Event
407
+ logger.info "FunctionsFramework: Handling CloudEvent"
408
+ begin
409
+ @function.call event
410
+ "ok"
411
+ rescue ::StandardError => e
412
+ logger.warn e
413
+ e
414
+ end
415
+ else
416
+ logger.warn e.inspect
417
+ string_response usage_message(e), "text/plain", 400
418
+ end
419
+ interpret_response response
420
+ end
421
+ end
422
+ end
423
+ end