functions_framework 0.0.0 → 0.1.0

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