functions_framework 0.6.0 → 0.7.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: 67b0d5e61bb58f49adefbff9794c960438c1577b4e3d54ee5bf072b134834b71
4
- data.tar.gz: 9cb295fd8ba39ea9ad5e8eab1c3055eaa18dddef5a1838cc58d7dcdb2d12fc5e
3
+ metadata.gz: faa749d3b692f754f7ad60dfc3a5b18c2b9b26c757769ab51e16786a6289b4ad
4
+ data.tar.gz: 8d918a95adc8caa077fdc19fbc1ec3f3b531a6de279b19da2282bbd4479305d5
5
5
  SHA512:
6
- metadata.gz: 99e58e1511a97dc9e8643a034bc089f5bb4b71d5f842b507151d8728f058a3763b0f55dcd952ac71f212773e6087cb333fcfebb814a33f57cd2057fde09d6272
7
- data.tar.gz: e5afdebdc20c069c247e58196d75787a8c3b3e51924b7714851006ade62dc70bce62cd9e42501528103eb41f3ffc2d6239e9dd8c6650798bdbd1693268fa431a
6
+ metadata.gz: 8e372039d4885f765f634f1f90975282b12c28f97acf7ba9a5ad4dba100a444c5d6a494a8792a9ed2c2fe6cb6f969ee886dab0f26eda8334ebe21288d506413e
7
+ data.tar.gz: 1f61e76bbc9ba84283e120e77da66dc488ded6178bd07691d42d8c5472605b8d839dd85c75747b3611dfe87bffae3db2d2a4803389ff56fb3b0415f663fce2b3
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ### v0.7.0 / 2020-09-25
4
+
5
+ * Now requires Ruby 2.5 or later.
6
+ * BREAKING CHANGE: Renamed "context" hash to "globals" and made it read-only for normal functions.
7
+ * BREAKING CHANGE: Server config is no longer passed to startup blocks.
8
+ * ADDED: Provided a "logger" convenience method in the context object.
9
+ * ADDED: Globals can be set from startup blocks, which is useful for initializing shared resources.
10
+ * ADDED: Support for testing startup tasks in the Testing module.
11
+ * ADDED: Support for controlling logging in the Testing module.
12
+ * FIXED: Fixed crash introduced in 0.6.0 when a block didn't declare an expected argument.
13
+ * FIXED: Better support for running concurrent tests.
14
+ * DOCS: Expanded documentation on initialization, execution context, and shared resources.
15
+ * DEPRECATED: The functions-framework executable is deprecated. Use functions-framework-ruby instead.
16
+
3
17
  ### v0.6.0 / 2020-09-17
4
18
 
5
19
  * ADDED: You can use the --version flag to print the framework version
data/README.md CHANGED
@@ -42,11 +42,11 @@ requiring an HTTP server or complicated request handling logic.
42
42
 
43
43
  ## Supported Ruby versions
44
44
 
45
- This library is supported on Ruby 2.4+.
45
+ This library is supported on Ruby 2.5+.
46
46
 
47
47
  Google provides official support for Ruby versions that are actively supported
48
48
  by Ruby Core—that is, Ruby versions that are either in normal maintenance or
49
- in security maintenance, and not end of life. Currently, this means Ruby 2.4
49
+ in security maintenance, and not end of life. Currently, this means Ruby 2.5
50
50
  and later. Older versions of Ruby _may_ still work, but are unsupported and not
51
51
  recommended. See https://www.ruby-lang.org/en/downloads/branches/ for details
52
52
  about the Ruby support schedule.
@@ -60,7 +60,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
60
60
  ```ruby
61
61
  # Gemfile
62
62
  source "https://rubygems.org"
63
- gem "functions_framework", "~> 0.5"
63
+ gem "functions_framework", "~> 0.7"
64
64
  ```
65
65
 
66
66
  Create a file called `app.rb` and include the following code. This defines a
@@ -16,4 +16,7 @@
16
16
 
17
17
  require "functions_framework/cli"
18
18
 
19
+ puts "WARNING: The functions-framework executable is deprecated and will be"
20
+ puts "removed in a future version. Please use functions-framework-ruby instead."
21
+
19
22
  ::FunctionsFramework::CLI.new.parse_args(::ARGV).run.complete
@@ -46,11 +46,11 @@ requiring an HTTP server or complicated request handling logic.
46
46
 
47
47
  ## Supported Ruby versions
48
48
 
49
- This library is supported on Ruby 2.4+.
49
+ This library is supported on Ruby 2.5+.
50
50
 
51
51
  Google provides official support for Ruby versions that are actively supported
52
52
  by Ruby Core—that is, Ruby versions that are either in normal maintenance or
53
- in security maintenance, and not end of life. Currently, this means Ruby 2.4
53
+ in security maintenance, and not end of life. Currently, this means Ruby 2.5
54
54
  and later. Older versions of Ruby _may_ still work, but are unsupported and not
55
55
  recommended. See https://www.ruby-lang.org/en/downloads/branches/ for details
56
56
  about the Ruby support schedule.
@@ -64,7 +64,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
64
64
  ```ruby
65
65
  # Gemfile
66
66
  source "https://rubygems.org"
67
- gem "functions_framework", "~> 0.5"
67
+ gem "functions_framework", "~> 0.7"
68
68
  ```
69
69
 
70
70
  Create a file called `app.rb` and include the following code. This defines a
@@ -165,3 +165,53 @@ class MyTest < Minitest::Test
165
165
  end
166
166
  end
167
167
  ```
168
+
169
+ ## Testing startup tasks
170
+
171
+ When a functions server is starting up, it calls startup tasks automatically.
172
+ In the testing environment, when you call a function using the
173
+ {FunctionsFramework::Testing#call_http} or
174
+ {FunctionsFramework::Testing#call_event} methods, the testing environment will
175
+ also automatically execute any startup tasks for you.
176
+
177
+ You can also call startup tasks explicitly to test them in isolation, using the
178
+ {FunctionsFramework::Testing#run_startup_tasks} method. Pass the name of a
179
+ function, and the testing module will execute all defined startup blocks, in
180
+ order, as if the server were preparing that function for execution.
181
+ {FunctionsFramework::Testing#run_startup_tasks} returns the resulting globals
182
+ as a hash, so you can assert against its contents.
183
+
184
+ If you use {FunctionsFramework::Testing#run_startup_tasks} to run the startup
185
+ tasks explicitly, they will not be run again when you call the function itself
186
+ using {FunctionsFramework::Testing#call_http} or
187
+ {FunctionsFramework::Testing#call_event}. However, if startup tasks have
188
+ already been run implicitly by {FunctionsFramework::Testing#call_http} or
189
+ {FunctionsFramework::Testing#call_event}, then attempting to run them again
190
+ explicitly by calling {FunctionsFramework::Testing#run_startup_tasks} will
191
+ result in an exception.
192
+
193
+ There is currently no way to run a single startup block in isolation. If you
194
+ have multiple startup blocks defined, they are always executed together.
195
+
196
+ Following is an example test that runs startup tasks explicitly and asserts
197
+ against the effect on the globals.
198
+
199
+ ```ruby
200
+ require "minitest/autorun"
201
+ require "functions_framework/testing"
202
+
203
+ class MyTest < Minitest::Test
204
+ include FunctionsFramework::Testing
205
+
206
+ def test_startup_tasks
207
+ load_temporary "app.rb" do
208
+ globals = run_startup_tasks "my_function"
209
+ assert_equal "foo", globals[:my_global]
210
+
211
+ request = make_get_request "https://example.com/foo"
212
+ response = call_http "my_function", request
213
+ assert_equal 200, response.status
214
+ end
215
+ end
216
+ end
217
+ ```
@@ -111,7 +111,7 @@ dependency on Sinatra in your `Gemfile`:
111
111
 
112
112
  ```ruby
113
113
  source "https://rubygems.org"
114
- gem "functions_framework", "~> 0.5"
114
+ gem "functions_framework", "~> 0.7"
115
115
  gem "sinatra", "~> 2.0"
116
116
  ```
117
117
 
@@ -197,67 +197,191 @@ FunctionsFramework.http "error_reporter" do |request|
197
197
  end
198
198
  ```
199
199
 
200
- ## Shared resources
201
-
202
- Generally, functions should be self-contained and stateless, and should not use
203
- or share any global state in the Ruby VM. This is because serverless runtimes
204
- may spin up or terminate instances of your app at any time, and because a
205
- single instance may be running multiple functions at a time in separate threads.
206
-
207
- However, it is sometimes useful to share a resource across multiple function
208
- invocations that run on the same Ruby instance. For example, you might establish
209
- a single connection to a remote database or other service, and share it across
210
- function invocations to avoid incurring the overhead of re-establishing it
211
- for every function invocation.
212
-
213
- When using a shared resource, it is important to keep three things in mind:
214
-
215
- 1. **The shared resource should be thread-safe.** This is because serverless
216
- runtimes such as Google Cloud Functions may run multiple functions at a time
217
- in separate threads.
218
-
219
- 2. **Use `FunctionsFramework.on_startup` to initialize shared resources.**
220
- Do not initialize a shared resource at the top level of your app. This is
221
- because serverless runtimes may load your files (and thus execute any Ruby
222
- code at the top level) in a build/deployment environment that may not be
223
- equipped to support the resource. Instead, initialize resources in a
224
- `FunctionsFramework.on_startup` block, which the Functions Framework will
225
- call only just before starting a server.
226
-
227
- For example:
228
-
229
- ```ruby
230
- require "functions_framework"
231
-
232
- # This local variable is lexically shared among all blocks.
233
- storage_client = nil
234
-
235
- # Do not create the storage client here. This may run during deployment
236
- # when, e.g., your storage credentials are not accessible.
237
- # require "google/cloud/storage"
238
- # storage_client = Google::Cloud::Storage.new # <- may fail
239
-
240
- # Use an on_startup block to initialize the shared client.
241
- # This block runs only when the framework is starting an actual server,
242
- # and is guaranteed to complete before any functions are executed.
243
- FunctionsFramework.on_startup do
244
- require "google/cloud/storage"
245
- storage_client = Google::Cloud::Storage.new
246
- end
247
-
248
- # The storage_client is shared among all function invocations
249
- FunctionsFramework.http "storage_example" do |request|
250
- bucket = storage_client.bucket "my-bucket"
251
- file = bucket.file "path/to/my-file.txt"
252
- file.download.to_s
253
- end
254
- ```
255
-
256
- 3. **There is no guaranteed cleanup hook.** The Functions Framework does not
257
- provide a guaranteed way to register a cleanup task. You can register a
258
- `Kernel.at_exit` task, but remember that it is possible for the Ruby VM to
259
- terminate without calling it. It is strongly recommended that you use
260
- resources that do not require "cleanup".
200
+ ## The runtime environment
201
+
202
+ A serverless environment may be somewhat different from server-based runtime
203
+ environments you might be used to. Serverless runtimes often provide a simpler
204
+ programming model, transparent scaling, and cost savings, but they do so by
205
+ controlling how your code is managed and executed. The Functions Framework is
206
+ designed around a "functions-as-a-service" (FaaS) paradigm, which runs
207
+ self-contained stateless functions that have an input and a return value. It's
208
+ important to understand what that means for your Ruby code in order to get the
209
+ most out of a cloud serverless product.
210
+
211
+ For example, multithreading is a core element of the Functions Framework. When
212
+ you write functions, you should assume that multiple executions may be taking
213
+ place concurrently in different threads, and thus you should avoid operations
214
+ that can cause concurrency issues or race conditions. The easiest way to do
215
+ this is to make your functions self-contained and stateless. Avoid global
216
+ variables and don't share mutable data between different function executions.
217
+
218
+ Additionally, a serverless runtime may throttle the CPU whenever no actual
219
+ function executions are taking place. This lets it reduce the CPU resources
220
+ used (and therefore the cost to you), while keeping your application warmed up
221
+ and ready to respond to new requests quickly. An important implication, though,
222
+ is that you should avoid starting up background threads or processes. They may
223
+ not get any CPU time during periods when your Ruby application is not actually
224
+ executing a function.
225
+
226
+ In the sections below, we'll discuss a few techniques and features of the
227
+ Functions Framework to help you write Ruby code that fits well into a
228
+ serverless paradigm.
229
+
230
+ ### Startup tasks
231
+
232
+ It is sometimes useful to perform one-time initialization that applies to many
233
+ function executions, for example to warm up caches, perform precomputation, or
234
+ establish shared remote connections. To run code during initialization, use
235
+ {FunctionsFramework.on_startup} to define a _startup task_.
236
+
237
+ ```ruby
238
+ require "functions_framework"
239
+
240
+ FunctionsFramework.on_startup do |function|
241
+ # Perform initialization here.
242
+ require "my_cache"
243
+ MyCache.warmup
244
+ end
245
+
246
+ FunctionsFramework.http "hello" do |request|
247
+ # Initialization will be done by the time a normal function is called.
248
+ end
249
+ ```
250
+
251
+ Startup tasks are run once per Ruby instance, before the framework starts
252
+ receiving requests and executing functions. You can define multiple startup
253
+ tasks, and they will run in order, and are guaranteed to complete before any
254
+ function is executed.
255
+
256
+ The block is optionally passed the {FunctionsFramework::Function} representing
257
+ the function that will be run. You code can, for example, perform different
258
+ initialization depending on the {FunctionsFramework::Function#name} or
259
+ {FunctionsFramework::Function#type}.
260
+
261
+ **In most cases, initialization code should live in an `on_startup` block
262
+ instead of at the "top level" of your Ruby file.** This is because some
263
+ serverless runtimes may load your Ruby code at build or deployment time (for
264
+ example, to verify that it properly defines the requested function), and this
265
+ will execute any code present at the top level of the Ruby file. If top-level
266
+ code is long-running or depends on runtime resources or environment variables,
267
+ this could cause the deployment to fail. By performing initialization in an
268
+ `on_startup` block instead, you ensure it will run only when an actual runtime
269
+ server is starting up, not at build/deployment time.
270
+
271
+ ```ruby
272
+ require "functions_framework"
273
+
274
+ # DO NOT perform initialization here because this could get run at build time.
275
+ # require "my_cache"
276
+ # MyCache.warmup
277
+
278
+ # Instead initialize in an on_startup block, which is executed only when a
279
+ # runtime server is starting up.
280
+ FunctionsFramework.on_startup do
281
+ # Perform initialization here.
282
+ require "my_cache"
283
+ MyCache.warmup
284
+ end
285
+
286
+ # ...
287
+ ```
288
+
289
+ ### The execution context and global data
290
+
291
+ When your function block executes, the _object context_ (i.e. `self`) is set to
292
+ an instance of {FunctionsFramework::Function::Callable}. Each function
293
+ invocation (including functions that might be running concurrently in separate
294
+ threads) runs within a different instance, to help you avoid having functions
295
+ interfere with each other.
296
+
297
+ The object context also defines a few methods that may be useful when writing
298
+ your function.
299
+
300
+ First, you can obtain the logger by calling the
301
+ {FunctionsFramework::Function::Callable#logger} convenience method. This is
302
+ the same logger that is provided by the HTTP request object or by the
303
+ {FunctionsFramework.logger} global method.
304
+
305
+ Second, you can access global shared data by passing a key to
306
+ {FunctionsFramework::Function::Callable#global}. _Global shared data_ is a set
307
+ of key-value pairs that are available to every function invocation. By default,
308
+ two keys are available to all functions:
309
+
310
+ * `:function_name` whose String value is the name of the running function.
311
+ * `:function_type` whose value is either `:http` or `:cloud_event` depending
312
+ on the type of the running function.
313
+
314
+ Following is a simple example using the `logger` and `global` methods of the
315
+ context object:
316
+
317
+ ```ruby
318
+ require "functions_framework"
319
+
320
+ FunctionsFramework.cloud_event "hello" do |event|
321
+ logger.info "Now running the function called #{global(:function_name)}"
322
+ end
323
+ ```
324
+
325
+ To avoid concurrency issues, global shared data is immutable when executing a
326
+ function. You cannot add or delete keys or change the value of existing keys.
327
+ However, the global data is settable during startup tasks, because startup
328
+ tasks never run concurrently. You can use this feature to initialize shared
329
+ resources, as described below.
330
+
331
+ Using the global data mechanism is generally preferred over actual Ruby global
332
+ variables, because the Functions Framework can help you avoid concurrent edits.
333
+ Additionally, the framework will isolate the sets of global data associated
334
+ with different sets of functions, which lets you test functions in isolation
335
+ without the tests interfering with one another by writing to global variables.
336
+
337
+ ### Sharing resources
338
+
339
+ Although functions should generally be self-contained and stateless, it is
340
+ sometimes useful to share certain kinds of resources across multiple function
341
+ invocations that run on the same Ruby instance. For example, you might
342
+ establish a single connection to a remote database or other service, and share
343
+ it across function invocations to avoid incurring the overhead of
344
+ re-establishing it for every function invocation.
345
+
346
+ The best practice for sharing a resource across function invocations is to
347
+ initialize it in a {FunctionsFramework.on_startup} block, and reference it from
348
+ global shared data. (As discussed above, prefer to initialize shared resources
349
+ in a startup task rather than at the top level of a Ruby file, and prefer using
350
+ the Functions Framework's global data mechanism rather than Ruby's global
351
+ variables.)
352
+
353
+ Here is a simple example:
354
+
355
+ ```ruby
356
+ require "functions_framework"
357
+
358
+ # Use an on_startup block to initialize a shared client and store it in
359
+ # the global shared data.
360
+ FunctionsFramework.on_startup do
361
+ require "google/cloud/storage"
362
+ set_global :storage_client, Google::Cloud::Storage.new
363
+ end
364
+
365
+ # The shared storage_client can be accessed by all function invocations
366
+ # via the global shared data.
367
+ FunctionsFramework.http "storage_example" do |request|
368
+ bucket = global(:storage_client).bucket "my-bucket"
369
+ file = bucket.file "path/to/my-file.txt"
370
+ file.download.to_s
371
+ end
372
+ ```
373
+
374
+ Importantly, if you do share a resource across function invocations, make sure
375
+ the resource is thread-safe, so that separate functions running concurrently in
376
+ different threads can access them safely. The API clients provided by Google,
377
+ for example, are thread-safe and can be used concurrently.
378
+
379
+ Also of note: There is no guaranteed cleanup hook. The Functions Framework does
380
+ not provide a way to register a cleanup task, and we recommend against using
381
+ resources that require explicit "cleanup". This is because serverless runtimes
382
+ may perform CPU throttling, and therefore there may not be an opportunity for
383
+ cleanup tasks to run. (For example, you could register a `Kernel.at_exit` task,
384
+ but the Ruby VM may still terminate without calling it.)
261
385
 
262
386
  ## Structuring a project
263
387
 
@@ -271,12 +395,14 @@ and methods that assist in the function implementation.
271
395
  By convention, the "main" Ruby file that defines functions should be called
272
396
  `app.rb` and be located at the root of the project. The path to this file is
273
397
  sometimes known as the **function source**. The Functions Framework allows you
274
- to specify an arbitrary source, but suome hosting environments (such as Google
398
+ to specify an arbitrary source, but some hosting environments (such as Google
275
399
  Cloud Functions) require it to be `./app.rb`.
276
400
 
277
401
  A source file can define any number of functions (with distinct names). Each of
278
402
  the names is known as a **function target**.
279
403
 
404
+ Following is a typical layout for a Functions Framework based project.
405
+
280
406
  ```
281
407
  (project directory)
282
408
  |
@@ -296,13 +422,16 @@ the names is known as a **function target**.
296
422
  ```ruby
297
423
  # Gemfile
298
424
  source "https://rubygems.org"
299
- gem "functions_framework", "~> 0.5"
425
+ gem "functions_framework", "~> 0.7"
300
426
  ```
301
427
 
302
428
  ```ruby
303
429
  # app.rb
304
430
  require "functions_framework"
305
- require_relative "lib/hello"
431
+
432
+ FunctionsFramework.on_startup do
433
+ require_relative "lib/hello"
434
+ end
306
435
 
307
436
  FunctionsFramework.http "hello" do |request|
308
437
  Hello.new(request).build_response
@@ -175,10 +175,8 @@ module FunctionsFramework
175
175
  # run only when preparing to run functions. They are not run, for example,
176
176
  # if an app is loaded to verify its integrity during deployment.
177
177
  #
178
- # Startup tasks are passed two arguments: the {FunctionsFramework::Function}
179
- # identifying the function to execute, and the
180
- # {FunctionsFramework::Server::Config} specifying the (frozen) server
181
- # configuration. Tasks have no return value.
178
+ # Startup tasks are passed the {FunctionsFramework::Function} identifying
179
+ # the function to execute, and have no return value.
182
180
  #
183
181
  # @param block [Proc] The startup task
184
182
  # @return [self]
@@ -189,8 +187,9 @@ module FunctionsFramework
189
187
  end
190
188
 
191
189
  ##
192
- # Start the functions framework server in the background. The server will
193
- # look up the given target function name in the global registry.
190
+ # Run startup tasks, then start the functions framework server in the
191
+ # background. The startup tasks and target function will be looked up in
192
+ # the global registry.
194
193
  #
195
194
  # @param target [FunctionsFramework::Function,String] The function to run,
196
195
  # or the name of the function to look up in the global registry.
@@ -206,8 +205,12 @@ module FunctionsFramework
206
205
  function = global_registry[target]
207
206
  raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
208
207
  end
209
- server = Server.new function, &block
210
- global_registry.run_startup_tasks server
208
+ globals = function.populate_globals
209
+ server = Server.new function, globals, &block
210
+ global_registry.startup_tasks.each do |task|
211
+ task.call function, globals: globals, logger: server.config.logger
212
+ end
213
+ globals.freeze
211
214
  server.respond_to_signals
212
215
  server.start
213
216
  end
@@ -18,44 +18,91 @@ module FunctionsFramework
18
18
  #
19
19
  # A function has a name, a type, and an implementation.
20
20
  #
21
+ # ## Function implementations
22
+ #
21
23
  # The implementation in general is an object that responds to the `call`
22
- # method. For a function of type `:http`, the `call` method takes a single
23
- # `Rack::Request` argument and returns one of various HTTP response types.
24
- # See {FunctionsFramework::Registry.add_http}. For a function of type
25
- # `:cloud_event`, the `call` method takes a single
26
- # [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event)
27
- # argument, and does not return a value.
28
- # See {FunctionsFramework::Registry.add_cloud_event}.
24
+ # method.
25
+ #
26
+ # * For a function of type `:http`, the `call` method takes a single
27
+ # `Rack::Request` argument and returns one of various HTTP response
28
+ # types. See {FunctionsFramework::Registry.add_http}.
29
+ # * For a function of type `:cloud_event`, the `call` method takes a single
30
+ # [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event)
31
+ # argument, and does not return a value. See
32
+ # {FunctionsFramework::Registry.add_cloud_event}.
33
+ # * For a function of type `:startup_task`, the `call` method takes a
34
+ # single {FunctionsFramework::Function} argument, and does not return a
35
+ # value. See {FunctionsFramework::Registry.add_startup_task}.
29
36
  #
30
- # If a callable object is provided directly, its `call` method is invoked for
31
- # every function execution. Note that this means it may be called multiple
32
- # times concurrently in separate threads.
37
+ # The implementation can be specified in one of three ways:
33
38
  #
34
- # Alternately, the implementation may be provided as a class that should be
35
- # instantiated to produce a callable object. If a class is provided, it should
36
- # either subclass {FunctionsFramework::Function::CallBase} or respond to the
37
- # same constructor interface, i.e. accepting arbitrary keyword arguments. A
38
- # separate callable object will be instantiated from this class for every
39
- # function invocation, so each instance will be used for only one invocation.
39
+ # * A callable object can be passed in the `callable` keyword argument. The
40
+ # object's `call` method will be invoked for every function execution.
41
+ # Note that this means it may be called multiple times concurrently in
42
+ # separate threads.
43
+ # * A callable _class_ can be passed in the `callable` keyword argument.
44
+ # This class should subclass {FunctionsFramework::Function::Callable} and
45
+ # define the `call` method. A separate instance of this class will be
46
+ # created for each function invocation.
47
+ # * A block can be provided. It will be used to define the `call` method in
48
+ # an anonymous subclass of {FunctionsFramework::Function::Callable}.
49
+ # Thus, providing a block is really just syntactic sugar for providing a
50
+ # class. (This means, for example, that the `return` keyword will work
51
+ # as expected within the block because it is treated as a method.)
40
52
  #
41
- # Finally, an implementation can be provided as a block. If a block is
42
- # provided, it will be recast as a `call` method in an anonymous subclass of
43
- # {FunctionsFramework::Function::CallBase}. Thus, providing a block is really
44
- # just syntactic sugar for providing a class. (This means, for example, that
45
- # the `return` keyword will work within the block because it is treated as a
46
- # method.)
53
+ # When the implementation is provided as a callable class or block, it is
54
+ # executed in the context of a {FunctionsFramework::Function::Callable}
55
+ # object. This object provides a convenience accessor for the Logger, and
56
+ # access to _globals_, which are data defined by the application startup
57
+ # process and available to each function invocation. Typically, globals are
58
+ # used for shared global resources such as service connections and clients.
47
59
  #
48
60
  class Function
61
+ ##
62
+ # Create a new HTTP function definition.
63
+ #
64
+ # @param name [String] The function name
65
+ # @param callable [Class,#call] A callable object or class.
66
+ # @param block [Proc] The function code as a block.
67
+ # @return [FunctionsFramework::Function]
68
+ #
69
+ def self.http name, callable: nil, &block
70
+ new name, :http, callable: callable, &block
71
+ end
72
+
73
+ ##
74
+ # Create a new CloudEvents function definition.
75
+ #
76
+ # @param name [String] The function name
77
+ # @param callable [Class,#call] A callable object or class.
78
+ # @param block [Proc] The function code as a block.
79
+ # @return [FunctionsFramework::Function]
80
+ #
81
+ def self.cloud_event name, callable: nil, &block
82
+ new name, :cloud_event, callable: callable, &block
83
+ end
84
+
85
+ ##
86
+ # Create a new startup task function definition.
87
+ #
88
+ # @param callable [Class,#call] A callable object or class.
89
+ # @param block [Proc] The function code as a block.
90
+ # @return [FunctionsFramework::Function]
91
+ #
92
+ def self.startup_task callable: nil, &block
93
+ new nil, :startup_task, callable: callable, &block
94
+ end
95
+
49
96
  ##
50
97
  # Create a new function definition.
51
98
  #
52
99
  # @param name [String] The function name
53
- # @param type [Symbol] The type of function. Valid types are `:http` and
54
- # `:cloud_event`.
100
+ # @param type [Symbol] The type of function. Valid types are `:http`,
101
+ # `:cloud_event`, and `:startup_task`.
55
102
  # @param callable [Class,#call] A callable object or class.
56
103
  # @param block [Proc] The function code as a block.
57
104
  #
58
- def initialize name, type, callable = nil, &block
105
+ def initialize name, type, callable: nil, &block
59
106
  @name = name
60
107
  @type = type
61
108
  @callable = @callable_class = nil
@@ -64,7 +111,7 @@ module FunctionsFramework
64
111
  elsif callable.is_a? ::Class
65
112
  @callable_class = callable
66
113
  elsif block_given?
67
- @callable_class = ::Class.new CallBase do
114
+ @callable_class = ::Class.new Callable do
68
115
  define_method :call, &block
69
116
  end
70
117
  else
@@ -83,18 +130,38 @@ module FunctionsFramework
83
130
  attr_reader :type
84
131
 
85
132
  ##
86
- # Get a callable for performing a function invocation. This will either
87
- # return the singleton callable object, or instantiate a new callable from
88
- # the configured class.
89
- #
90
- # @param logger [::Logger] The logger for use by function executions. This
91
- # may or may not be used by the callable.
92
- # @return [#call]
93
- #
94
- def new_call logger: nil
95
- return @callable unless @callable.nil?
96
- logger ||= FunctionsFramework.logger
97
- @callable_class.new logger: logger, function_name: name, function_type: type
133
+ # Populate the given globals hash with this function's info.
134
+ #
135
+ # @param globals [Hash] Initial globals hash (optional).
136
+ # @return [Hash] A new globals hash with this function's info included.
137
+ #
138
+ def populate_globals globals = nil
139
+ result = { function_name: name, function_type: type }
140
+ result.merge! globals if globals
141
+ result
142
+ end
143
+
144
+ ##
145
+ # Call the function given a set of arguments. Set the given logger and/or
146
+ # globals in the context if the callable supports it.
147
+ #
148
+ # If the given arguments exceeds what the function will accept, the args
149
+ # are silently truncated. However, if the function requires more arguments
150
+ # than are provided, an ArgumentError is raised.
151
+ #
152
+ # @param args [Array] Argument to pass to the function.
153
+ # @param logger [Logger] Logger for use by function executions.
154
+ # @param globals [Hash] Globals for the function execution context
155
+ # @return [Object] The function return value.
156
+ #
157
+ def call *args, globals: nil, logger: nil
158
+ callable = @callable || @callable_class.new(globals: globals, logger: logger)
159
+ params = callable.method(:call).parameters.map(&:first)
160
+ unless params.include? :rest
161
+ max_params = params.count(:req) + params.count(:opt)
162
+ args = args.take max_params
163
+ end
164
+ callable.call(*args)
98
165
  end
99
166
 
100
167
  ##
@@ -102,29 +169,56 @@ module FunctionsFramework
102
169
  #
103
170
  # An object of this class is `self` while a function block is running.
104
171
  #
105
- class CallBase
172
+ class Callable
106
173
  ##
107
174
  # Create a callable object with the given context.
108
175
  #
109
- # @param context [keywords] A set of context arguments. See {#context} for
110
- # a list of keys that will generally be passed in. However,
111
- # implementations should be prepared to accept any abritrary keys.
176
+ # @param globals [Hash] A set of globals available to the call.
177
+ # @param logger [Logger] A logger for use by the function call.
112
178
  #
113
- def initialize **context
114
- @context = context
179
+ def initialize globals: nil, logger: nil
180
+ @__globals = globals || {}
181
+ @__logger = logger || FunctionsFramework.logger
115
182
  end
116
183
 
117
184
  ##
118
- # A keyed hash of context information. Common context keys include:
185
+ # Get the given named global.
186
+ #
187
+ # For most function calls, the following globals will be defined:
119
188
  #
120
- # * **:logger** (`Logger`) A logger for use by this function call.
121
189
  # * **:function_name** (`String`) The name of the running function.
122
190
  # * **:function_type** (`Symbol`) The type of the running function,
123
191
  # either `:http` or `:cloud_event`.
124
192
  #
125
- # @return [Hash]
193
+ # You can also set additional globals from a startup task.
194
+ #
195
+ # @param key [Symbol,String] The name of the global to get.
196
+ # @return [Object]
126
197
  #
127
- attr_reader :context
198
+ def global key
199
+ @__globals[key]
200
+ end
201
+
202
+ ##
203
+ # Set a global. This can be called from startup tasks, but the globals
204
+ # are frozen when the server starts, so this call will raise an exception
205
+ # if called from a normal function.
206
+ #
207
+ # @param key [Symbol,String]
208
+ # @param value [Object]
209
+ #
210
+ def set_global key, value
211
+ @__globals[key] = value
212
+ end
213
+
214
+ ##
215
+ # A logger for use by this call.
216
+ #
217
+ # @return [Logger]
218
+ #
219
+ def logger
220
+ @__logger
221
+ end
128
222
  end
129
223
  end
130
224
  end
@@ -12,20 +12,16 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "monitor"
16
-
17
15
  module FunctionsFramework
18
16
  ##
19
17
  # Registry providing lookup of functions by name.
20
18
  #
21
19
  class Registry
22
- include ::MonitorMixin
23
-
24
20
  ##
25
21
  # Create a new empty registry.
26
22
  #
27
23
  def initialize
28
- @mutex = ::Monitor.new
24
+ @mutex = ::Mutex.new
29
25
  @functions = {}
30
26
  @start_tasks = []
31
27
  end
@@ -51,17 +47,12 @@ module FunctionsFramework
51
47
  end
52
48
 
53
49
  ##
54
- # Run all startup tasks.
50
+ # Return an array of startup tasks.
55
51
  #
56
- # @param server [FunctionsFramework::Server] The server that is starting.
57
- # @return [self]
52
+ # @return [Array<FunctionsFramework::Function>]
58
53
  #
59
- def run_startup_tasks server
60
- tasks = @mutex.synchronize { @start_tasks.dup }
61
- tasks.each do |task|
62
- task.call server.function, server.config
63
- end
64
- self
54
+ def startup_tasks
55
+ @mutex.synchronize { @start_tasks.dup }
65
56
  end
66
57
 
67
58
  ##
@@ -85,7 +76,7 @@ module FunctionsFramework
85
76
  name = name.to_s
86
77
  @mutex.synchronize do
87
78
  raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
88
- @functions[name] = Function.new name, :http, &block
79
+ @functions[name] = Function.http name, &block
89
80
  end
90
81
  self
91
82
  end
@@ -106,7 +97,7 @@ module FunctionsFramework
106
97
  name = name.to_s
107
98
  @mutex.synchronize do
108
99
  raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
109
- @functions[name] = Function.new name, :cloud_event, &block
100
+ @functions[name] = Function.cloud_event name, &block
110
101
  end
111
102
  self
112
103
  end
@@ -115,16 +106,15 @@ module FunctionsFramework
115
106
  # Add a startup task.
116
107
  #
117
108
  # Startup tasks are generally run just before a server starts. They are
118
- # passed two arguments: the {FunctionsFramework::Function} identifying the
119
- # function to execute, and the {FunctionsFramework::Server::Config}
120
- # specifying the (frozen) server configuration. Tasks have no return value.
109
+ # passed the {FunctionsFramework::Function} identifying the function to
110
+ # execute, and have no return value.
121
111
  #
122
112
  # @param block [Proc] The startup task
123
113
  # @return [self]
124
114
  #
125
115
  def add_startup_task &block
126
116
  @mutex.synchronize do
127
- @start_tasks << block
117
+ @start_tasks << Function.startup_task(&block)
128
118
  end
129
119
  self
130
120
  end
@@ -27,17 +27,22 @@ module FunctionsFramework
27
27
  include ::MonitorMixin
28
28
 
29
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.
30
+ # Create a new web server given a function definition, a set of application
31
+ # globals, and server configuration.
32
+ #
33
+ # To configure the server, pass a block that takes a
34
+ # {FunctionsFramework::Server::Config} object as the parameter. This block
35
+ # is the only opportunity to modify the configuration; once the server is
36
+ # initialized, configuration is frozen.
35
37
  #
36
38
  # @param function [FunctionsFramework::Function] The function to execute.
39
+ # @param globals [Hash] Globals to pass to invocations. This hash should
40
+ # normally be frozen so separate function invocations cannot interfere
41
+ # with one another's globals.
37
42
  # @yield [FunctionsFramework::Server::Config] A config object that can be
38
43
  # manipulated to configure this server.
39
44
  #
40
- def initialize function
45
+ def initialize function, globals
41
46
  super()
42
47
  @config = Config.new
43
48
  yield @config if block_given?
@@ -46,9 +51,9 @@ module FunctionsFramework
46
51
  @app =
47
52
  case function.type
48
53
  when :http
49
- HttpApp.new function, @config
54
+ HttpApp.new function, globals, @config
50
55
  when :cloud_event
51
- EventApp.new function, @config
56
+ EventApp.new function, globals, @config
52
57
  else
53
58
  raise "Unrecognized function type: #{function.type}"
54
59
  end
@@ -379,9 +384,10 @@ module FunctionsFramework
379
384
 
380
385
  ## @private
381
386
  class HttpApp < AppBase
382
- def initialize function, config
387
+ def initialize function, globals, config
383
388
  super config
384
389
  @function = function
390
+ @globals = globals
385
391
  end
386
392
 
387
393
  def call env
@@ -391,8 +397,7 @@ module FunctionsFramework
391
397
  logger = env["rack.logger"] ||= @config.logger
392
398
  request = ::Rack::Request.new env
393
399
  logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
394
- calling_context = @function.new_call logger: logger
395
- calling_context.call request
400
+ @function.call request, globals: @globals, logger: logger
396
401
  rescue ::StandardError => e
397
402
  e
398
403
  end
@@ -402,9 +407,10 @@ module FunctionsFramework
402
407
 
403
408
  ## @private
404
409
  class EventApp < AppBase
405
- def initialize function, config
410
+ def initialize function, globals, config
406
411
  super config
407
412
  @function = function
413
+ @globals = globals
408
414
  @cloud_events = ::CloudEvents::HttpBinding.default
409
415
  @legacy_events = LegacyEventConverter.new
410
416
  end
@@ -439,8 +445,7 @@ module FunctionsFramework
439
445
 
440
446
  def handle_cloud_event event, logger
441
447
  logger.info "FunctionsFramework: Handling CloudEvent"
442
- calling_context = @function.new_call logger: logger
443
- calling_context.call event
448
+ @function.call event, globals: @globals, logger: logger
444
449
  "ok"
445
450
  rescue ::StandardError => e
446
451
  e
@@ -75,19 +75,71 @@ module FunctionsFramework
75
75
  Testing.load_for_testing path, &block
76
76
  end
77
77
 
78
+ ##
79
+ # Run startup tasks for the given function name and return the initialized
80
+ # globals hash.
81
+ #
82
+ # Normally, this will be run automatically prior to the first call to the
83
+ # function using {call_http} or {call_event}, if it has not already been
84
+ # run. However, you can call it explicitly to test its behavior. It cannot
85
+ # be called more than once for any given function.
86
+ #
87
+ # By default, the {FunctionsFramework.logger} will be used, but you can
88
+ # override that by providing your own logger. In particular, to disable
89
+ # logging, you can pass `Logger.new(nil)`.
90
+ #
91
+ # @param name [String] The name of the function to start up.
92
+ # @param logger [Logger] Use the given logger instead of the Functions
93
+ # Framework's global logger. Optional.
94
+ # @param lenient [Boolean] If false (the default), raise an error if the
95
+ # given function has already had its startup tasks run. If true,
96
+ # duplicate requests to run startup tasks are ignored.
97
+ # @return [Hash] The initialized globals.
98
+ #
99
+ def run_startup_tasks name, logger: nil, lenient: false
100
+ function = Testing.current_registry[name]
101
+ raise "Unknown function name #{name}" unless function
102
+ globals = Testing.current_globals name
103
+ if globals
104
+ raise "Function #{name} has already been started up" unless lenient
105
+ else
106
+ globals = function.populate_globals
107
+ Testing.current_registry.startup_tasks.each do |task|
108
+ task.call function, globals: globals, logger: logger
109
+ end
110
+ Testing.current_globals name, globals
111
+ end
112
+ globals.freeze
113
+ end
114
+
78
115
  ##
79
116
  # Call the given HTTP function for testing. The underlying function must
80
- # be of type `:http`.
117
+ # be of type `:http`. Returns the Rack response.
118
+ #
119
+ # By default, the startup tasks will be run for the given function if they
120
+ # have not already been run. You can, however, disable running startup
121
+ # tasks by providing an explicit globals hash.
122
+ #
123
+ # By default, the {FunctionsFramework.logger} will be used, but you can
124
+ # override that by providing your own logger. In particular, to disable
125
+ # logging, you can pass `Logger.new(nil)`.
81
126
  #
82
127
  # @param name [String] The name of the function to call
83
128
  # @param request [Rack::Request] The Rack request to send
129
+ # @param globals [Hash] Do not run startup tasks, and instead provide the
130
+ # globals directly. Optional.
131
+ # @param logger [Logger] Use the given logger instead of the Functions
132
+ # Framework's global logger. Optional.
84
133
  # @return [Rack::Response]
85
134
  #
86
- def call_http name, request
87
- function = ::FunctionsFramework.global_registry[name]
135
+ def call_http name, request, globals: nil, logger: nil
136
+ globals ||= run_startup_tasks name, logger: logger, lenient: true
137
+ function = Testing.current_registry[name]
88
138
  case function&.type
89
139
  when :http
90
- Testing.interpret_response { function.new_call.call request }
140
+ Testing.interpret_response do
141
+ function.call request, globals: globals, logger: logger
142
+ end
91
143
  when nil
92
144
  raise "Unknown function name #{name}"
93
145
  else
@@ -99,15 +151,28 @@ module FunctionsFramework
99
151
  # Call the given event function for testing. The underlying function must
100
152
  # be of type :cloud_event`.
101
153
  #
154
+ # By default, the startup tasks will be run for the given function if they
155
+ # have not already been run. You can, however, disable running startup
156
+ # tasks by providing an explicit globals hash.
157
+ #
158
+ # By default, the {FunctionsFramework.logger} will be used, but you can
159
+ # override that by providing your own logger. In particular, to disable
160
+ # logging, you can pass `Logger.new(nil)`.
161
+ #
102
162
  # @param name [String] The name of the function to call
103
163
  # @param event [::CloudEvents::Event] The event to send
164
+ # @param globals [Hash] Do not run startup tasks, and instead provide the
165
+ # globals directly. Optional.
166
+ # @param logger [Logger] Use the given logger instead of the Functions
167
+ # Framework's global logger. Optional.
104
168
  # @return [nil]
105
169
  #
106
- def call_event name, event
107
- function = ::FunctionsFramework.global_registry[name]
170
+ def call_event name, event, globals: nil, logger: nil
171
+ globals ||= run_startup_tasks name, logger: logger, lenient: true
172
+ function = Testing.current_registry[name]
108
173
  case function&.type
109
174
  when :cloud_event
110
- function.new_call.call event
175
+ function.call event, globals: globals, logger: logger
111
176
  nil
112
177
  when nil
113
178
  raise "Unknown function name #{name}"
@@ -208,27 +273,49 @@ module FunctionsFramework
208
273
  extend self
209
274
 
210
275
  @testing_registries = {}
276
+ @main_globals = {}
211
277
  @mutex = ::Mutex.new
212
278
 
213
279
  class << self
214
280
  ## @private
215
281
  def load_for_testing path
216
282
  old_registry = ::FunctionsFramework.global_registry
217
- @mutex.synchronize do
218
- if @testing_registries.key? path
219
- ::FunctionsFramework.global_registry = @testing_registries[path]
220
- else
221
- new_registry = ::FunctionsFramework::Registry.new
222
- ::FunctionsFramework.global_registry = new_registry
223
- ::Kernel.load path
224
- @testing_registries[path] = new_registry
283
+ ::Thread.current[:functions_framework_testing_registry] =
284
+ @mutex.synchronize do
285
+ if @testing_registries.key? path
286
+ ::FunctionsFramework.global_registry = @testing_registries[path]
287
+ else
288
+ new_registry = ::FunctionsFramework::Registry.new
289
+ ::FunctionsFramework.global_registry = new_registry
290
+ ::Kernel.load path
291
+ @testing_registries[path] = new_registry
292
+ end
225
293
  end
226
- end
294
+ ::Thread.current[:functions_framework_testing_globals] = {}
227
295
  yield
228
296
  ensure
297
+ ::Thread.current[:functions_framework_testing_registry] = nil
298
+ ::Thread.current[:functions_framework_testing_globals] = nil
229
299
  ::FunctionsFramework.global_registry = old_registry
230
300
  end
231
301
 
302
+ ## @private
303
+ def current_registry
304
+ ::Thread.current[:functions_framework_testing_registry] ||
305
+ ::FunctionsFramework.global_registry
306
+ end
307
+
308
+ ## @private
309
+ def current_globals name, globals = nil
310
+ name = name.to_s
311
+ globals_by_name = ::Thread.current[:functions_framework_testing_globals] || @main_globals
312
+ if globals
313
+ globals_by_name[name] = globals
314
+ else
315
+ globals_by_name[name]
316
+ end
317
+ end
318
+
232
319
  ## @private
233
320
  def interpret_response
234
321
  response =
@@ -17,5 +17,5 @@ module FunctionsFramework
17
17
  # Version of the Ruby Functions Framework
18
18
  # @return [String]
19
19
  #
20
- VERSION = "0.6.0".freeze
20
+ VERSION = "0.7.0".freeze
21
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: functions_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-17 00:00:00.000000000 Z
11
+ date: 2020-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cloud_events
@@ -54,8 +54,8 @@ dependencies:
54
54
  version: '2.1'
55
55
  description: The Functions Framework is an open source framework for writing lightweight,
56
56
  portable Ruby functions that run in a serverless environment. Functions written
57
- to this Framework will run Google Cloud Google Cloud Functions, Google Cloud Run,
58
- or any other Knative-based environment.
57
+ to this Framework will run on Google Cloud Functions, Google Cloud Run, or any other
58
+ Knative-based environment.
59
59
  email:
60
60
  - dazuma@google.com
61
61
  executables:
@@ -87,10 +87,10 @@ homepage: https://github.com/GoogleCloudPlatform/functions-framework-ruby
87
87
  licenses:
88
88
  - Apache-2.0
89
89
  metadata:
90
- changelog_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.6.0/file.CHANGELOG.html
90
+ changelog_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.7.0/file.CHANGELOG.html
91
91
  source_code_uri: https://github.com/GoogleCloudPlatform/functions-framework-ruby
92
92
  bug_tracker_uri: https://github.com/GoogleCloudPlatform/functions-framework-ruby/issues
93
- documentation_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.6.0
93
+ documentation_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.7.0
94
94
  post_install_message:
95
95
  rdoc_options: []
96
96
  require_paths:
@@ -99,7 +99,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: 2.4.0
102
+ version: 2.5.0
103
103
  required_rubygems_version: !ruby/object:Gem::Requirement
104
104
  requirements:
105
105
  - - ">="