functions_framework 0.5.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -71,7 +71,7 @@ require "functions_framework/version"
71
71
  #
72
72
  module FunctionsFramework
73
73
  @global_registry = Registry.new
74
- @logger = ::Logger.new ::STDERR
74
+ @logger = ::Logger.new $stderr
75
75
  @logger.level = ::Logger::INFO
76
76
 
77
77
  ##
@@ -166,8 +166,30 @@ module FunctionsFramework
166
166
  end
167
167
 
168
168
  ##
169
- # Start the functions framework server in the background. The server will
170
- # look up the given target function name in the global registry.
169
+ # Define a server startup task. This is useful for initializing shared
170
+ # resources that should be accessible across all function invocations in
171
+ # this Ruby VM.
172
+ #
173
+ # Startup tasks are run just before a server starts. All startup tasks are
174
+ # guaranteed to complete before any function executes. However, they are
175
+ # run only when preparing to run functions. They are not run, for example,
176
+ # if an app is loaded to verify its integrity during deployment.
177
+ #
178
+ # Startup tasks are passed the {FunctionsFramework::Function} identifying
179
+ # the function to execute, and have no return value.
180
+ #
181
+ # @param block [Proc] The startup task
182
+ # @return [self]
183
+ #
184
+ def on_startup &block
185
+ global_registry.add_startup_task(&block)
186
+ self
187
+ end
188
+
189
+ ##
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.
171
193
  #
172
194
  # @param target [FunctionsFramework::Function,String] The function to run,
173
195
  # or the name of the function to look up in the global registry.
@@ -183,7 +205,12 @@ module FunctionsFramework
183
205
  function = global_registry[target]
184
206
  raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
185
207
  end
186
- server = Server.new function, &block
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
187
214
  server.respond_to_signals
188
215
  server.start
189
216
  end
@@ -42,8 +42,31 @@ module FunctionsFramework
42
42
  @detailed_errors = nil
43
43
  @signature_type = ::ENV["FUNCTION_SIGNATURE_TYPE"]
44
44
  @logging_level = init_logging_level
45
+ @what_to_do = nil
46
+ @error_message = nil
47
+ @exit_code = 0
45
48
  end
46
49
 
50
+ ##
51
+ # Determine if an error has occurred
52
+ #
53
+ # @return [boolean]
54
+ #
55
+ def error?
56
+ !@error_message.nil?
57
+ end
58
+
59
+ ##
60
+ # @return [Integer] The current exit status.
61
+ #
62
+ attr_reader :exit_code
63
+
64
+ ##
65
+ # @return [String] The current error message.
66
+ # @return [nil] if no error has occurred.
67
+ #
68
+ attr_reader :error_message
69
+
47
70
  ##
48
71
  # Parse the given command line arguments.
49
72
  # Exits if argument parsing failed.
@@ -51,8 +74,8 @@ module FunctionsFramework
51
74
  # @param argv [Array<String>]
52
75
  # @return [self]
53
76
  #
54
- def parse_args argv # rubocop:disable Metrics/MethodLength
55
- option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
77
+ def parse_args argv # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
78
+ @option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
56
79
  op.on "-t", "--target TARGET",
57
80
  "Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
58
81
  @target = val
@@ -84,55 +107,92 @@ module FunctionsFramework
84
107
  op.on "--[no-]detailed-errors", "Set whether to show error details" do |val|
85
108
  @detailed_errors = val
86
109
  end
110
+ op.on "--verify", "Verify the app only, but do not run the server." do
111
+ @what_to_do ||= :verify
112
+ end
87
113
  op.on "-v", "--verbose", "Increase log verbosity" do
88
114
  @logging_level -= 1
89
115
  end
90
116
  op.on "-q", "--quiet", "Decrease log verbosity" do
91
117
  @logging_level += 1
92
118
  end
119
+ op.on "--version", "Display the framework version" do
120
+ @what_to_do ||= :version
121
+ end
93
122
  op.on "--help", "Display help" do
94
- puts op
95
- exit
123
+ @what_to_do ||= :help
96
124
  end
97
125
  end
98
- option_parser.parse! argv
99
- error "Unrecognized arguments: #{argv}\n#{op}" unless argv.empty?
126
+ begin
127
+ @option_parser.parse! argv
128
+ error! "Unrecognized arguments: #{argv}\n#{@option_parser}", 2 unless argv.empty?
129
+ rescue ::OptionParser::ParseError => e
130
+ error! "#{e.message}\n#{@option_parser}", 2
131
+ end
100
132
  self
101
133
  end
102
134
 
103
135
  ##
104
- # Run the configured server, and block until it stops.
105
- # If a validation error occurs, print a message and exit.
136
+ # Perform the requested function.
137
+ #
138
+ # * If the `--version` flag was given, display the version.
139
+ # * If the `--help` flag was given, display online help.
140
+ # * If the `--verify` flag was given, load and verify the function,
141
+ # displaying any errors, then exit without starting a server.
142
+ # * Otherwise, start the configured server and block until it stops.
106
143
  #
107
144
  # @return [self]
108
145
  #
109
146
  def run
110
- begin
111
- server = start_server
112
- rescue ::StandardError => e
113
- error e.message
147
+ return self if error?
148
+ case @what_to_do
149
+ when :version
150
+ puts ::FunctionsFramework::VERSION
151
+ when :help
152
+ puts @option_parser
153
+ when :verify
154
+ begin
155
+ load_function
156
+ puts "OK"
157
+ rescue ::StandardError => e
158
+ error! e.message
159
+ end
160
+ else
161
+ begin
162
+ start_server.wait_until_stopped
163
+ rescue ::StandardError => e
164
+ error! e.message
165
+ end
114
166
  end
115
- server.wait_until_stopped
116
167
  self
117
168
  end
118
169
 
119
170
  ##
120
- # Start the configured server and return the running server object.
171
+ # Finish the CLI, displaying any error status and exiting with the current
172
+ # exit code. Never returns.
173
+ #
174
+ def complete
175
+ warn @error_message if @error_message
176
+ exit @exit_code
177
+ end
178
+
179
+ ##
180
+ # Load the source and get and verify the requested function.
121
181
  # If a validation error occurs, raise an exception.
122
- # This is used for testing the CLI.
123
182
  #
124
- # @return [FunctionsFramework::Server]
183
+ # @return [FunctionsFramework::Function]
125
184
  #
126
185
  # @private
127
186
  #
128
- def start_server
187
+ def load_function
129
188
  ::FunctionsFramework.logger.level = @logging_level
130
- ::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION} server starting."
189
+ ::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION}"
131
190
  ::ENV["FUNCTION_TARGET"] = @target
132
191
  ::ENV["FUNCTION_SOURCE"] = @source
133
192
  ::ENV["FUNCTION_SIGNATURE_TYPE"] = @signature_type
134
193
  ::FunctionsFramework.logger.info "FunctionsFramework: Loading functions from #{@source.inspect}..."
135
194
  load @source
195
+ ::FunctionsFramework.logger.info "FunctionsFramework: Looking for function name #{@target.inspect}..."
136
196
  function = ::FunctionsFramework.global_registry[@target]
137
197
  raise "Undefined function: #{@target.inspect}" if function.nil?
138
198
  unless @signature_type.nil? ||
@@ -140,6 +200,20 @@ module FunctionsFramework
140
200
  ["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event
141
201
  raise "Function #{@target.inspect} does not match type #{@signature_type}"
142
202
  end
203
+ function
204
+ end
205
+
206
+ ##
207
+ # Start the configured server and return the running server object.
208
+ # If a validation error occurs, raise an exception.
209
+ #
210
+ # @return [FunctionsFramework::Server]
211
+ #
212
+ # @private
213
+ #
214
+ def start_server
215
+ function = load_function
216
+ ::FunctionsFramework.logger.info "FunctionsFramework: Starting server..."
143
217
  ::FunctionsFramework.start function do |config|
144
218
  config.rack_env = @env
145
219
  config.port = @port
@@ -160,12 +234,13 @@ module FunctionsFramework
160
234
  end
161
235
 
162
236
  ##
163
- # Print the given error message and exit.
164
- # @param message [String]
237
+ # Set the error status.
238
+ # @param message [String] Error message.
239
+ # @param code [Integer] Exit code, defaults to 1.
165
240
  #
166
- def error message
167
- warn message
168
- exit 1
241
+ def error! message, code = 1
242
+ @error_message = message
243
+ @exit_code = code
169
244
  end
170
245
  end
171
246
  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,60 @@ 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)
165
+ end
166
+
167
+ ##
168
+ # A lazy evaluator for a global
169
+ # @private
170
+ #
171
+ class LazyGlobal
172
+ def initialize block
173
+ @block = block
174
+ @value = nil
175
+ @mutex = ::Mutex.new
176
+ end
177
+
178
+ def value
179
+ @mutex.synchronize do
180
+ if @block
181
+ @value = @block.call
182
+ @block = nil
183
+ end
184
+ @value
185
+ end
186
+ end
98
187
  end
99
188
 
100
189
  ##
@@ -102,29 +191,82 @@ module FunctionsFramework
102
191
  #
103
192
  # An object of this class is `self` while a function block is running.
104
193
  #
105
- class CallBase
194
+ class Callable
106
195
  ##
107
196
  # Create a callable object with the given context.
108
197
  #
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.
198
+ # @param globals [Hash] A set of globals available to the call.
199
+ # @param logger [Logger] A logger for use by the function call.
112
200
  #
113
- def initialize **context
114
- @context = context
201
+ def initialize globals: nil, logger: nil
202
+ @__globals = globals || {}
203
+ @__logger = logger || FunctionsFramework.logger
115
204
  end
116
205
 
117
206
  ##
118
- # A keyed hash of context information. Common context keys include:
207
+ # Get the given named global.
208
+ #
209
+ # For most function calls, the following globals will be defined:
119
210
  #
120
- # * **:logger** (`Logger`) A logger for use by this function call.
121
211
  # * **:function_name** (`String`) The name of the running function.
122
212
  # * **:function_type** (`Symbol`) The type of the running function,
123
213
  # either `:http` or `:cloud_event`.
124
214
  #
125
- # @return [Hash]
215
+ # You can also set additional globals from a startup task.
216
+ #
217
+ # @param key [Symbol,String] The name of the global to get.
218
+ # @return [Object]
219
+ #
220
+ def global key
221
+ value = @__globals[key]
222
+ value = value.value if value.is_a? LazyGlobal
223
+ value
224
+ end
225
+
226
+ ##
227
+ # Set a global. This can be called from startup tasks, but the globals
228
+ # are frozen when the server starts, so this call will raise an exception
229
+ # if called from a normal function.
230
+ #
231
+ # You can set a global to a final value, or you can provide a block that
232
+ # lazily computes the global the first time it is requested.
233
+ #
234
+ # @overload set_global(key, value)
235
+ # Set the given global to the given value. For example:
236
+ #
237
+ # set_global(:project_id, "my-project-id")
238
+ #
239
+ # @param key [Symbol,String]
240
+ # @param value [Object]
241
+ # @return [self]
242
+ #
243
+ # @overload set_global(key, &block)
244
+ # Call the given block to compute the global's value only when the
245
+ # value is actually requested. This block will be called at most once,
246
+ # and its result reused for subsequent calls. For example:
247
+ #
248
+ # set_global(:connection_pool) do
249
+ # ExpensiveConnectionPool.new
250
+ # end
126
251
  #
127
- attr_reader :context
252
+ # @param key [Symbol,String]
253
+ # @param block [Proc] A block that lazily computes a value
254
+ # @yieldreturn [Object] The value
255
+ # @return [self]
256
+ #
257
+ def set_global key, value = nil, &block
258
+ @__globals[key] = block ? LazyGlobal.new(block) : value
259
+ self
260
+ end
261
+
262
+ ##
263
+ # A logger for use by this call.
264
+ #
265
+ # @return [Logger]
266
+ #
267
+ def logger
268
+ @__logger
269
+ end
128
270
  end
129
271
  end
130
272
  end