functions_framework 0.6.0 → 0.7.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.
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
  - - ">="